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 ++++++-- 3 files changed, 115 insertions(+), 56 deletions(-) (limited to 'cps-ri/src') 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 c725b4224..b7da66e46 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 fbf414d2a..12585eb5a 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 e69cbee47..255e8e52f 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 -- cgit 1.2.3-korg