aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorToine Siebelink <toine.siebelink@est.tech>2021-02-10 14:27:55 +0000
committerGerrit Code Review <gerrit@onap.org>2021-02-10 14:27:55 +0000
commit994c59050730a200d030ade14b768539089576a2 (patch)
tree221482e100b99862880e3ef60f1478136d9fc11e
parent98ed3a73393dd7e0edb319f5f54e6952d83505f1 (diff)
parent9c6175cbf9ef0f2bfcd84e24974858c32d6ba587 (diff)
Merge "Data fragment update by xpath - parsing and validation"
-rw-r--r--cps-service/src/main/java/org/onap/cps/spi/model/DataNodeBuilder.java18
-rw-r--r--[-rwxr-xr-x]cps-service/src/main/java/org/onap/cps/utils/YangUtils.java110
-rw-r--r--cps-service/src/test/groovy/org/onap/cps/model/DataNodeBuilderSpec.groovy56
-rw-r--r--cps-service/src/test/groovy/org/onap/cps/utils/YangUtilsSpec.groovy42
-rw-r--r--cps-service/src/test/java/org/onap/cps/TestUtils.java22
5 files changed, 212 insertions, 36 deletions
diff --git a/cps-service/src/main/java/org/onap/cps/spi/model/DataNodeBuilder.java b/cps-service/src/main/java/org/onap/cps/spi/model/DataNodeBuilder.java
index 67e93dd82..ea0626c2b 100644
--- a/cps-service/src/main/java/org/onap/cps/spi/model/DataNodeBuilder.java
+++ b/cps-service/src/main/java/org/onap/cps/spi/model/DataNodeBuilder.java
@@ -43,9 +43,21 @@ public class DataNodeBuilder {
private NormalizedNode<?, ?> normalizedNodeTree;
private String xpath;
+ private String parentNodeXpath = "";
private Map<String, Object> leaves = Collections.emptyMap();
private Collection<DataNode> childDataNodes = Collections.emptySet();
+ /**
+ * To use parent node xpath for creating {@link DataNode}.
+ *
+ * @param parentNodeXpath xpath of a parent node
+ * @return this {@link DataNodeBuilder} object
+ */
+ public DataNodeBuilder withParentNodeXpath(final String parentNodeXpath) {
+ this.parentNodeXpath = parentNodeXpath;
+ return this;
+ }
+
/**
* To use {@link NormalizedNode} for creating {@link DataNode}.
@@ -115,9 +127,9 @@ public class DataNodeBuilder {
}
private DataNode buildFromNormalizedNodeTree() {
- final DataNode formalRootDataNode = new DataNodeBuilder().withXpath("").build();
- addDataNodeFromNormalizedNode(formalRootDataNode, normalizedNodeTree);
- return formalRootDataNode.getChildDataNodes().iterator().next();
+ final DataNode parentDataNode = new DataNodeBuilder().withXpath(parentNodeXpath).build();
+ addDataNodeFromNormalizedNode(parentDataNode, normalizedNodeTree);
+ return parentDataNode.getChildDataNodes().iterator().next();
}
private static void addDataNodeFromNormalizedNode(final DataNode currentDataNode,
diff --git a/cps-service/src/main/java/org/onap/cps/utils/YangUtils.java b/cps-service/src/main/java/org/onap/cps/utils/YangUtils.java
index 389b8967a..733378ecb 100755..100644
--- a/cps-service/src/main/java/org/onap/cps/utils/YangUtils.java
+++ b/cps-service/src/main/java/org/onap/cps/utils/YangUtils.java
@@ -2,6 +2,7 @@
* ============LICENSE_START=======================================================
* Copyright (C) 2020 Nordix Foundation
* Modifications Copyright (C) 2021 Bell Canada. All rights reserved.
+ * Modifications Copyright (C) 2021 Pantheon.tech
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -23,9 +24,14 @@ package org.onap.cps.utils;
import com.google.gson.stream.JsonReader;
import java.io.IOException;
import java.io.StringReader;
+import java.util.Arrays;
+import java.util.Collection;
import java.util.Collections;
import java.util.List;
+import java.util.Optional;
import java.util.stream.Collectors;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.onap.cps.spi.exceptions.DataValidationException;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
@@ -36,36 +42,58 @@ 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.DataNodeContainer;
+import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
import org.opendaylight.yangtools.yang.model.api.SchemaContext;
@Slf4j
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class YangUtils {
+ private static final String XPATH_DELIMITER_REGEX = "\\/";
+ private static final String XPATH_NODE_KEY_ATTRIBUTES_REGEX = "\\[.+";
- private YangUtils() {
- // Private constructor fo security reasons
+ /**
+ * Parses jsonData into NormalizedNode according to given schema context.
+ *
+ * @param jsonData json data as string
+ * @param schemaContext schema context describing associated data model
+ * @return the NormalizedNode object
+ */
+ public static NormalizedNode<?, ?> parseJsonData(final String jsonData, final SchemaContext schemaContext) {
+ return parseJsonData(jsonData, schemaContext, Optional.empty());
}
/**
- * Parse a string containing json data for a certain model (schemaContext).
+ * Parses jsonData into NormalizedNode according to given schema context.
*
- * @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
+ * @param jsonData json data fragment as string
+ * @param schemaContext schema context describing associated data model
+ * @param parentNodeXpath the xpath referencing the parent node current data fragment belong to
+ * @return the NormalizedNode object
*/
- public static NormalizedNode<?, ?> parseJsonData(final String jsonData, final SchemaContext schemaContext) {
+ public static NormalizedNode<?, ?> parseJsonData(final String jsonData, final SchemaContext schemaContext,
+ final String parentNodeXpath) {
+ final DataSchemaNode parentSchemaNode = getDataSchemaNodeByXpath(parentNodeXpath, schemaContext);
+ return parseJsonData(jsonData, schemaContext, Optional.of(parentSchemaNode));
+ }
+
+ private static NormalizedNode<?, ?> parseJsonData(final String jsonData, final SchemaContext schemaContext,
+ final Optional<DataSchemaNode> optionalParentSchemaNode) {
final JSONCodecFactory jsonCodecFactory = JSONCodecFactorySupplier.DRAFT_LHOTKA_NETMOD_YANG_JSON_02
- .getShared(schemaContext);
+ .getShared(schemaContext);
final NormalizedNodeResult normalizedNodeResult = new NormalizedNodeResult();
final NormalizedNodeStreamWriter normalizedNodeStreamWriter = ImmutableNormalizedNodeStreamWriter
- .from(normalizedNodeResult);
- try {
- try (final JsonParserStream jsonParserStream = JsonParserStream
- .create(normalizedNodeStreamWriter, jsonCodecFactory)) {
- final JsonReader jsonReader = new JsonReader(new StringReader(jsonData));
- jsonParserStream.parse(jsonReader);
- }
- } catch (final IOException e) {
+ .from(normalizedNodeResult);
+
+ try (final JsonParserStream jsonParserStream = optionalParentSchemaNode.isPresent()
+ ? JsonParserStream.create(normalizedNodeStreamWriter, jsonCodecFactory, optionalParentSchemaNode.get())
+ : JsonParserStream.create(normalizedNodeStreamWriter, jsonCodecFactory)
+ ) {
+ final JsonReader jsonReader = new JsonReader(new StringReader(jsonData));
+ jsonParserStream.parse(jsonReader);
+
+ } catch (final IOException | IllegalStateException e) {
throw new DataValidationException("Failed to parse json data.", String
.format("Exception occurred on parsing string %s.", jsonData), e);
}
@@ -74,6 +102,7 @@ public class YangUtils {
/**
* Create an xpath form a Yang Tools NodeIdentifier (i.e. PathArgument).
+ *
* @param nodeIdentifier the NodeIdentifier
* @return an xpath
*/
@@ -83,13 +112,13 @@ public class YangUtils {
if (nodeIdentifier instanceof YangInstanceIdentifier.NodeIdentifierWithPredicates) {
xpathBuilder.append(getKeyAttributesStatement(
- (YangInstanceIdentifier.NodeIdentifierWithPredicates) nodeIdentifier));
+ (YangInstanceIdentifier.NodeIdentifierWithPredicates) nodeIdentifier));
}
return xpathBuilder.toString();
}
private static String getKeyAttributesStatement(
- final YangInstanceIdentifier.NodeIdentifierWithPredicates nodeIdentifier) {
+ final YangInstanceIdentifier.NodeIdentifierWithPredicates nodeIdentifier) {
final List<String> keyAttributes = nodeIdentifier.entrySet().stream().map(
entry -> {
final String name = entry.getKey().getLocalName();
@@ -105,4 +134,49 @@ public class YangUtils {
return "[" + String.join(" and ", keyAttributes) + "]";
}
}
+
+ private static DataSchemaNode getDataSchemaNodeByXpath(final String parentNodeXpath,
+ final SchemaContext schemaContext) {
+ final String[] xpathNodeIdSequence = xpathToNodeIdSequence(parentNodeXpath);
+ return findDataSchemaNodeByXpathNodeIdSequence(xpathNodeIdSequence, schemaContext.getChildNodes());
+ }
+
+ private static String[] xpathToNodeIdSequence(final String xpath) {
+ final String[] xpathNodeIdSequence = Arrays.stream(xpath.split(XPATH_DELIMITER_REGEX))
+ .map(identifier -> identifier.replaceFirst(XPATH_NODE_KEY_ATTRIBUTES_REGEX, ""))
+ .filter(identifier -> !identifier.isEmpty())
+ .toArray(String[]::new);
+ if (xpathNodeIdSequence.length < 1) {
+ throw new DataValidationException("Invalid xpath.", "Xpath contains no node identifiers.");
+ }
+ return xpathNodeIdSequence;
+ }
+
+ private static DataSchemaNode findDataSchemaNodeByXpathNodeIdSequence(final String[] xpathNodeIdSequence,
+ final Collection<? extends DataSchemaNode> dataSchemaNodes) {
+ final String currentXpathNodeId = xpathNodeIdSequence[0];
+ final DataSchemaNode currentDataSchemaNode = dataSchemaNodes.stream()
+ .filter(dataSchemaNode -> currentXpathNodeId.equals(dataSchemaNode.getQName().getLocalName()))
+ .findFirst().orElseThrow(() -> schemaNodeNotFoundException(currentXpathNodeId));
+ if (xpathNodeIdSequence.length <= 1) {
+ return currentDataSchemaNode;
+ }
+ if (currentDataSchemaNode instanceof DataNodeContainer) {
+ return findDataSchemaNodeByXpathNodeIdSequence(
+ getNextLevelXpathNodeIdSequence(xpathNodeIdSequence),
+ ((DataNodeContainer) currentDataSchemaNode).getChildNodes());
+ }
+ throw schemaNodeNotFoundException(xpathNodeIdSequence[1]);
+ }
+
+ private static String[] getNextLevelXpathNodeIdSequence(final String[] xpathNodeIdSequence) {
+ final String[] nextXpathNodeIdSequence = new String[xpathNodeIdSequence.length - 1];
+ System.arraycopy(xpathNodeIdSequence, 1, nextXpathNodeIdSequence, 0, nextXpathNodeIdSequence.length);
+ return nextXpathNodeIdSequence;
+ }
+
+ private static DataValidationException schemaNodeNotFoundException(final String schemaNodeIdentifier) {
+ return new DataValidationException("Invalid xpath.",
+ String.format("No schema node was found for xpath identifier '%s'.", schemaNodeIdentifier));
+ }
}
diff --git a/cps-service/src/test/groovy/org/onap/cps/model/DataNodeBuilderSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/model/DataNodeBuilderSpec.groovy
index d881e77ad..dca648b04 100644
--- a/cps-service/src/test/groovy/org/onap/cps/model/DataNodeBuilderSpec.groovy
+++ b/cps-service/src/test/groovy/org/onap/cps/model/DataNodeBuilderSpec.groovy
@@ -1,7 +1,25 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2021 Pantheon.tech
+ * ================================================================================
+ * 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.model
import org.onap.cps.TestUtils
-import org.onap.cps.spi.model.DataNode
import org.onap.cps.spi.model.DataNodeBuilder
import org.onap.cps.utils.YangUtils
import org.onap.cps.yang.YangTextSchemaSourceSetBuilder
@@ -17,16 +35,16 @@ class DataNodeBuilderSpec extends Specification {
'/test-tree/branch[@name=\'Right\']/nest': [name: 'Big', birds: ['Owl', 'Raven', 'Crow']]
]
- def 'Converting Normalized Node (tree) to a DataNode (tree).'() {
- given: 'a Yang module'
+ def 'Converting NormalizedNode (tree) to a DataNode (tree).'() {
+ given: 'the schema context for expected model'
def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('test-tree.yang')
def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent) getSchemaContext()
- and: 'a normalized node for that model'
+ and: 'the json data parsed into normalized node object'
def jsonData = TestUtils.getResourceFileContent('test-tree.json')
def normalizedNode = YangUtils.parseJsonData(jsonData, schemaContext)
- when: 'the normalized node is converted to a DataNode (tree)'
+ when: 'the normalized node is converted to a data node'
def result = new DataNodeBuilder().withNormalizedNodeTree(normalizedNode).build()
- def mappedResult = treeToFlatMapByXpath(new HashMap<>(), result)
+ def mappedResult = TestUtils.getFlattenMapByXpath(result)
then: '5 DataNode objects with unique xpath were created in total'
mappedResult.size() == 5
and: 'all expected xpaths were built'
@@ -37,6 +55,26 @@ class DataNodeBuilderSpec extends Specification {
}
}
+ def 'Converting NormalizedNode (tree) to a DataNode (tree) for known parent node.'() {
+ given: 'a schema context for expected model'
+ def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('test-tree.yang')
+ def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent) getSchemaContext()
+ and: 'the json data parsed into normalized node object'
+ def jsonData = '{ "branch": [{ "name": "Branch", "nest": { "name": "Nest", "birds": ["bird"] } }] }'
+ def normalizedNode = YangUtils.parseJsonData(jsonData, schemaContext, "/test-tree")
+ when: 'the normalized node is converted to a data node with parent node xpath defined'
+ def result = new DataNodeBuilder()
+ .withNormalizedNodeTree(normalizedNode)
+ .withParentNodeXpath("/test-tree")
+ .build()
+ def mappedResult = TestUtils.getFlattenMapByXpath(result)
+ then: '2 DataNode objects with unique xpath were created in total'
+ mappedResult.size() == 2
+ and: 'all expected xpaths were built'
+ mappedResult.keySet()
+ .containsAll(['/test-tree/branch[@name=\'Branch\']', '/test-tree/branch[@name=\'Branch\']/nest'])
+ }
+
def static assertLeavesMaps(actualLeavesMap, expectedLeavesMap) {
expectedLeavesMap.each { key, value ->
{
@@ -51,10 +89,4 @@ class DataNodeBuilderSpec extends Specification {
}
}
- def treeToFlatMapByXpath(Map<String, DataNode> flatMap, DataNode dataNodeTree) {
- flatMap.put(dataNodeTree.getXpath(), dataNodeTree)
- dataNodeTree.getChildDataNodes()
- .forEach(childDataNode -> treeToFlatMapByXpath(flatMap, childDataNode))
- return flatMap
- }
}
diff --git a/cps-service/src/test/groovy/org/onap/cps/utils/YangUtilsSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/utils/YangUtilsSpec.groovy
index e6e5d6a36..826cdd8c7 100644
--- a/cps-service/src/test/groovy/org/onap/cps/utils/YangUtilsSpec.groovy
+++ b/cps-service/src/test/groovy/org/onap/cps/utils/YangUtilsSpec.groovy
@@ -1,6 +1,7 @@
/*
* ============LICENSE_START=======================================================
* Copyright (C) 2020 Nordix Foundation
+ * Modifications Copyright (C) 2021 Pantheon.tech
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,14 +21,14 @@
package org.onap.cps.utils
import org.onap.cps.TestUtils
+import org.onap.cps.spi.exceptions.DataValidationException
import org.onap.cps.yang.YangTextSchemaSourceSetBuilder
import org.opendaylight.yangtools.yang.common.QName
-import org.opendaylight.yangtools.yang.common.Revision
import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode
import spock.lang.Specification
import spock.lang.Unroll
-class YangUtilsSpec extends Specification{
+class YangUtilsSpec extends Specification {
def 'Parsing a valid Json String.'() {
given: 'a yang model (file)'
def jsonData = org.onap.cps.TestUtils.getResourceFileContent('bookstore.json')
@@ -48,10 +49,45 @@ class YangUtilsSpec extends Specification{
when: 'invalid data is parsed'
YangUtils.parseJsonData(invalidJson, schemaContext)
then: 'an exception is thrown'
- thrown(IllegalStateException)
+ thrown(DataValidationException)
where: 'the following invalid json is provided'
invalidJson | description
'{incomplete json' | 'incomplete json'
'{"test:bookstore": {"address": "Parnell st." }}' | 'json with un-modelled data'
}
+
+ @Unroll
+ def 'Parsing json data fragment by xpath for #scenario.'() {
+ given: 'schema context'
+ def yangResourcesMap = TestUtils.getYangResourcesAsMap('test-tree.yang')
+ def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourcesMap).getSchemaContext()
+ when: 'json string is parsed'
+ def result = YangUtils.parseJsonData(jsonData, schemaContext, parentNodeXpath)
+ then: 'result represents a node of expected type'
+ result.nodeType == QName.create('org:onap:cps:test:test-tree', '2020-02-02', nodeName)
+ where:
+ scenario | jsonData | parentNodeXpath || nodeName
+ 'list element as container' | '{ "branch": { "name": "B", "nest": { "name": "N", "birds": ["bird"] } } }' | '/test-tree' || 'branch'
+ 'list element within list' | '{ "branch": [{ "name": "B", "nest": { "name": "N", "birds": ["bird"] } }] }' | '/test-tree' || 'branch'
+ 'container element' | '{ "nest": { "name": "N", "birds": ["bird"] } }' | '/test-tree/branch[@name=\'Branch\']' || 'nest'
+ }
+
+ @Unroll
+ def 'Parsing json data fragment by xpath error scenario: #scenario.'() {
+ given: 'schema context'
+ def yangResourcesMap = TestUtils.getYangResourcesAsMap('test-tree.yang')
+ def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourcesMap).getSchemaContext()
+ when: 'json string is parsed'
+ YangUtils.parseJsonData('{"nest": {"name" : "Nest", "birds": ["bird"]}}', schemaContext,
+ parentNodeXpath)
+ then: 'expected exception is thrown'
+ thrown(DataValidationException)
+ where:
+ scenario | parentNodeXpath
+ 'xpath has no identifiers' | '/'
+ 'xpath has no valid identifiers' | '/[@name=\'Name\']'
+ 'invalid parent path' | '/test-bush'
+ 'another invalid parent path' | '/test-tree/branch[@name=\'Branch\']/nest/name/last-name'
+ 'fragment does not belong to parent' | '/test-tree/'
+ }
}
diff --git a/cps-service/src/test/java/org/onap/cps/TestUtils.java b/cps-service/src/test/java/org/onap/cps/TestUtils.java
index 4ec4e4a00..bf59e1713 100644
--- a/cps-service/src/test/java/org/onap/cps/TestUtils.java
+++ b/cps-service/src/test/java/org/onap/cps/TestUtils.java
@@ -1,6 +1,7 @@
/*
* ============LICENSE_START=======================================================
* Copyright (C) 2020 Nordix Foundation
+ * Modifications Copyright (C) 2021 Pantheon.tech
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -24,6 +25,7 @@ import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Map;
+import org.onap.cps.spi.model.DataNode;
/**
* Common convenience methods for testing.
@@ -69,4 +71,24 @@ public class TestUtils {
}
return yangResourceNameToContentBuilder.build();
}
+
+ /**
+ * Represents given data node object as flatten map by xpath.
+ * For easy finding child node within hierarchy.
+ *
+ * @param dataNode data node representing a root of tree structure
+ * @return the map containing all the data nodes from given structure where key is xpath, value is datanode object
+ */
+ public static Map<String, DataNode> getFlattenMapByXpath(final DataNode dataNode) {
+ final ImmutableMap.Builder<String, DataNode> dataNodeMapBuilder = ImmutableMap.builder();
+ buildFlattenMapByXpath(dataNode, dataNodeMapBuilder);
+ return dataNodeMapBuilder.build();
+ }
+
+ private static void buildFlattenMapByXpath(final DataNode dataNode,
+ final ImmutableMap.Builder<String, DataNode> dataNodeMapBuilder) {
+ dataNodeMapBuilder.put(dataNode.getXpath(), dataNode);
+ dataNode.getChildDataNodes()
+ .forEach(childDataNode -> buildFlattenMapByXpath(childDataNode, dataNodeMapBuilder));
+ }
}