From 79ade5d05ead5c020adcaa0220e42149ef18683d Mon Sep 17 00:00:00 2001 From: ToineSiebelink Date: Thu, 1 Oct 2020 14:43:49 +0100 Subject: [PATCH] introducing YangUtils with tests Issue-ID: CCSDK-2757 Jira Link: https://jira.onap.org/browse/CCSDK-2757 Change-Id: I3c396ef1e29e9f30027702f3d36ee3bbb1de9b8e --- .../java/org/onap/cps/api/impl/CpServiceImpl.java | 28 ++---- .../main/java/org/onap/cps/utils/YangUtils.java | 99 ++++++++++++++++++++++ .../org/onap/cps/api/impl/CpServiceImplSpec.groovy | 30 +++++-- .../groovy/org/onap/cps/utils/YangUtilsSpec.groovy | 80 +++++++++++++++++ .../src/test/java/org/onap/cps/TestUtils.java | 41 +++++++++ cps/cps-service/src/test/resources/bookstore.json | 34 ++++++++ cps/cps-service/src/test/resources/bookstore.yang | 50 +++++++++++ cps/cps-service/src/test/resources/invalid.yang | 1 + .../src/test/resources/someOtherFile.txt | 0 9 files changed, 333 insertions(+), 30 deletions(-) create mode 100644 cps/cps-service/src/main/java/org/onap/cps/utils/YangUtils.java create mode 100644 cps/cps-service/src/test/groovy/org/onap/cps/utils/YangUtilsSpec.groovy create mode 100644 cps/cps-service/src/test/java/org/onap/cps/TestUtils.java create mode 100644 cps/cps-service/src/test/resources/bookstore.json create mode 100644 cps/cps-service/src/test/resources/bookstore.yang create mode 100644 cps/cps-service/src/test/resources/invalid.yang create mode 100644 cps/cps-service/src/test/resources/someOtherFile.txt diff --git a/cps/cps-service/src/main/java/org/onap/cps/api/impl/CpServiceImpl.java b/cps/cps-service/src/main/java/org/onap/cps/api/impl/CpServiceImpl.java index 8d6b63bd5..cb8e20c8a 100644 --- a/cps/cps-service/src/main/java/org/onap/cps/api/impl/CpServiceImpl.java +++ b/cps/cps-service/src/main/java/org/onap/cps/api/impl/CpServiceImpl.java @@ -24,18 +24,14 @@ import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; import java.io.IOException; -import java.util.Iterator; -import java.util.ServiceLoader; import org.onap.cps.api.CpService; import org.onap.cps.spi.DataPersistencyService; import org.onap.cps.spi.ModelPersistencyService; +import org.onap.cps.utils.YangUtils; import org.opendaylight.yangtools.yang.model.api.Module; import org.opendaylight.yangtools.yang.model.api.SchemaContext; -import org.opendaylight.yangtools.yang.model.parser.api.YangParser; import org.opendaylight.yangtools.yang.model.parser.api.YangParserException; import org.opendaylight.yangtools.yang.model.parser.api.YangParserFactory; -import org.opendaylight.yangtools.yang.model.repo.api.StatementParserMode; -import org.opendaylight.yangtools.yang.model.repo.api.YangTextSchemaSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -49,16 +45,7 @@ public class CpServiceImpl implements CpService { private static final YangParserFactory PARSER_FACTORY; - static { - final Iterator it = - ServiceLoader.load(YangParserFactory.class).iterator(); - if (!it.hasNext()) { - throw new IllegalStateException("No YangParserFactory found"); - } - PARSER_FACTORY = it.next(); - } - - @Autowired + @Autowired private ModelPersistencyService modelPersistencyService; @Autowired @@ -66,23 +53,18 @@ public class CpServiceImpl implements CpService { @Override - public final SchemaContext parseAndValidateModel(final String yangModelContent) - throws IOException, YangParserException { + public final SchemaContext parseAndValidateModel(final String yangModelContent) throws IOException, + YangParserException { final File tempFile = File.createTempFile("yang", ".yang"); try (BufferedWriter writer = new BufferedWriter(new FileWriter(tempFile))) { writer.write(yangModelContent); - } catch (final IOException e) { - LOGGER.error("Unable to write to temporary file {}", e.getMessage()); } return parseAndValidateModel(tempFile); } @Override public final SchemaContext parseAndValidateModel(final File yangModelFile) throws IOException, YangParserException { - final YangTextSchemaSource yangTextSchemaSource = YangTextSchemaSource.forFile(yangModelFile); - final YangParser yangParser = PARSER_FACTORY.createParser(StatementParserMode.DEFAULT_MODE); - yangParser.addSource(yangTextSchemaSource); - return yangParser.buildEffectiveModel(); + return YangUtils.parseYangModelFile(yangModelFile); } @Override diff --git a/cps/cps-service/src/main/java/org/onap/cps/utils/YangUtils.java b/cps/cps-service/src/main/java/org/onap/cps/utils/YangUtils.java new file mode 100644 index 000000000..e9757eca0 --- /dev/null +++ b/cps/cps-service/src/main/java/org/onap/cps/utils/YangUtils.java @@ -0,0 +1,99 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2020 Nordix Foundation + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps.utils; + +import com.google.gson.stream.JsonReader; +import java.io.File; +import java.io.IOException; +import java.io.StringReader; +import java.util.Iterator; +import java.util.ServiceLoader; +import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; +import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter; +import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactory; +import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactorySupplier; +import org.opendaylight.yangtools.yang.data.codec.gson.JsonParserStream; +import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNormalizedNodeStreamWriter; +import org.opendaylight.yangtools.yang.data.impl.schema.NormalizedNodeResult; +import org.opendaylight.yangtools.yang.model.api.SchemaContext; +import org.opendaylight.yangtools.yang.model.parser.api.YangParser; +import org.opendaylight.yangtools.yang.model.parser.api.YangParserException; +import org.opendaylight.yangtools.yang.model.parser.api.YangParserFactory; +import org.opendaylight.yangtools.yang.model.repo.api.StatementParserMode; +import org.opendaylight.yangtools.yang.model.repo.api.YangTextSchemaSource; + +public class YangUtils { + + private static final YangParserFactory PARSER_FACTORY; + + private YangUtils() { + throw new IllegalStateException("Utility class"); + } + + static { + final Iterator it = ServiceLoader.load(YangParserFactory.class).iterator(); + if (!it.hasNext()) { + throw new IllegalStateException("No YangParserFactory found"); + } + PARSER_FACTORY = it.next(); + } + + /** + * Parse a file containing yang modules. + * @param yangModelFile a file containing one or more yang modules + * (please note the file has to have a .yang extension if not an exception will be thrown) + * @return a SchemaContext representing the yang model + * @throws IOException when the system as an IO issue + * @throws YangParserException when the file does not contain a valid yang structure + */ + public static SchemaContext parseYangModelFile(final File yangModelFile) throws IOException, YangParserException { + YangTextSchemaSource yangTextSchemaSource = YangTextSchemaSource.forFile(yangModelFile); + final YangParser yangParser = PARSER_FACTORY + .createParser(StatementParserMode.DEFAULT_MODE); + yangParser.addSource(yangTextSchemaSource); + return yangParser.buildEffectiveModel(); + } + + /** + * Parse a file containing json data for a certain model (schemaContext). + * @param jsonData a string containing json data for the given model + * @param schemaContext the SchemaContext for the given data + * @return the NormalizedNode representing the json data + */ + public static NormalizedNode parseJsonData(final String jsonData, final SchemaContext schemaContext) + throws IOException { + JSONCodecFactory jsonCodecFactory = JSONCodecFactorySupplier.DRAFT_LHOTKA_NETMOD_YANG_JSON_02 + .getShared(schemaContext); + final NormalizedNodeResult normalizedNodeResult = new NormalizedNodeResult(); + final NormalizedNodeStreamWriter normalizedNodeStreamWriter = ImmutableNormalizedNodeStreamWriter + .from(normalizedNodeResult); + try (JsonParserStream jsonParserStream = JsonParserStream + .create(normalizedNodeStreamWriter, jsonCodecFactory)) { + final JsonReader jsonReader = new JsonReader(new StringReader(jsonData)); + jsonParserStream.parse(jsonReader); + } + return normalizedNodeResult.getResult(); + } + + public static void chopNormalizedNode(NormalizedNode tree) { + //TODO Toine Siebelink, add code from proto-type (other user story) + } + +} diff --git a/cps/cps-service/src/test/groovy/org/onap/cps/api/impl/CpServiceImplSpec.groovy b/cps/cps-service/src/test/groovy/org/onap/cps/api/impl/CpServiceImplSpec.groovy index 27f748211..a1c9dd951 100644 --- a/cps/cps-service/src/test/groovy/org/onap/cps/api/impl/CpServiceImplSpec.groovy +++ b/cps/cps-service/src/test/groovy/org/onap/cps/api/impl/CpServiceImplSpec.groovy @@ -1,3 +1,22 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2020 Nordix Foundation + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + package org.onap.cps.api.impl import org.onap.cps.spi.DataPersistencyService @@ -14,14 +33,11 @@ class CpServiceImplSpec extends Specification { objectUnderTest.dataPersistencyService = dataPersistencyService; } - def 'Storing a json object'() { - given: 'that the data persistency service returns an id of 123' + def 'Cps Service provides to its client the id assigned by the system when storing a data structure'() { + given: 'that data persistency service is giving id 123 to a data structure it is asked to store' dataPersistencyService.storeJsonStructure(_) >> 123 - when: 'a json structure is stored using the data persistency service' - def result = objectUnderTest.storeJsonStructure('') - - then: ' the same id is returned' - result == 123 + expect: 'Cps service returns the same id when storing data structure' + objectUnderTest.storeJsonStructure('') == 123 } } diff --git a/cps/cps-service/src/test/groovy/org/onap/cps/utils/YangUtilsSpec.groovy b/cps/cps-service/src/test/groovy/org/onap/cps/utils/YangUtilsSpec.groovy new file mode 100644 index 000000000..8aabc4844 --- /dev/null +++ b/cps/cps-service/src/test/groovy/org/onap/cps/utils/YangUtilsSpec.groovy @@ -0,0 +1,80 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2020 Nordix Foundation + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps.utils + +import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode +import org.opendaylight.yangtools.yang.common.QName +import org.opendaylight.yangtools.yang.common.Revision +import org.opendaylight.yangtools.yang.model.parser.api.YangSyntaxErrorException +import spock.lang.Specification +import spock.lang.Unroll + +class YangUtilsSpec extends Specification{ + def 'Parsing a valid Yang Model'() { + given: 'a yang model (file)' + def file = new File(ClassLoader.getSystemClassLoader().getResource('bookstore.yang').getFile()) + when: 'the file is parsed' + def result = YangUtils.parseYangModelFile(file) + then: 'the result contain 1 module of the correct name and revision' + result.modules.size() == 1 + def optionalModule = result.findModule('bookstore', Revision.of('2020-09-15')) + optionalModule.isPresent() + } + + @Unroll + def 'parsing invalid yang file (#description)'() { + given: 'a file with #description' + File file = new File(ClassLoader.getSystemClassLoader().getResource(filename).getFile()); + when: 'the file is parsed' + YangUtils.parseYangModelFile(file) + then: 'an exception is thrown' + thrown(expectedException) + where: 'the following parameters are used' + filename | description || expectedException + 'invalid.yang' | 'no valid content' || YangSyntaxErrorException + 'someOtherFile.txt' | 'no .yang extension' || IllegalArgumentException + } + + def 'Parsing a valid Json String'() { + given: 'a yang model (file)' + def jsonData = org.onap.cps.TestUtils.getResourceFileContent('bookstore.json') + and: 'a model for that data' + def file = new File(ClassLoader.getSystemClassLoader().getResource('bookstore.yang').getFile()) + def schemaContext = YangUtils.parseYangModelFile(file) + when: 'the json data is parsed' + NormalizedNode result = YangUtils.parseJsonData(jsonData, schemaContext); + then: 'the result is a normalized node of the correct type' + result.nodeType == QName.create('org:onap:ccsdk:sample','2020-09-15','bookstore') + } + + def 'Parsing an invalid Json String'() { + given: 'a yang model (file)' + def jsonData = '{incomplete json' + and: 'a model' + def file = new File(ClassLoader.getSystemClassLoader().getResource('bookstore.yang').getFile()) + def schemaContext = YangUtils.parseYangModelFile(file) + when: 'the invalid json is parsed' + YangUtils.parseJsonData(jsonData, schemaContext); + then: ' an exception is thrown' + thrown(IllegalStateException) + } + + +} diff --git a/cps/cps-service/src/test/java/org/onap/cps/TestUtils.java b/cps/cps-service/src/test/java/org/onap/cps/TestUtils.java new file mode 100644 index 000000000..e15cf525f --- /dev/null +++ b/cps/cps-service/src/test/java/org/onap/cps/TestUtils.java @@ -0,0 +1,41 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2020 Nordix Foundation + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +/** + * Common convenience methods for testing + */ +public class TestUtils { + /** + * Convert a file in the test resource folder to a string + * + * @param filename to name of the file in test/resources + * @return the content of the file as a String + * @throws IOException when there is an IO issue + */ + public static String getResourceFileContent(final String filename) throws IOException { + File file = new File(ClassLoader.getSystemClassLoader().getResource(filename).getFile()); + return new String(Files.readAllBytes(file.toPath())); + } +} diff --git a/cps/cps-service/src/test/resources/bookstore.json b/cps/cps-service/src/test/resources/bookstore.json new file mode 100644 index 000000000..44d5d424c --- /dev/null +++ b/cps/cps-service/src/test/resources/bookstore.json @@ -0,0 +1,34 @@ +{ + "test:bookstore":{ + "categories":[ + { + "name":"web", + "books":[ + { + "authors":[ + "Toine Siebelink","David Lang" + ], + "lang":"en", + "price":"123456", + "pub_year":"2020", + "title":"My first book" + } + ] + }, + { + "name":"art", + "books":[ + { + "authors":[ + "Test" + ], + "lang":"en", + "price":"1234", + "pub_year":"2020", + "title":"My 2nd book" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/cps/cps-service/src/test/resources/bookstore.yang b/cps/cps-service/src/test/resources/bookstore.yang new file mode 100644 index 000000000..01eac5f33 --- /dev/null +++ b/cps/cps-service/src/test/resources/bookstore.yang @@ -0,0 +1,50 @@ +module bookstore { + yang-version 1.1; + + namespace "org:onap:ccsdk:sample"; + + prefix book-store; + + revision "2020-09-15" { + description + "Sample Model"; + } + + typedef year { + type uint16 { + range "1000..9999"; + } + } + + container bookstore { + + list categories { + + key name; + + leaf name { + type string; + } + + list books { + key title; + + leaf title { + type string; + } + leaf lang { + type string; + } + leaf-list authors { + type string; + } + leaf pub_year { + type year; + } + leaf price { + type uint64; + } + } + } + } +} diff --git a/cps/cps-service/src/test/resources/invalid.yang b/cps/cps-service/src/test/resources/invalid.yang new file mode 100644 index 000000000..66cfd1079 --- /dev/null +++ b/cps/cps-service/src/test/resources/invalid.yang @@ -0,0 +1 @@ +no yang at all! diff --git a/cps/cps-service/src/test/resources/someOtherFile.txt b/cps/cps-service/src/test/resources/someOtherFile.txt new file mode 100644 index 000000000..e69de29bb -- 2.16.6