CPS-341 Support for multiple data tree instances under 1 anchor 66/131466/19
authorarpitsingh <as00745003@techmahindra.com>
Thu, 13 Oct 2022 21:12:43 +0000 (02:42 +0530)
committerarpitsingh <as00745003@techmahindra.com>
Thu, 15 Dec 2022 12:49:48 +0000 (18:19 +0530)
- Updated the parseJsonData method so it can parse JSON with multiple data trees, now it returns a ContainerNode
- ContainerNode holds a collection of NormalizedNodes
- Updated DataNodeBuilder and FragmentRepository as well to support collection of NormalizedNodes
- Added new methods in CpsDataPersistenceService to store multiple Data Nodes
- Added new test cases
- Updated existing test cases and fixed code coverage
- Addressed comments from previous patch

Issue-ID: CPS-341
Change-Id: Ie893e91c0fbfb139a1a406e962721b0f52412ced
Signed-off-by: arpitsingh <as00745003@techmahindra.com>
13 files changed:
cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java
cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceIntegrationSpec.groovy
cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy
cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java
cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java
cps-service/src/main/java/org/onap/cps/spi/model/DataNodeBuilder.java
cps-service/src/main/java/org/onap/cps/utils/YangUtils.java
cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy
cps-service/src/test/groovy/org/onap/cps/api/impl/E2ENetworkSliceSpec.groovy
cps-service/src/test/groovy/org/onap/cps/spi/model/DataNodeBuilderSpec.groovy
cps-service/src/test/groovy/org/onap/cps/utils/JsonParserStreamSpec.groovy
cps-service/src/test/groovy/org/onap/cps/utils/YangUtilsSpec.groovy
cps-service/src/test/groovy/org/onap/cps/yang/YangTextSchemaSourceSetBuilderSpec.groovy

index c725b42..b7da66e 100644 (file)
@@ -3,6 +3,7 @@
  *  Copyright (C) 2021-2022 Nordix Foundation
  *  Modifications Copyright (C) 2021 Pantheon.tech
  *  Modifications Copyright (C) 2020-2022 Bell Canada.
+ *  Modifications Copyright (C) 2022 TechMahindra Ltd.
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -88,6 +89,12 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
         addNewChildDataNode(dataspaceName, anchorName, parentNodeXpath, newChildDataNode);
     }
 
