From: Ruslan Kashapov Date: Fri, 14 May 2021 09:04:34 +0000 (+0300) Subject: Replace list-node content (part 1): CPS Service and persistence layers X-Git-Tag: 1.1.0~47 X-Git-Url: https://gerrit.onap.org/r/gitweb?a=commitdiff_plain;h=b0e78acb6aefdcb5866955802099e6091b2a6952;p=cps.git Replace list-node content (part 1): CPS Service and persistence layers Issue-ID: CPS-362 Change-Id: I669c9fc6ef67c1992fe95e17a765f0c616b00f7e Signed-off-by: Ruslan Kashapov --- 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 ae399a138..10440924e 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 @@ -35,6 +35,7 @@ import java.util.Map; import java.util.Set; import java.util.regex.Pattern; import java.util.stream.Collectors; +import javax.transaction.Transactional; import org.onap.cps.spi.CpsDataPersistenceService; import org.onap.cps.spi.FetchDescendantsOption; import org.onap.cps.spi.entities.AnchorEntity; @@ -253,6 +254,30 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService fragmentRepository.save(fragmentEntity); } + @Override + @Transactional + public void replaceListDataNodes(final String dataspaceName, final String anchorName, final String parentNodeXpath, + final Collection dataNodes) { + final var parentEntity = getFragmentByXpath(dataspaceName, anchorName, parentNodeXpath); + final var firstChildNodeXpath = dataNodes.iterator().next().getXpath(); + final var listNodeXpath = firstChildNodeXpath.substring(0, firstChildNodeXpath.lastIndexOf("[")); + removeListNodeDescendants(parentEntity, listNodeXpath); + final Set childFragmentEntities = dataNodes.stream().map( + dataNode -> convertToFragmentWithAllDescendants( + parentEntity.getDataspace(), parentEntity.getAnchor(), dataNode) + ).collect(Collectors.toUnmodifiableSet()); + parentEntity.getChildFragments().addAll(childFragmentEntities); + fragmentRepository.save(parentEntity); + } + + private void removeListNodeDescendants(final FragmentEntity parentFragmentEntity, final String listNodeXpath) { + final String listNodeXpathPrefix = listNodeXpath + "["; + if (parentFragmentEntity.getChildFragments() + .removeIf(fragment -> fragment.getXpath().startsWith(listNodeXpathPrefix))) { + fragmentRepository.save(parentFragmentEntity); + } + } + private void removeExistingDescendants(final FragmentEntity fragmentEntity) { fragmentEntity.setChildFragments(Collections.emptySet()); fragmentRepository.save(fragmentEntity); 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 0f0b1b430..1f2ea6d36 100755 --- 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 @@ -328,6 +328,36 @@ class CpsDataPersistenceServiceSpec extends CpsPersistenceSpecBase { 'non-existing xpath' | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | 'NON-EXISTING XPATH' || DataNodeNotFoundException } + @Sql([CLEAR_DATA, SET_DATA]) + def 'Replace list-node content of #scenario.'() { + given: 'list node data fragment as a collection of data nodes' + def listNodeCollection = buildDataNodeCollection(listNodeXpaths) + when: 'list-node elements replaced within the existing parent node' + objectUnderTest.replaceListDataNodes(DATASPACE_NAME, ANCHOR_NAME3, '/parent-201', listNodeCollection) + then: 'child list elements are updated as expected, non-list element remains as is' + def parentFragment = fragmentRepository.getOne(LIST_DATA_NODE_PARENT_FRAGMENT_ID) + def allChildXpaths = parentFragment.getChildFragments().collect { it.getXpath() } + assert allChildXpaths.size() == expectedChildXpaths.size() + assert allChildXpaths.containsAll(expectedChildXpaths) + where: 'following parameters were used' + scenario | listNodeXpaths || expectedChildXpaths + 'existing list-node' | ['/parent-201/child-204[@key="B"]'] || ['/parent-201/child-203', '/parent-201/child-204[@key="B"]'] + 'non-existing list-node' | ['/parent-201/child-205[@key="1"]'] || ['/parent-201/child-203', '/parent-201/child-204[@key="A"]', '/parent-201/child-204[@key="X"]', '/parent-201/child-205[@key="1"]'] + } + + @Sql([CLEAR_DATA, SET_DATA]) + def 'Replace list-node fragment error scenario: #scenario.'() { + given: 'list node data fragment as a collection of data nodes' + def listNodeCollection = buildDataNodeCollection(listNodeXpaths) + when: 'list-node elements were replaced under existing parent node' + objectUnderTest.replaceListDataNodes(DATASPACE_NAME, ANCHOR_NAME3, parentNodeXpath, listNodeCollection) + then: 'a #expectedException is thrown' + thrown(expectedException) + where: 'following parameters were used' + scenario | parentNodeXpath | listNodeXpaths || expectedException + 'parent node does not exist' | '/unknown' | ['irrelevant'] || DataNodeNotFoundException + } + static Collection buildDataNodeCollection(xpaths) { return xpaths.collect { new DataNodeBuilder().withXpath(it).build() } } diff --git a/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java b/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java index 8e59ebcbb..3b50c5144 100644 --- a/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java +++ b/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java @@ -105,4 +105,18 @@ public interface CpsDataService { */ void replaceNodeTree(@NonNull String dataspaceName, @NonNull String anchorName, @NonNull String parentNodeXpath, @NonNull String jsonData); + + /** + * Replaces (if exists) child data fragment representing list-node (with one or more elements) + * under existing data node for the given anchor and dataspace. + * + * @param dataspaceName dataspace name + * @param anchorName anchor name + * @param parentNodeXpath parent node xpath + * @param jsonData json data representing list element + * @throws DataValidationException when json data is invalid (incl. list-node being empty) + * @throws DataNodeNotFoundException when parent node cannot be found by parent node xpath + */ + void replaceListNodeData(@NonNull String dataspaceName, @NonNull String anchorName, @NonNull String parentNodeXpath, + @NonNull String jsonData); } 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 523657a7f..23bf4f2ee 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 @@ -71,9 +71,6 @@ public class CpsDataServiceImpl implements CpsDataService { final String parentNodeXpath, final String jsonData) { final Collection dataNodesCollection = buildDataNodeCollectionFromJson(dataspaceName, anchorName, parentNodeXpath, jsonData); - if (dataNodesCollection.isEmpty()) { - throw new DataValidationException("Invalid list data.", "List node is empty."); - } cpsDataPersistenceService.addListDataNodes(dataspaceName, anchorName, parentNodeXpath, dataNodesCollection); } @@ -98,6 +95,14 @@ public class CpsDataServiceImpl implements CpsDataService { cpsDataPersistenceService.replaceDataNodeTree(dataspaceName, anchorName, dataNode); } + @Override + public void replaceListNodeData(final String dataspaceName, final String anchorName, final String parentNodeXpath, + final String jsonData) { + final Collection dataNodes = + buildDataNodeCollectionFromJson(dataspaceName, anchorName, parentNodeXpath, jsonData); + cpsDataPersistenceService.replaceListDataNodes(dataspaceName, anchorName, parentNodeXpath, dataNodes); + } + private DataNode buildDataNodeFromJson(final String dataspaceName, final String anchorName, final String parentNodeXpath, final String jsonData) { @@ -123,10 +128,15 @@ public class CpsDataServiceImpl implements CpsDataService { final var schemaContext = getSchemaContext(dataspaceName, anchor.getSchemaSetName()); final NormalizedNode normalizedNode = YangUtils.parseJsonData(jsonData, schemaContext, parentNodeXpath); - return new DataNodeBuilder() + final Collection dataNodes = new DataNodeBuilder() .withParentNodeXpath(parentNodeXpath) .withNormalizedNodeTree(normalizedNode) .buildCollection(); + if (dataNodes.isEmpty()) { + throw new DataValidationException("Invalid list data.", "List node is empty."); + } + return dataNodes; + } private SchemaContext getSchemaContext(final String dataspaceName, final String schemaSetName) { 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 0ed3bf005..3b16b0d81 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 @@ -100,6 +100,17 @@ public interface CpsDataPersistenceService { */ void replaceDataNodeTree(@NonNull String dataspaceName, @NonNull String anchorName, @NonNull DataNode dataNode); + /** + * Replaces existing list data node content including descendants. + * + * @param dataspaceName dataspace name + * @param anchorName anchor name + * @param parentNodeXpath parent node xpath + * @param dataNodes collection of data nodes representing list node elements + */ + void replaceListDataNodes(@NonNull String dataspaceName, @NonNull String anchorName, + @NonNull String parentNodeXpath, @NonNull Collection dataNodes); + /** * Get a datanode by cps path. * 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 5f930a152..27a5a4e0c 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 @@ -139,6 +139,34 @@ class CpsDataServiceImplSpec extends Specification { 'level 2 node' | '/test-tree' | '{"branch": [{"name":"Name"}]}' || '/test-tree/branch[@name=\'Name\']' } + def 'Replace list-node data fragment under existing node.'() { + given: 'schema set for given anchor and dataspace references test-tree model' + setupSchemaSetMocks('test-tree.yang') + when: 'replace list data method is invoked with list-node json data' + def jsonData = '{"branch": [{"name": "A"}, {"name": "B"}]}' + objectUnderTest.replaceListNodeData(dataspaceName, anchorName, '/test-tree', jsonData) + then: 'the persistence service method is invoked with correct parameters' + 1 * mockCpsDataPersistenceService.replaceListDataNodes(dataspaceName, anchorName, '/test-tree', + { dataNodeCollection -> + { + assert dataNodeCollection.size() == 2 + assert dataNodeCollection.collect { it.getXpath() } + .containsAll(['/test-tree/branch[@name=\'A\']', '/test-tree/branch[@name=\'B\']']) + } + } + ) + } + + def 'Replace with empty list-node data fragment.'() { + given: 'schema set for given anchor and dataspace references test-tree model' + setupSchemaSetMocks('test-tree.yang') + when: 'replace list data method is invoked with empty list-node data fragment' + def jsonData = '{"branch": []}' + objectUnderTest.replaceListNodeData(dataspaceName, anchorName, '/test-tree', jsonData) + then: 'invalid data exception is thrown' + thrown(DataValidationException) + } + def setupSchemaSetMocks(String... yangResources) { def anchor = Anchor.builder().name(anchorName).schemaSetName(schemaSetName).build() mockCpsAdminService.getAnchor(dataspaceName, anchorName) >> anchor