From 0c8068aadbb34f30ca58efb9a860b2d88016627a Mon Sep 17 00:00:00 2001 From: arpitsingh Date: Fri, 14 Oct 2022 02:42:43 +0530 Subject: CPS-341 Support for multiple data tree instances under 1 anchor - 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 --- .../spi/impl/CpsDataPersistenceServiceImpl.java | 46 ++++++++++- ...CpsDataPersistenceServiceIntegrationSpec.groovy | 92 +++++++++++----------- .../spi/impl/CpsDataPersistenceServiceSpec.groovy | 33 ++++++-- .../org/onap/cps/api/impl/CpsDataServiceImpl.java | 48 +++++++---- .../onap/cps/spi/CpsDataPersistenceService.java | 23 +++++- .../org/onap/cps/spi/model/DataNodeBuilder.java | 39 ++++----- .../main/java/org/onap/cps/utils/YangUtils.java | 26 +++--- .../cps/api/impl/CpsDataServiceImplSpec.groovy | 22 ++++-- .../onap/cps/api/impl/E2ENetworkSliceSpec.groovy | 9 ++- .../onap/cps/spi/model/DataNodeBuilderSpec.groovy | 83 ++++++++++--------- .../org/onap/cps/utils/JsonParserStreamSpec.groovy | 27 ++++++- .../groovy/org/onap/cps/utils/YangUtilsSpec.groovy | 21 +++-- .../yang/YangTextSchemaSourceSetBuilderSpec.groovy | 4 +- 13 files changed, 306 insertions(+), 167 deletions(-) diff --git a/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java b/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java index c725b4224e..b7da66e467 100644 --- a/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java +++ b/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java @@ -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 dataNodes) { + addChildrenDataNodes(dataspaceName, anchorName, parentNodeXpath, dataNodes); + } + @Override public void addListElements(final String dataspaceName, final String anchorName, final String parentNodeXpath, final Collection 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 dataNodes) { final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName); final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName); - final FragmentEntity fragmentEntity = convertToFragmentWithAllDescendants(dataspaceEntity, anchorEntity, - dataNode); + final List 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 dataNodes) { + final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName); + final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName); + final Collection 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); } } diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceIntegrationSpec.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceIntegrationSpec.groovy index fbf414d2ad..12585eb5a9 100755 --- a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceIntegrationSpec.groovy +++ b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceIntegrationSpec.groovy @@ -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 newDataNodes = [new DataNodeBuilder().build()] + static Collection existingDataNodes = [createDataNodeTree(XPATH_DATA_NODE_WITH_DESCENDANTS)] + static Collection 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 | dataNodes || expectedException + 'dataspace does not exist' | 'unknown' | 'not-relevant' | newDataNodes || DataspaceNotFoundException + 'schema set does not exist' | DATASPACE_NAME | 'unknown' | newDataNodes || AnchorNotFoundException + 'anchor already exists' | DATASPACE_NAME | ANCHOR_NAME1 | newDataNodes || 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 | dataNodes || expectedException + 'parent does not exist' | '/unknown' | newDataNodes || DataNodeNotFoundException + 'already existing child' | XPATH_DATA_NODE_WITH_DESCENDANTS | existingChildDataNodes || AlreadyDefinedExceptionBatch } @Sql([CLEAR_DATA, SET_DATA]) diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy index e69cbee471..255e8e52f4 100644 --- a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy +++ b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy @@ -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 diff --git a/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java b/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java index b08d8c1eba..732b494994 100755 --- a/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java +++ b/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java @@ -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 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 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 dataNodes = + buildDataNodes(dataspaceName, anchorName, parentNodeXpath, jsonData); + final ArrayList 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 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 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 dataNodes = new DataNodeBuilder() .withParentNodeXpath(parentNodeXpath) - .withNormalizedNodeTree(normalizedNode) + .withContainerNode(containerNode) .buildCollection(); if (dataNodes.isEmpty()) { throw new DataValidationException("Invalid data.", "No data nodes provided"); diff --git a/cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java b/cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java index 28b18b3b5c..b9da4af025 100644 --- a/cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java +++ b/cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java @@ -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 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 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 listElementsCollection); 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 1d8bac0dde..b23cdfc8d1 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 @@ -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 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 dataNodeCollection = buildCollectionFromNormalizedNodeTree(); + private DataNode buildFromContainerNode() { + final Collection 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 buildCollectionFromNormalizedNodeTree() { + private Collection 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(); } 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 48241ed392..9a61579b12 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 @@ -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 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> dataSchemaNodeIdentifiers) { final JSONCodecFactory jsonCodecFactory = JSONCodecFactorySupplier.DRAFT_LHOTKA_NETMOD_YANG_JSON_02 .getShared((EffectiveModelContext) schemaContext); - final NormalizedNodeResult normalizedNodeResult = new NormalizedNodeResult(); + final DataContainerNodeBuilder 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(); } /** diff --git a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy index b60e7e86e0..b78ab8a451 100644 --- a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy @@ -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' diff --git a/cps-service/src/test/groovy/org/onap/cps/api/impl/E2ENetworkSliceSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/api/impl/E2ENetworkSliceSpec.groovy index 2fc85aa5a4..ccfb23b449 100755 --- a/cps-service/src/test/groovy/org/onap/cps/api/impl/E2ENetworkSliceSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/api/impl/E2ENetworkSliceSpec.groovy @@ -3,6 +3,7 @@ * Copyright (C) 2021-2022 Nordix Foundation. * Modifications Copyright (C) 2021-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. @@ -90,9 +91,9 @@ class E2ENetworkSliceSpec extends Specification { when: 'saveData method is invoked' cpsDataServiceImpl.saveData(dataspaceName, anchorName, jsonData, noTimestamp) then: 'Parameters are validated and processing is delegated to persistence service' - 1 * mockDataStoreService.storeDataNode('someDataspace', 'someAnchor', _) >> + 1 * mockDataStoreService.storeDataNodes('someDataspace', 'someAnchor', _) >> { args -> dataNodeStored = args[2]} - def child = dataNodeStored.childDataNodes[0] + def child = dataNodeStored[0].childDataNodes[0] assert child.childDataNodes.size() == 1 and: 'list of Tracking Area for a Coverage Area are stored with correct xpath and child nodes ' def listOfTAForCoverageArea = child.childDataNodes[0] @@ -122,10 +123,10 @@ class E2ENetworkSliceSpec extends Specification { when: 'saveData method is invoked' cpsDataServiceImpl.saveData('someDataspace', 'someAnchor', jsonData, noTimestamp) then: 'parameters are validated and processing is delegated to persistence service' - 1 * mockDataStoreService.storeDataNode('someDataspace', 'someAnchor', _) >> + 1 * mockDataStoreService.storeDataNodes('someDataspace', 'someAnchor', _) >> { args -> dataNodeStored = args[2]} and: 'the size of the tree is correct' - def cpsRanInventory = TestUtils.getFlattenMapByXpath(dataNodeStored) + def cpsRanInventory = TestUtils.getFlattenMapByXpath(dataNodeStored[0]) assert cpsRanInventory.size() == 4 and: 'ran-inventory contains the correct child node' def ranInventory = cpsRanInventory.get('/ran-inventory') diff --git a/cps-service/src/test/groovy/org/onap/cps/spi/model/DataNodeBuilderSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/spi/model/DataNodeBuilderSpec.groovy index e46147c04d..1559783e97 100644 --- a/cps-service/src/test/groovy/org/onap/cps/spi/model/DataNodeBuilderSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/spi/model/DataNodeBuilderSpec.groovy @@ -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. @@ -21,13 +22,10 @@ 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.'() { diff --git a/cps-service/src/test/groovy/org/onap/cps/utils/JsonParserStreamSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/utils/JsonParserStreamSpec.groovy index 40f0e0a2ae..2eede23913 100644 --- a/cps-service/src/test/groovy/org/onap/cps/utils/JsonParserStreamSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/utils/JsonParserStreamSpec.groovy @@ -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 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' 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 65aa3af7d8..990b7186f7 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 @@ -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' diff --git a/cps-service/src/test/groovy/org/onap/cps/yang/YangTextSchemaSourceSetBuilderSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/yang/YangTextSchemaSourceSetBuilderSpec.groovy index 236221aca7..6d570d6432 100644 --- a/cps-service/src/test/groovy/org/onap/cps/yang/YangTextSchemaSourceSetBuilderSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/yang/YangTextSchemaSourceSetBuilderSpec.groovy @@ -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. @@ -20,11 +21,12 @@ * ============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 -- cgit 1.2.3-korg