+    @Override
+    public void addChildDataNodes(final String dataspaceName, final String anchorName,
+                                  final String parentNodeXpath, final Collection<DataNode> dataNodes) {
+        addChildrenDataNodes(dataspaceName, anchorName, parentNodeXpath, dataNodes);
+    }
+
     @Override
     public void addListElements(final String dataspaceName, final String anchorName, final String parentNodeXpath,
                                 final Collection<DataNode> newListElements) {
@@ -167,14 +174,45 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
 
     @Override
     public void storeDataNode(final String dataspaceName, final String anchorName, final DataNode dataNode) {
+        storeDataNodes(dataspaceName, anchorName, Collections.singletonList(dataNode));
+    }
+
+    @Override
+    public void storeDataNodes(final String dataspaceName, final String anchorName,
+                               final Collection<DataNode> dataNodes) {
         final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
         final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
-        final FragmentEntity fragmentEntity = convertToFragmentWithAllDescendants(dataspaceEntity, anchorEntity,
-                dataNode);
+        final List<FragmentEntity> fragmentEntities = new ArrayList<>(dataNodes.size());
         try {
-            fragmentRepository.save(fragmentEntity);
+            for (final DataNode dataNode: dataNodes) {
+                final FragmentEntity fragmentEntity = convertToFragmentWithAllDescendants(dataspaceEntity, anchorEntity,
+                        dataNode);
+                fragmentEntities.add(fragmentEntity);
+            }
+            fragmentRepository.saveAll(fragmentEntities);
         } catch (final DataIntegrityViolationException exception) {
-            throw AlreadyDefinedException.forDataNode(dataNode.getXpath(), anchorName, exception);
+            log.warn("Exception occurred : {} , While saving : {} data nodes, Retrying saving data nodes individually",
+                    exception, dataNodes.size());
+            storeDataNodesIndividually(dataspaceName, anchorName, dataNodes);
+        }
+    }
+
+    private void storeDataNodesIndividually(final String dataspaceName, final String anchorName,
+                                           final Collection<DataNode> dataNodes) {
+        final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
+        final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
+        final Collection<String> failedXpaths = new HashSet<>();
+        for (final DataNode dataNode: dataNodes) {
+            try {
+                final FragmentEntity fragmentEntity = convertToFragmentWithAllDescendants(dataspaceEntity, anchorEntity,
+                        dataNode);
+                fragmentRepository.save(fragmentEntity);
+            } catch (final DataIntegrityViolationException e) {
+                failedXpaths.add(dataNode.getXpath());
+            }
+        }
+        if (!failedXpaths.isEmpty()) {
+            throw new AlreadyDefinedExceptionBatch(failedXpaths);
         }
     }
 
index fbf414d..12585eb 100755 (executable)
@@ -3,6 +3,7 @@
  *  Copyright (C) 2021-2022 Nordix Foundation
  *  Modifications Copyright (C) 2021 Pantheon.tech
  *  Modifications Copyright (C) 2021-2022 Bell Canada.
+ *  Modifications Copyright (C) 2022 TechMahindra Ltd.
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -26,7 +27,6 @@ import com.google.common.collect.ImmutableSet
 import org.onap.cps.cpspath.parser.PathParsingException
 import org.onap.cps.spi.CpsDataPersistenceService
 import org.onap.cps.spi.entities.FragmentEntity
-import org.onap.cps.spi.exceptions.AlreadyDefinedException
 import org.onap.cps.spi.exceptions.AlreadyDefinedExceptionBatch
 import org.onap.cps.spi.exceptions.AnchorNotFoundException
 import org.onap.cps.spi.exceptions.CpsAdminException
@@ -48,25 +48,25 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
     @Autowired
     CpsDataPersistenceService objectUnderTest
 
-    static final JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
-    static final DataNodeBuilder dataNodeBuilder = new DataNodeBuilder()
+    static JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
+    static DataNodeBuilder dataNodeBuilder = new DataNodeBuilder()
 
     static final String SET_DATA = '/data/fragment.sql'
-    static final int DATASPACE_1001_ID = 1001L
-    static final int ANCHOR_3003_ID = 3003L
-    static final long ID_DATA_NODE_WITH_DESCENDANTS = 4001
-    static final String XPATH_DATA_NODE_WITH_DESCENDANTS = '/parent-1'
-    static final String XPATH_DATA_NODE_WITH_LEAVES = '/parent-207'
-    static final long DATA_NODE_202_FRAGMENT_ID = 4202L
-    static final long CHILD_OF_DATA_NODE_202_FRAGMENT_ID = 4203L
-    static final long LIST_DATA_NODE_PARENT201_FRAGMENT_ID = 4206L
-    static final long LIST_DATA_NODE_PARENT203_FRAGMENT_ID = 4214L
-    static final long LIST_DATA_NODE_PARENT202_FRAGMENT_ID = 4211L
-    static final long PARENT_3_FRAGMENT_ID = 4003L
-
-    static final DataNode newDataNode = new DataNodeBuilder().build()
-    static DataNode existingDataNode
-    static DataNode existingChildDataNode
+    static int DATASPACE_1001_ID = 1001L
+    static int ANCHOR_3003_ID = 3003L
+    static long ID_DATA_NODE_WITH_DESCENDANTS = 4001
+    static String XPATH_DATA_NODE_WITH_DESCENDANTS = '/parent-1'
+    static String XPATH_DATA_NODE_WITH_LEAVES = '/parent-207'
+    static long DATA_NODE_202_FRAGMENT_ID = 4202L
+    static long CHILD_OF_DATA_NODE_202_FRAGMENT_ID = 4203L
+    static long LIST_DATA_NODE_PARENT201_FRAGMENT_ID = 4206L
+    static long LIST_DATA_NODE_PARENT203_FRAGMENT_ID = 4214L
+    static long LIST_DATA_NODE_PARENT202_FRAGMENT_ID = 4211L
+    static long PARENT_3_FRAGMENT_ID = 4003L
+
+    static Collection<DataNode> newDataNodes = [new DataNodeBuilder().build()]
+    static Collection<DataNode> existingDataNodes = [createDataNodeTree(XPATH_DATA_NODE_WITH_DESCENDANTS)]
+    static Collection<DataNode> existingChildDataNodes = [createDataNodeTree('/parent-1/child-1')]
 
     def expectedLeavesByXpathMap = [
             '/parent-207'                      : ['parent-leaf': 'parent-leaf value'],
@@ -75,11 +75,6 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
             '/parent-207/child-002/grand-child': ['grand-child-leaf': 'grand-child-leaf value']
     ]
 
-    static {
-        existingDataNode = createDataNodeTree(XPATH_DATA_NODE_WITH_DESCENDANTS)
-        existingChildDataNode = createDataNodeTree('/parent-1/child-1')
-    }
-
     @Sql([CLEAR_DATA, SET_DATA])
     def 'Get existing datanode with descendants.'() {
         when: 'the node is retrieved by its xpath'
@@ -93,13 +88,13 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
     }
 
     @Sql([CLEAR_DATA, SET_DATA])
-    def 'Storing and Retrieving a new DataNode with descendants.'() {
+    def 'Storing and Retrieving a new DataNodes with descendants.'() {
         when: 'a fragment with descendants is stored'
             def parentXpath = '/parent-new'
             def childXpath = '/parent-new/child-new'
             def grandChildXpath = '/parent-new/child-new/grandchild-new'
-            objectUnderTest.storeDataNode(DATASPACE_NAME, ANCHOR_NAME1,
-                    createDataNodeTree(parentXpath, childXpath, grandChildXpath))
+            def dataNodes = [createDataNodeTree(parentXpath, childXpath, grandChildXpath)]
+            objectUnderTest.storeDataNodes(DATASPACE_NAME, ANCHOR_NAME1, dataNodes)
         then: 'it can be retrieved by its xpath'
             def dataNode = objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_NAME1, parentXpath, INCLUDE_ALL_DESCENDANTS)
             assert dataNode.xpath == parentXpath
@@ -117,9 +112,9 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
     def 'Store data node for multiple anchors using the same schema.'() {
         def xpath = '/parent-new'
         given: 'a fragment is stored for an anchor'
-            objectUnderTest.storeDataNode(DATASPACE_NAME, ANCHOR_NAME1, createDataNodeTree(xpath))
+            objectUnderTest.storeDataNodes(DATASPACE_NAME, ANCHOR_NAME1, [createDataNodeTree(xpath)])
         when: 'another fragment is stored for an other anchor, using the same schema set'
-            objectUnderTest.storeDataNode(DATASPACE_NAME, ANCHOR_NAME3, createDataNodeTree(xpath))
+            objectUnderTest.storeDataNodes(DATASPACE_NAME, ANCHOR_NAME3, [createDataNodeTree(xpath)])
         then: 'both fragments can be retrieved by their xpath'
             def fragment1 = getFragmentByXpath(DATASPACE_NAME, ANCHOR_NAME1, xpath)
             fragment1.anchor.name == ANCHOR_NAME1
@@ -130,45 +125,48 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
     }
 
     @Sql([CLEAR_DATA, SET_DATA])
-    def 'Store datanode error scenario: #scenario.'() {
+    def 'Store datanodes error scenario: #scenario.'() {
         when: 'attempt to store a data node with #scenario'
-            objectUnderTest.storeDataNode(dataspaceName, anchorName, dataNode)
+            objectUnderTest.storeDataNodes(dataspaceName, anchorName, dataNodes)
         then: 'a #expectedException is thrown'
             thrown(expectedException)
         where: 'the following data is used'
-            scenario                    | dataspaceName  | anchorName     | dataNode         || expectedException
-            'dataspace does not exist'  | 'unknown'      | 'not-relevant' | newDataNode      || DataspaceNotFoundException
-            'schema set does not exist' | DATASPACE_NAME | 'unknown'      | newDataNode      || AnchorNotFoundException
-            'anchor already exists'     | DATASPACE_NAME | ANCHOR_NAME1   | newDataNode      || ConstraintViolationException
-            'datanode already exists'   | DATASPACE_NAME | ANCHOR_NAME1   | existingDataNode || AlreadyDefinedException
+            scenario                    | dataspaceName  | anchorName     | dataNode         || expectedException
+            'dataspace does not exist'  | 'unknown'      | 'not-relevant' | newDataNode      || DataspaceNotFoundException
+            'schema set does not exist' | DATASPACE_NAME | 'unknown'      | newDataNode      || AnchorNotFoundException
+            'anchor already exists'     | DATASPACE_NAME | ANCHOR_NAME1   | newDataNode      || ConstraintViolationException
+            'datanode already exists'   | DATASPACE_NAME | ANCHOR_NAME1   | existingDataNodes  || AlreadyDefinedExceptionBatch
     }
 
     @Sql([CLEAR_DATA, SET_DATA])
-    def 'Add a child to a Fragment that already has a child.'() {
-        given: ' a new child node'
-            def newChild = createDataNodeTree('xpath for new child')
+    def 'Add children to a Fragment that already has a child.'() {
+        given: 'collection of new child data nodes'
+            def newChild1 = createDataNodeTree('/parent-1/child-2')
+            def newChild2 = createDataNodeTree('/parent-1/child-3')
+            def newChildrenCollection = [newChild1, newChild2]
         when: 'the child is added to an existing parent with 1 child'
-            objectUnderTest.addChildDataNode(DATASPACE_NAME, ANCHOR_NAME1, XPATH_DATA_NODE_WITH_DESCENDANTS, newChild)
-        then: 'the parent is now has to 2 children'
+            objectUnderTest.addChildDataNodes(DATASPACE_NAME, ANCHOR_NAME1, XPATH_DATA_NODE_WITH_DESCENDANTS, newChildrenCollection)
+        then: 'the parent is now has to 3 children'
             def expectedExistingChildPath = '/parent-1/child-1'
             def parentFragment = fragmentRepository.findById(ID_DATA_NODE_WITH_DESCENDANTS).orElseThrow()
-            parentFragment.childFragments.size() == 2
+            parentFragment.childFragments.size() == 3
         and: 'it still has the old child'
             parentFragment.childFragments.find({ it.xpath == expectedExistingChildPath })
-        and: 'it has the new child'
-            parentFragment.childFragments.find({ it.xpath == newChild.xpath })
+        and: 'it has the new children'
+            parentFragment.childFragments.find({ it.xpath == newChildrenCollection[0].xpath })
+            parentFragment.childFragments.find({ it.xpath == newChildrenCollection[1].xpath })
     }
 
     @Sql([CLEAR_DATA, SET_DATA])
     def 'Add child error scenario: #scenario.'() {
         when: 'attempt to add a child data node with #scenario'
-            objectUnderTest.addChildDataNode(DATASPACE_NAME, ANCHOR_NAME1, parentXpath, dataNode)
+            objectUnderTest.addChildDataNodes(DATASPACE_NAME, ANCHOR_NAME1, parentXpath, dataNodes)
         then: 'a #expectedException is thrown'
             thrown(expectedException)
         where: 'the following data is used'
-            scenario                 | parentXpath                      | dataNode              || expectedException
-            'parent does not exist'  | '/unknown'                       | newDataNode           || DataNodeNotFoundException
-            'already existing child' | XPATH_DATA_NODE_WITH_DESCENDANTS | existingChildDataNode || AlreadyDefinedException
+            scenario                 | parentXpath                      | dataNode              || expectedException
+            'parent does not exist'  | '/unknown'                       | newDataNode           || DataNodeNotFoundException
+            'already existing child' | XPATH_DATA_NODE_WITH_DESCENDANTS | existingChildDataNodes  || AlreadyDefinedExceptionBatch
     }
 
     @Sql([CLEAR_DATA, SET_DATA])
index e69cbee..255e8e5 100644 (file)
@@ -2,6 +2,7 @@
  * ============LICENSE_START=======================================================
  * Copyright (c) 2021 Bell Canada.
  * Modifications Copyright (C) 2021-2022 Nordix Foundation
+ *  Modifications Copyright (C) 2022 TechMahindra Ltd.
  * ================================================================================
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -34,6 +35,7 @@ import org.onap.cps.spi.repository.DataspaceRepository
 import org.onap.cps.spi.repository.FragmentRepository
 import org.onap.cps.spi.utils.SessionManager
 import org.onap.cps.utils.JsonObjectMapper
+import org.springframework.dao.DataIntegrityViolationException
 import spock.lang.Specification
 
 class CpsDataPersistenceServiceSpec extends Specification {
@@ -44,7 +46,28 @@ class CpsDataPersistenceServiceSpec extends Specification {
     def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
     def mockSessionManager = Mock(SessionManager)
 
-    def objectUnderTest = new CpsDataPersistenceServiceImpl(mockDataspaceRepository, mockAnchorRepository, mockFragmentRepository, jsonObjectMapper, mockSessionManager)
+    def objectUnderTest = Spy(new CpsDataPersistenceServiceImpl(mockDataspaceRepository, mockAnchorRepository, mockFragmentRepository, jsonObjectMapper, mockSessionManager))
+
+    def 'Storing data nodes individually when batch operation fails'(){
+        given: 'two data nodes and supporting repository mock behavior'
+            def dataNode1 = createDataNodeAndMockRepositoryMethodSupportingIt('xpath1','OK')
+            def dataNode2 = createDataNodeAndMockRepositoryMethodSupportingIt('xpath2','OK')
+        and: 'the batch store operation will fail'
+            mockFragmentRepository.saveAll(*_) >> { throw new DataIntegrityViolationException("Exception occurred") }
+        when: 'trying to store data nodes'
+            objectUnderTest.storeDataNodes('dataSpaceName', 'anchorName', [dataNode1, dataNode2])
+        then: 'the two data nodes are saved individually'
+            2 * mockFragmentRepository.save(_);
+    }
+
+    def 'Store single data node.'() {
+        given: 'a data node'
+            def dataNode = new DataNode()
+        when: 'storing a single data node'
+            objectUnderTest.storeDataNode('dataspace1', 'anchor1', dataNode)
+        then: 'the call is redirected to storing a collection of data nodes with just the given data node'
+            1 * objectUnderTest.storeDataNodes('dataspace1', 'anchor1', [dataNode])
+    }
 
     def 'Handling of StaleStateException (caused by concurrent updates) during update data node and descendants.'() {
         given: 'the fragment repository returns a fragment entity'
@@ -66,10 +89,10 @@ class CpsDataPersistenceServiceSpec extends Specification {
 
     def 'Handling of StaleStateException (caused by concurrent updates) during update data nodes and descendants.'() {
         given: 'the system contains and can update one datanode'
-            def dataNode1 = mockDataNodeAndFragmentEntity('/node1', 'OK')
+            def dataNode1 = createDataNodeAndMockRepositoryMethodSupportingIt('/node1', 'OK')
         and: 'the system contains two more datanodes that throw an exception while updating'
-            def dataNode2 = mockDataNodeAndFragmentEntity('/node2', 'EXCEPTION')
-            def dataNode3 = mockDataNodeAndFragmentEntity('/node3', 'EXCEPTION')
+            def dataNode2 = createDataNodeAndMockRepositoryMethodSupportingIt('/node2', 'EXCEPTION')
+            def dataNode3 = createDataNodeAndMockRepositoryMethodSupportingIt('/node3', 'EXCEPTION')
         and: 'the batch update will therefore also fail'
             mockFragmentRepository.saveAll(*_) >> { throw new StaleStateException("concurrent updates") }
         when: 'attempt batch update data nodes'
@@ -174,7 +197,7 @@ class CpsDataPersistenceServiceSpec extends Specification {
             }})
     }
 
-    def mockDataNodeAndFragmentEntity(xpath, scenario) {
+    def createDataNodeAndMockRepositoryMethodSupportingIt(xpath, scenario) {
         def dataNode = new DataNodeBuilder().withXpath(xpath).build()
         def fragmentEntity = new FragmentEntity(xpath: xpath, childFragments: [])
         mockFragmentRepository.getByDataspaceAndAnchorAndXpath(_, _, xpath) >> fragmentEntity
index b08d8c1..732b494 100755 (executable)
@@ -3,6 +3,7 @@
  *  Copyright (C) 2021-2022 Nordix Foundation
  *  Modifications Copyright (C) 2020-2022 Bell Canada.
  *  Modifications Copyright (C) 2021 Pantheon.tech
+ *  Modifications Copyright (C) 2022 TechMahindra Ltd.
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -27,6 +28,7 @@ import static org.onap.cps.notification.Operation.DELETE;
 import static org.onap.cps.notification.Operation.UPDATE;
 
 import java.time.OffsetDateTime;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
@@ -45,7 +47,7 @@ import org.onap.cps.spi.model.DataNode;
 import org.onap.cps.spi.model.DataNodeBuilder;
 import org.onap.cps.spi.utils.CpsValidator;
 import org.onap.cps.utils.YangUtils;
-import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
+import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
 import org.springframework.stereotype.Service;
 
@@ -67,8 +69,9 @@ public class CpsDataServiceImpl implements CpsDataService {
     public void saveData(final String dataspaceName, final String anchorName, final String jsonData,
         final OffsetDateTime observedTimestamp) {
         cpsValidator.validateNameCharacters(dataspaceName, anchorName);
-        final DataNode dataNode = buildDataNode(dataspaceName, anchorName, ROOT_NODE_XPATH, jsonData);
-        cpsDataPersistenceService.storeDataNode(dataspaceName, anchorName, dataNode);
+        final Collection<DataNode> dataNodes =
+                buildDataNodes(dataspaceName, anchorName, ROOT_NODE_XPATH, jsonData);
+        cpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName, dataNodes);
         processDataUpdatedEventAsync(dataspaceName, anchorName, ROOT_NODE_XPATH, CREATE, observedTimestamp);
     }
 
@@ -76,8 +79,9 @@ public class CpsDataServiceImpl implements CpsDataService {
     public void saveData(final String dataspaceName, final String anchorName, final String parentNodeXpath,
         final String jsonData, final OffsetDateTime observedTimestamp) {
         cpsValidator.validateNameCharacters(dataspaceName, anchorName);
-        final DataNode dataNode = buildDataNode(dataspaceName, anchorName, parentNodeXpath, jsonData);
-        cpsDataPersistenceService.addChildDataNode(dataspaceName, anchorName, parentNodeXpath, dataNode);
+        final Collection<DataNode> dataNodes =
+                buildDataNodes(dataspaceName, anchorName, parentNodeXpath, jsonData);
+        cpsDataPersistenceService.addChildDataNodes(dataspaceName, anchorName, parentNodeXpath, dataNodes);
         processDataUpdatedEventAsync(dataspaceName, anchorName, parentNodeXpath, CREATE, observedTimestamp);
     }
 
@@ -161,8 +165,10 @@ public class CpsDataServiceImpl implements CpsDataService {
                                              final String parentNodeXpath, final String jsonData,
                                              final OffsetDateTime observedTimestamp) {
         cpsValidator.validateNameCharacters(dataspaceName, anchorName);
-        final DataNode dataNode = buildDataNode(dataspaceName, anchorName, parentNodeXpath, jsonData);
-        cpsDataPersistenceService.updateDataNodeAndDescendants(dataspaceName, anchorName, dataNode);
+        final Collection<DataNode> dataNodes =
+                buildDataNodes(dataspaceName, anchorName, parentNodeXpath, jsonData);
+        final ArrayList<DataNode> nodes = new ArrayList<>(dataNodes);
+        cpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName, nodes);
         processDataUpdatedEventAsync(dataspaceName, anchorName, parentNodeXpath, UPDATE, observedTimestamp);
     }
 
@@ -226,15 +232,16 @@ public class CpsDataServiceImpl implements CpsDataService {
         final SchemaContext schemaContext = getSchemaContext(dataspaceName, anchor.getSchemaSetName());
 
         if (ROOT_NODE_XPATH.equals(parentNodeXpath)) {
-            final NormalizedNode normalizedNode = YangUtils.parseJsonData(jsonData, schemaContext);
-            return new DataNodeBuilder().withNormalizedNodeTree(normalizedNode).build();
+            final ContainerNode containerNode = YangUtils.parseJsonData(jsonData, schemaContext);
+            return new DataNodeBuilder().withContainerNode(containerNode).build();
         }
 
-        final NormalizedNode normalizedNode = YangUtils.parseJsonData(jsonData, schemaContext, parentNodeXpath);
+        final ContainerNode containerNode = YangUtils
+                .parseJsonData(jsonData, schemaContext, parentNodeXpath);
         return new DataNodeBuilder()
-            .withParentNodeXpath(parentNodeXpath)
-            .withNormalizedNodeTree(normalizedNode)
-            .build();
+                .withParentNodeXpath(parentNodeXpath)
+                .withContainerNode(containerNode)
+                .build();
     }
 
     private List<DataNode> buildDataNodes(final String dataspaceName, final String anchorName,
@@ -251,11 +258,20 @@ public class CpsDataServiceImpl implements CpsDataService {
 
         final Anchor anchor = cpsAdminService.getAnchor(dataspaceName, anchorName);
         final SchemaContext schemaContext = getSchemaContext(dataspaceName, anchor.getSchemaSetName());
-
-        final NormalizedNode normalizedNode = YangUtils.parseJsonData(jsonData, schemaContext, parentNodeXpath);
+        if (ROOT_NODE_XPATH.equals(parentNodeXpath)) {
+            final ContainerNode containerNode = YangUtils.parseJsonData(jsonData, schemaContext);
+            final Collection<DataNode> dataNodes = new DataNodeBuilder()
+                    .withContainerNode(containerNode)
+                    .buildCollection();
+            if (dataNodes.isEmpty()) {
+                throw new DataValidationException("Invalid data.", "No data nodes provided");
+            }
+            return dataNodes;
+        }
+        final ContainerNode containerNode = YangUtils.parseJsonData(jsonData, schemaContext, parentNodeXpath);
         final Collection<DataNode> dataNodes = new DataNodeBuilder()
             .withParentNodeXpath(parentNodeXpath)
-            .withNormalizedNodeTree(normalizedNode)
+            .withContainerNode(containerNode)
             .buildCollection();
         if (dataNodes.isEmpty()) {
             throw new DataValidationException("Invalid data.", "No data nodes provided");
index 28b18b3..b9da4af 100644 (file)
@@ -3,6 +3,7 @@
  *  Copyright (C) 2020-2022 Nordix Foundation.
  *  Modifications Copyright (C) 2021 Pantheon.tech
  *  Modifications Copyright (C) 2022 Bell Canada
+ *  Modifications Copyright (C) 2022 TechMahindra Ltd.
  * ================================================================================
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -35,15 +36,26 @@ import org.onap.cps.spi.model.DataNode;
  */
 public interface CpsDataPersistenceService {
 
+
     /**
      * Store a datanode.
      *
      * @param dataspaceName dataspace name
      * @param anchorName    anchor name
      * @param dataNode      data node
+     * @deprecated Please use {@link #storeDataNodes(String, String, Collection)} as it supports multiple data nodes.
      */
+    @Deprecated
     void storeDataNode(String dataspaceName, String anchorName, DataNode dataNode);
 
+    /**
+     * Store multiple datanodes at once.
+     * @param dataspaceName dataspace name
+     * @param anchorName    anchor name
+     * @param dataNodes     data nodes
+     */
+    void storeDataNodes(String dataspaceName, String anchorName, Collection<DataNode> dataNodes);
+
     /**
      * Add a child to a Fragment.
      *
@@ -54,6 +66,16 @@ public interface CpsDataPersistenceService {
      */
     void addChildDataNode(String dataspaceName, String anchorName, String parentXpath, DataNode dataNode);
 
+    /**
+     * Add multiple children to a Fragment.
+     *
+     * @param dataspaceName dataspace name
+     * @param anchorName    anchor name
+     * @param parentXpath   parent xpath
+     * @param dataNodes     collection of dataNodes
+     */
+    void addChildDataNodes(String dataspaceName, String anchorName, String parentXpath, Collection<DataNode> dataNodes);
+
     /**
      * Adds list child elements to a Fragment.
      *
@@ -62,7 +84,6 @@ public interface CpsDataPersistenceService {
      * @param parentNodeXpath        parent node xpath
      * @param listElementsCollection collection of data nodes representing list elements
      */
-
     void addListElements(String dataspaceName, String anchorName, String parentNodeXpath,
         Collection<DataNode> listElementsCollection);
 
index 1d8bac0..b23cdfc 100644 (file)
@@ -3,6 +3,7 @@
  *  Copyright (C) 2021 Bell Canada. All rights reserved.
  *  Modifications Copyright (C) 2021 Pantheon.tech
  *  Modifications Copyright (C) 2022 Nordix Foundation.
+ *  Modifications Copyright (C) 2022 TechMahindra Ltd.
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -35,6 +36,7 @@ import org.onap.cps.spi.exceptions.DataValidationException;
 import org.onap.cps.utils.YangUtils;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
 import org.opendaylight.yangtools.yang.data.api.schema.ChoiceNode;
+import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
 import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild;
 import org.opendaylight.yangtools.yang.data.api.schema.DataContainerNode;
 import org.opendaylight.yangtools.yang.data.api.schema.LeafSetNode;
@@ -46,7 +48,7 @@ import org.opendaylight.yangtools.yang.data.api.schema.ValueNode;
 @Slf4j
 public class DataNodeBuilder {
 
-    private NormalizedNode normalizedNodeTree;
+    private ContainerNode containerNode;
     private String xpath;
     private String moduleNamePrefix;
     private String parentNodeXpath = "";
@@ -64,15 +66,14 @@ public class DataNodeBuilder {
         return this;
     }
 
-
     /**
-     * To use {@link NormalizedNode} for creating {@link DataNode}.
+     * To use {@link Collection} of Normalized Nodes for creating {@link DataNode}.
      *
-     * @param normalizedNodeTree used for creating the Data Node
+     * @param containerNode used for creating the Data Node
      * @return this {@link DataNodeBuilder} object
      */
-    public DataNodeBuilder withNormalizedNodeTree(final NormalizedNode normalizedNodeTree) {
-        this.normalizedNodeTree = normalizedNodeTree;
+    public DataNodeBuilder withContainerNode(final ContainerNode containerNode) {
+        this.containerNode = containerNode;
         return this;
     }
 
@@ -128,11 +129,10 @@ public class DataNodeBuilder {
      * @return {@link DataNode}
      */
     public DataNode build() {
-        if (normalizedNodeTree != null) {
-            return buildFromNormalizedNodeTree();
-        } else {
-            return buildFromAttributes();
+        if (containerNode != null) {
+            return buildFromContainerNode();
         }
+        return buildFromAttributes();
     }
 
     /**
@@ -141,11 +141,10 @@ public class DataNodeBuilder {
      * @return {@link DataNode} {@link Collection}
      */
     public Collection<DataNode> buildCollection() {
-        if (normalizedNodeTree != null) {
-            return buildCollectionFromNormalizedNodeTree();
-        } else {
-            return Set.of(buildFromAttributes());
+        if (containerNode != null) {
+            return buildCollectionFromContainerNode();
         }
+        return Collections.emptySet();
     }
 
     private DataNode buildFromAttributes() {
@@ -157,8 +156,8 @@ public class DataNodeBuilder {
         return dataNode;
     }
 
-    private DataNode buildFromNormalizedNodeTree() {
-        final Collection<DataNode> dataNodeCollection = buildCollectionFromNormalizedNodeTree();
+    private DataNode buildFromContainerNode() {
+        final Collection<DataNode> dataNodeCollection = buildCollectionFromContainerNode();
         if (!dataNodeCollection.iterator().hasNext()) {
             throw new DataValidationException(
                 "Unsupported xpath: ", "Unsupported xpath as it is referring to one element");
@@ -166,9 +165,13 @@ public class DataNodeBuilder {
         return dataNodeCollection.iterator().next();
     }
 
-    private Collection<DataNode> buildCollectionFromNormalizedNodeTree() {
+    private Collection<DataNode> buildCollectionFromContainerNode() {
         final var parentDataNode = new DataNodeBuilder().withXpath(parentNodeXpath).build();
-        addDataNodeFromNormalizedNode(parentDataNode, normalizedNodeTree);
+        if (containerNode.body() != null) {
+            for (final NormalizedNode normalizedNode: containerNode.body()) {
+                addDataNodeFromNormalizedNode(parentDataNode, normalizedNode);
+            }
+        }
         return parentDataNode.getChildDataNodes();
     }
 
index 48241ed..9a61579 100644 (file)
@@ -3,6 +3,7 @@
  *  Copyright (C) 2020-2022 Nordix Foundation
  *  Modifications Copyright (C) 2021 Bell Canada.
  *  Modifications Copyright (C) 2021 Pantheon.tech
+ *  Modifications Copyright (C) 2022 TechMahindra Ltd.
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -39,13 +40,14 @@ import lombok.extern.slf4j.Slf4j;
 import org.onap.cps.spi.exceptions.DataValidationException;
 import org.opendaylight.yangtools.yang.common.QName;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
-import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
+import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
+import org.opendaylight.yangtools.yang.data.api.schema.builder.DataContainerNodeBuilder;
 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.Builders;
 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.EffectiveModelContext;
@@ -62,38 +64,40 @@ public class YangUtils {
     private static final String XPATH_NODE_KEY_ATTRIBUTES_REGEX = "\\[.*?\\]";
 
     /**
-     * Parses jsonData into NormalizedNode according to given schema context.
+     * Parses jsonData into Collection of NormalizedNode according to given schema context.
      *
      * @param jsonData      json data as string
      * @param schemaContext schema context describing associated data model
-     * @return the NormalizedNode object
+     * @return the Collection of NormalizedNode object
      */
-    public static NormalizedNode parseJsonData(final String jsonData, final SchemaContext schemaContext) {
+    public static ContainerNode parseJsonData(final String jsonData, final SchemaContext schemaContext) {
         return parseJsonData(jsonData, schemaContext, Optional.empty());
     }
 
     /**
-     * Parses jsonData into NormalizedNode according to given schema context.
+     * Parses jsonData into Collection of NormalizedNode according to given schema context.
      *
      * @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 ContainerNode parseJsonData(final String jsonData, final SchemaContext schemaContext,
         final String parentNodeXpath) {
         final Collection<QName> dataSchemaNodeIdentifiers =
                 getDataSchemaNodeIdentifiersByXpath(parentNodeXpath, schemaContext);
         return parseJsonData(jsonData, schemaContext, Optional.of(dataSchemaNodeIdentifiers));
     }
 
-    private static NormalizedNode parseJsonData(final String jsonData, final SchemaContext schemaContext,
+    private static ContainerNode parseJsonData(final String jsonData, final SchemaContext schemaContext,
         final Optional<Collection<QName>> dataSchemaNodeIdentifiers) {
         final JSONCodecFactory jsonCodecFactory = JSONCodecFactorySupplier.DRAFT_LHOTKA_NETMOD_YANG_JSON_02
             .getShared((EffectiveModelContext) schemaContext);
-        final NormalizedNodeResult normalizedNodeResult = new NormalizedNodeResult();
+        final DataContainerNodeBuilder<YangInstanceIdentifier.NodeIdentifier, ContainerNode> dataContainerNodeBuilder =
+                Builders.containerBuilder()
+                        .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(schemaContext.getQName()));
         final NormalizedNodeStreamWriter normalizedNodeStreamWriter = ImmutableNormalizedNodeStreamWriter
-            .from(normalizedNodeResult);
+                .from(dataContainerNodeBuilder);
         final JsonReader jsonReader = new JsonReader(new StringReader(jsonData));
         final JsonParserStream jsonParserStream;
 
@@ -119,7 +123,7 @@ public class YangUtils {
                 "Failed to parse json data. Unsupported xpath or json data:" + jsonData, illegalStateException
                 .getMessage(), illegalStateException);
         }
-        return normalizedNodeResult.getResult();
+        return dataContainerNodeBuilder.build();
     }
 
     /**
index b60e7e8..b78ab8a 100644 (file)
@@ -3,6 +3,7 @@
  *  Copyright (C) 2021-2022 Nordix Foundation
  *  Modifications Copyright (C) 2021 Pantheon.tech
  *  Modifications Copyright (C) 2021-2022 Bell Canada.
+ *  Modifications Copyright (C) 2022 TechMahindra Ltd.
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -62,17 +63,22 @@ class CpsDataServiceImplSpec extends Specification {
 
     def 'Saving json data.'() {
         given: 'schema set for given anchor and dataspace references test-tree model'
-            setupSchemaSetMocks('test-tree.yang')
+            setupSchemaSetMocks('multipleDataTree.yang')
         when: 'save data method is invoked with test-tree json data'
-            def jsonData = TestUtils.getResourceFileContent('test-tree.json')
+            def jsonData = TestUtils.getResourceFileContent('multiple-object-data.json')
             objectUnderTest.saveData(dataspaceName, anchorName, jsonData, observedTimestamp)
         then: 'the persistence service method is invoked with correct parameters'
-            1 * mockCpsDataPersistenceService.storeDataNode(dataspaceName, anchorName,
-                { dataNode -> dataNode.xpath == '/test-tree' })
+            1 * mockCpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName,
+                { dataNode -> dataNode.xpath[index] == xpath })
         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
         and: 'data updated event is sent to notification service'
             1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, '/', Operation.CREATE, observedTimestamp)
+        where:
+            index   |   xpath
+                0   | '/first-container'
+                1   | '/last-container'
+
     }
 
     def 'Saving child data fragment under existing node.'() {
@@ -82,8 +88,8 @@ class CpsDataServiceImplSpec extends Specification {
             def jsonData = '{"branch": [{"name": "New"}]}'
             objectUnderTest.saveData(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
         then: 'the persistence service method is invoked with correct parameters'
-            1 * mockCpsDataPersistenceService.addChildDataNode(dataspaceName, anchorName, '/test-tree',
-                { dataNode -> dataNode.xpath == '/test-tree/branch[@name=\'New\']' })
+            1 * mockCpsDataPersistenceService.addChildDataNodes(dataspaceName, anchorName, '/test-tree',
+                { dataNode -> dataNode.xpath[0] == '/test-tree/branch[@name=\'New\']' })
         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
         and: 'data updated event is sent to notification service'
@@ -207,8 +213,8 @@ class CpsDataServiceImplSpec extends Specification {
         when: 'replace data method is invoked with json data #jsonData and parent node xpath #parentNodeXpath'
             objectUnderTest.updateDataNodeAndDescendants(dataspaceName, anchorName, parentNodeXpath, jsonData, observedTimestamp)
         then: 'the persistence service method is invoked with correct parameters'
-            1 * mockCpsDataPersistenceService.updateDataNodeAndDescendants(dataspaceName, anchorName,
-                { dataNode -> dataNode.xpath == expectedNodeXpath })
+            1 * mockCpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName,
+                { dataNode -> dataNode.xpath[0] == expectedNodeXpath })
         and: 'data updated event is sent to notification service'
             1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, parentNodeXpath, Operation.UPDATE, observedTimestamp)
         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
index 2fc85aa..ccfb23b 100755 (executable)
@@ -3,6 +3,7 @@
  * Copyright (C) 2021-2022 Nordix Foundation.\r
  * Modifications Copyright (C) 2021-2022 Bell Canada.\r
  * Modifications Copyright (C) 2021 Pantheon.tech\r
+ * Modifications Copyright (C) 2022 TechMahindra Ltd.\r
  * ================================================================================\r
  * Licensed under the Apache License, Version 2.0 (the "License");\r
  * you may not use this file except in compliance with the License.\r
@@ -90,9 +91,9 @@ class E2ENetworkSliceSpec extends Specification {
         when: 'saveData method is invoked'\r
             cpsDataServiceImpl.saveData(dataspaceName, anchorName, jsonData, noTimestamp)\r
         then: 'Parameters are validated and processing is delegated to persistence service'\r
-            1 * mockDataStoreService.storeDataNode('someDataspace', 'someAnchor', _) >>\r
+            1 * mockDataStoreService.storeDataNodes('someDataspace', 'someAnchor', _) >>\r
                     { args -> dataNodeStored = args[2]}\r
-            def child = dataNodeStored.childDataNodes[0]\r
+            def child = dataNodeStored[0].childDataNodes[0]\r
             assert child.childDataNodes.size() == 1\r
         and: 'list of Tracking Area for a Coverage Area are stored with correct xpath and child nodes '\r
             def listOfTAForCoverageArea = child.childDataNodes[0]\r
@@ -122,10 +123,10 @@ class E2ENetworkSliceSpec extends Specification {
         when: 'saveData method is invoked'\r
             cpsDataServiceImpl.saveData('someDataspace', 'someAnchor', jsonData, noTimestamp)\r
         then: 'parameters are validated and processing is delegated to persistence service'\r
-            1 * mockDataStoreService.storeDataNode('someDataspace', 'someAnchor', _) >>\r
+            1 * mockDataStoreService.storeDataNodes('someDataspace', 'someAnchor', _) >>\r
                     { args -> dataNodeStored = args[2]}\r
         and: 'the size of the tree is correct'\r
-            def cpsRanInventory = TestUtils.getFlattenMapByXpath(dataNodeStored)\r
+            def cpsRanInventory = TestUtils.getFlattenMapByXpath(dataNodeStored[0])\r
             assert  cpsRanInventory.size() == 4\r
         and: 'ran-inventory contains the correct child node'\r
             def ranInventory = cpsRanInventory.get('/ran-inventory')\r
index e46147c..1559783 100644 (file)
@@ -2,6 +2,7 @@
  *  ============LICENSE_START=======================================================
  *  Copyright (C) 2021 Pantheon.tech
  *  Modifications Copyright (C) 2021-2022 Nordix Foundation.
+ *  Modifications Copyright (C) 2022 TechMahindra Ltd.
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
 package org.onap.cps.spi.model
 
 import org.onap.cps.TestUtils
-import org.onap.cps.spi.model.DataNodeBuilder
 import org.onap.cps.utils.DataMapUtils
 import org.onap.cps.utils.YangUtils
 import org.onap.cps.yang.YangTextSchemaSourceSetBuilder
-import org.opendaylight.yangtools.yang.common.QName
-import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier
-import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode
+import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode
 import spock.lang.Specification
 
 class DataNodeBuilderSpec extends Specification {
@@ -50,17 +48,17 @@ class DataNodeBuilderSpec extends Specification {
             'ietf/ietf-inet-types@2013-07-15.yang'
     ]
 
-    def 'Converting NormalizedNode (tree) to a DataNode (tree).'() {
+    def 'Converting ContainerNode (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: 'the json data parsed into normalized node object'
+        and: 'the json data parsed into container node object'
             def jsonData = TestUtils.getResourceFileContent('test-tree.json')
-            def normalizedNode = YangUtils.parseJsonData(jsonData, schemaContext)
-        when: 'the normalized node is converted to a data node'
-            def result = new DataNodeBuilder().withNormalizedNodeTree(normalizedNode).build()
+            def containerNode = YangUtils.parseJsonData(jsonData, schemaContext)
+        when: 'the container node is converted to a data node'
+            def result = new DataNodeBuilder().withContainerNode(containerNode).build()
             def mappedResult = TestUtils.getFlattenMapByXpath(result)
-        then: '5 DataNode objects with unique xpath were created in total'
+        then: '6 DataNode objects with unique xpath were created in total'
             mappedResult.size() == 6
         and: 'all expected xpaths were built'
             mappedResult.keySet().containsAll(expectedLeavesByXpathMap.keySet())
@@ -70,16 +68,16 @@ class DataNodeBuilderSpec extends Specification {
             }
     }
 
-    def 'Converting NormalizedNode (tree) to a DataNode (tree) for known parent node.'() {
+    def 'Converting ContainerNode (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'
+        and: 'the json data parsed into container 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 containerNode = YangUtils.parseJsonData(jsonData, schemaContext, "/test-tree")
+        when: 'the container node is converted to a data node with parent node xpath defined'
             def result = new DataNodeBuilder()
-                    .withNormalizedNodeTree(normalizedNode)
+                    .withContainerNode(containerNode)
                     .withParentNodeXpath("/test-tree")
                     .build()
             def mappedResult = TestUtils.getFlattenMapByXpath(result)
@@ -90,15 +88,15 @@ class DataNodeBuilderSpec extends Specification {
                     .containsAll(['/test-tree/branch[@name=\'Branch\']', '/test-tree/branch[@name=\'Branch\']/nest'])
     }
 
-    def 'Converting NormalizedNode (tree) to a DataNode (tree) -- augmentation case.'() {
+    def 'Converting ContainerNode (tree) to a DataNode (tree) -- augmentation case.'() {
         given: 'a schema context for expected model'
             def yangResourceNameToContent = TestUtils.getYangResourcesAsMap(networkTopologyModelRfc8345)
             def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent) getSchemaContext()
-        and: 'the json data parsed into normalized node object'
+        and: 'the json data parsed into container node object'
             def jsonData = TestUtils.getResourceFileContent('ietf/data/ietf-network-topology-sample-rfc8345.json')
-            def normalizedNode = YangUtils.parseJsonData(jsonData, schemaContext)
-        when: 'the normalized node is converted to a data node '
-            def result = new DataNodeBuilder().withNormalizedNodeTree(normalizedNode).build()
+            def containerNode = YangUtils.parseJsonData(jsonData, schemaContext)
+        when: 'the container node is converted to a data node '
+            def result = new DataNodeBuilder().withContainerNode(containerNode).build()
             def mappedResult = TestUtils.getFlattenMapByXpath(result)
         then: 'all expected data nodes are populated'
             mappedResult.size() == 32
@@ -122,17 +120,17 @@ class DataNodeBuilderSpec extends Specification {
             ])
     }
 
-    def 'Converting NormalizedNode (tree) to a DataNode (tree) for known parent node -- augmentation case.'() {
+    def 'Converting ContainerNode (tree) to a DataNode (tree) for known parent node -- augmentation case.'() {
         given: 'a schema context for expected model'
             def yangResourceNameToContent = TestUtils.getYangResourcesAsMap(networkTopologyModelRfc8345)
             def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent) getSchemaContext()
         and: 'parent node xpath referencing augmentation node within a model'
             def parentNodeXpath = "/networks/network[@network-id='otn-hc']/link[@link-id='D1,1-2-1,D2,2-1-1']"
-        and: 'the json data fragment parsed into normalized node object for given parent node xpath'
+        and: 'the json data fragment parsed into container node object for given parent node xpath'
             def jsonData = '{"source": {"source-node": "D1", "source-tp": "1-2-1"}}'
-            def normalizedNode = YangUtils.parseJsonData(jsonData, schemaContext, parentNodeXpath)
-        when: 'the normalized node is converted to a data node with given parent node xpath'
-            def result = new DataNodeBuilder().withNormalizedNodeTree(normalizedNode)
+            def containerNode = YangUtils.parseJsonData(jsonData, schemaContext, parentNodeXpath)
+        when: 'the container node is converted to a data node with given parent node xpath'
+            def result = new DataNodeBuilder().withContainerNode(containerNode)
                     .withParentNodeXpath(parentNodeXpath).build()
         then: 'the resulting data node represents a child of augmentation node'
             assert result.xpath == "/networks/network[@network-id='otn-hc']/link[@link-id='D1,1-2-1,D2,2-1-1']/source"
@@ -140,15 +138,15 @@ class DataNodeBuilderSpec extends Specification {
             assert result.leaves['source-tp'] == '1-2-1'
     }
 
-    def 'Converting NormalizedNode (tree) to a DataNode (tree) -- with ChoiceNode.'() {
+    def 'Converting ContainerNode (tree) to a DataNode (tree) -- with ChoiceNode.'() {
         given: 'a schema context for expected model'
             def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('yang-with-choice-node.yang')
             def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent) getSchemaContext()
-        and: 'the json data fragment parsed into normalized node object'
+        and: 'the json data fragment parsed into container node object'
             def jsonData = TestUtils.getResourceFileContent('data-with-choice-node.json')
-            def normalizedNode = YangUtils.parseJsonData(jsonData, schemaContext)
-        when: 'the normalized node is converted to a data node'
-            def result = new DataNodeBuilder().withNormalizedNodeTree(normalizedNode).build()
+            def containerNode = YangUtils.parseJsonData(jsonData, schemaContext)
+        when: 'the container node is converted to a data node'
+            def result = new DataNodeBuilder().withContainerNode(containerNode).build()
             def mappedResult = TestUtils.getFlattenMapByXpath(result)
         then: 'the resulting data node contains only one xpath with 3 leaves'
             mappedResult.keySet().containsAll([
@@ -159,16 +157,16 @@ class DataNodeBuilderSpec extends Specification {
             assert result.leaves['choice-case1-leaf-b'] == "test"
     }
 
-    def 'Converting NormalizedNode into DataNode collection: #scenario.'() {
+    def 'Converting ContainerNode into DataNode collection: #scenario.'() {
         given: 'a schema context for expected model'
             def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('test-tree.yang')
             def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent) getSchemaContext()
         and: 'parent node xpath referencing parent of list element'
             def parentNodeXpath = "/test-tree"
-        and: 'the json data fragment (list element) parsed into normalized node object'
-            def normalizedNode = YangUtils.parseJsonData(jsonData, schemaContext, parentNodeXpath)
-        when: 'the normalized node is converted to a data node collection'
-            def result = new DataNodeBuilder().withNormalizedNodeTree(normalizedNode)
+        and: 'the json data fragment (list element) parsed into container node object'
+            def containerNode = YangUtils.parseJsonData(jsonData, schemaContext, parentNodeXpath)
+        when: 'the container node is converted to a data node collection'
+            def result = new DataNodeBuilder().withContainerNode(containerNode)
                     .withParentNodeXpath(parentNodeXpath).buildCollection()
             def resultXpaths = result.collect { it.getXpath() }
         then: 'the resulting collection contains data nodes for expected list elements'
@@ -180,16 +178,15 @@ class DataNodeBuilderSpec extends Specification {
             'multiple entries' | '{"branch": [{"name": "One"}, {"name": "Two"}]}' | 2            | ['/test-tree/branch[@name=\'One\']', '/test-tree/branch[@name=\'Two\']']
     }
 
-    def 'Converting NormalizedNode to a DataNode collection -- edge cases: #scenario.'() {
-        when: 'the normalized node is #node'
-            def result = new DataNodeBuilder().withNormalizedNodeTree(normalizedNode).buildCollection()
+    def 'Converting ContainerNode to a DataNode collection -- edge cases: #scenario.'() {
+        when: 'the container node is #node'
+            def result = new DataNodeBuilder().withContainerNode(containerNode).buildCollection()
         then: 'the resulting collection contains data nodes for expected list elements'
-            assert result.size() == expectedSize
-            assert result.containsAll(expectedNodes)
+            assert result.isEmpty()
         where: 'following parameters are used'
-            scenario                                | node            | normalizedNode       | expectedSize | expectedNodes
-            'NormalizedNode is null'                | 'null'          | null                 | 1            | [ new DataNode() ]
-            'NormalizedNode is an unsupported type' | 'not supported' | Mock(NormalizedNode) | 0            | [ ]
+            scenario                               | containerNode
+            'ContainerNode is null'                | null
+            'ContainerNode is an unsupported type' | Mock(ContainerNode)
     }
 
     def 'Use of adding the module name prefix attribute of data node.'() {
index 40f0e0a..2eede23 100644 (file)
@@ -1,3 +1,24 @@
+/*
+ * ============LICENSE_START=======================================================
+ *  Copyright (C) 2022 Nordix Foundation
+ *  Modifications Copyright (C) 2022 TechMahindra Ltd.
+ *  ================================================================================
+ *  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
@@ -26,10 +47,10 @@ class JsonParserStreamSpec extends Specification{
             def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourcesMap).getSchemaContext()
         and: 'variable to store the result of parsing'
             DataContainerNodeBuilder<YangInstanceIdentifier.NodeIdentifier, ContainerNode> builder =
-                    Builders.containerBuilder().withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(schemaContext.getQName()));
-            def normalizedNodeStreamWriter = ImmutableNormalizedNodeStreamWriter.from(builder);
+                    Builders.containerBuilder().withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(schemaContext.getQName()))
+            def normalizedNodeStreamWriter = ImmutableNormalizedNodeStreamWriter.from(builder)
             def jsonCodecFactory = JSONCodecFactorySupplier.DRAFT_LHOTKA_NETMOD_YANG_JSON_02
-                    .getShared((EffectiveModelContext) schemaContext);
+                    .getShared((EffectiveModelContext) schemaContext)
         and: 'JSON parser stream'
             def jsonParserStream = JsonParserStream.create(normalizedNodeStreamWriter, jsonCodecFactory)
         when: 'parsing is invoked with the given JSON reader'
index 65aa3af..990b718 100644 (file)
@@ -2,6 +2,7 @@
  * ============LICENSE_START=======================================================
  *  Copyright (C) 2020-2022 Nordix Foundation
  *  Modifications Copyright (C) 2021 Pantheon.tech
+ *  Modifications Copyright (C) 2022 TechMahindra Ltd.
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -31,14 +32,20 @@ import spock.lang.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')
+            def jsonData = org.onap.cps.TestUtils.getResourceFileContent('multiple-object-data.json')
         and: 'a model for that data'
-            def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('bookstore.yang')
+            def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('multipleDataTree.yang')
             def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext()
         when: 'the json data is parsed'
-            NormalizedNode result = YangUtils.parseJsonData(jsonData, schemaContext)
-        then: 'the result is a normalized node of the correct type'
-            result.getIdentifier().nodeType == QName.create('org:onap:ccsdk:sample', '2020-09-15', 'bookstore')
+            def result = YangUtils.parseJsonData(jsonData, schemaContext)
+        then: 'a ContainerNode holding collection of normalized nodes is returned'
+            result.body().getAt(index) instanceof NormalizedNode == true
+        then: 'qualified name of children created is as expected'
+            result.body().getAt(index).getIdentifier().nodeType == QName.create('org:onap:ccsdk:multiDataTree', '2020-09-15', nodeName)
+        where:
+            index   | nodeName
+            0       | 'first-container'
+            1       | 'last-container'
     }
 
     def 'Parsing invalid data: #description.'() {
@@ -62,8 +69,10 @@ class YangUtilsSpec extends Specification {
             def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourcesMap).getSchemaContext()
         when: 'json string is parsed'
             def result = YangUtils.parseJsonData(jsonData, schemaContext, parentNodeXpath)
+        then: 'a ContainerNode holding collection of normalized nodes is returned'
+            result.body().getAt(0) instanceof NormalizedNode == true
         then: 'result represents a node of expected type'
-            result.getIdentifier().nodeType == QName.create('org:onap:cps:test:test-tree', '2020-02-02', nodeName)
+            result.body().getAt(0).getIdentifier().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'
index 236221a..6d570d6 100644 (file)
@@ -3,6 +3,7 @@
  *  Copyright (C) 2020-2021 Pantheon.tech
  *  Modifications Copyright (C) 2020-2022 Nordix Foundation
  *  Modifications Copyright (C) 2021 Bell Canada.
+ *  Modifications Copyright (C) 2022 TechMahindra Ltd.
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
  *  ============LICENSE_END=========================================================
  */
 
-package org.onap.cps.yang
+package org.onap.cps.utils.yang
 
 
 import org.onap.cps.TestUtils
 import org.onap.cps.spi.exceptions.ModelValidationException
+import org.onap.cps.yang.YangTextSchemaSourceSetBuilder
 import org.opendaylight.yangtools.yang.common.Revision
 import spock.lang.Specification