Replace list-node content (part 1): CPS Service and persistence layers 68/121368/4
authorRuslan Kashapov <ruslan.kashapov@pantheon.tech>
Fri, 14 May 2021 09:04:34 +0000 (12:04 +0300)
committerRishi Chail <rishi.chail@est.tech>
Mon, 24 May 2021 15:36:55 +0000 (15:36 +0000)
Issue-ID: CPS-362
Change-Id: I669c9fc6ef67c1992fe95e17a765f0c616b00f7e
Signed-off-by: Ruslan Kashapov <ruslan.kashapov@pantheon.tech>
cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java
cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy
cps-service/src/main/java/org/onap/cps/api/CpsDataService.java
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/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy

index ae399a1..1044092 100644 (file)
@@ -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<DataNode> 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<FragmentEntity> 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);
index 0f0b1b4..1f2ea6d 100755 (executable)
@@ -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<DataNode> buildDataNodeCollection(xpaths) {
         return xpaths.collect { new DataNodeBuilder().withXpath(it).build() }
     }
index 8e59ebc..3b50c51 100644 (file)
@@ -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);
 }
index 523657a..23bf4f2 100755 (executable)
@@ -71,9 +71,6 @@ public class CpsDataServiceImpl implements CpsDataService {
         final String parentNodeXpath, final String jsonData) {
         final Collection<DataNode> 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<DataNode> 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<DataNode> 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) {
index 0ed3bf0..3b16b0d 100644 (file)
@@ -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<DataNode> dataNodes);
+
     /**
      * Get a datanode by cps path.
      *
index 5f930a1..27a5a4e 100644 (file)
@@ -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