introducing YangUtils with tests
authorToineSiebelink <toine.siebelink@est.tech>
Thu, 1 Oct 2020 13:43:49 +0000 (14:43 +0100)
committerToineSiebelink <toine.siebelink@est.tech>
Fri, 2 Oct 2020 11:16:54 +0000 (12:16 +0100)
Issue-ID: CCSDK-2757
Link: https://jira.onap.org/browse/CCSDK-2757
Change-Id: I3c396ef1e29e9f30027702f3d36ee3bbb1de9b8e

cps/cps-service/src/main/java/org/onap/cps/api/impl/CpServiceImpl.java
cps/cps-service/src/main/java/org/onap/cps/utils/YangUtils.java [new file with mode: 0644]
cps/cps-service/src/test/groovy/org/onap/cps/api/impl/CpServiceImplSpec.groovy
cps/cps-service/src/test/groovy/org/onap/cps/utils/YangUtilsSpec.groovy [new file with mode: 0644]
cps/cps-service/src/test/java/org/onap/cps/TestUtils.java [new file with mode: 0644]
cps/cps-service/src/test/resources/bookstore.json [new file with mode: 0644]
cps/cps-service/src/test/resources/bookstore.yang [new file with mode: 0644]
cps/cps-service/src/test/resources/invalid.yang [new file with mode: 0644]
cps/cps-service/src/test/resources/someOtherFile.txt [new file with mode: 0644]

index 8d6b63b..cb8e20c 100644 (file)
@@ -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<YangParserFactory> 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 (file)
index 0000000..e9757ec
--- /dev/null
@@ -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<YangParserFactory> 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)
+    }
+
+}
index 27f7482..a1c9dd9 100644 (file)
@@ -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 (file)
index 0000000..8aabc48
--- /dev/null
@@ -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 (file)
index 0000000..e15cf52
--- /dev/null
@@ -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 (file)
index 0000000..44d5d42
--- /dev/null
@@ -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 (file)
index 0000000..01eac5f
--- /dev/null
@@ -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 (file)
index 0000000..66cfd10
--- /dev/null
@@ -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 (file)
index 0000000..e69de29