Delete list-node p1 service and persistence layers

Persistence Layer of delete List-Node Content
Service Layer of delete List-Node Content
Updating of deprecated FragementRepository.getOne() method to FragmentRepository.getByID() in CpsDataPersistenceServiceIntegration.groovy
Remove Runtime Exceptions thrown from CpsDataService.java javadoc

Issue-ID: CPS-361
Signed-off-by: lukegleeson <luke.gleeson@est.tech>
Change-Id: Ib5762e73a6e8620c50c2e07b00086b9287770bc3
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 c638b91..af010f4 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
@@ -34,6 +34,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 import javax.transaction.Transactional;
@@ -48,6 +49,7 @@
 import org.onap.cps.spi.exceptions.AlreadyDefinedException;
 import org.onap.cps.spi.exceptions.ConcurrencyException;
 import org.onap.cps.spi.exceptions.CpsPathException;
+import org.onap.cps.spi.exceptions.DataNodeNotFoundException;
 import org.onap.cps.spi.model.DataNode;
 import org.onap.cps.spi.model.DataNodeBuilder;
 import org.onap.cps.spi.repository.AnchorRepository;
@@ -82,6 +84,7 @@
 
     private static final Gson GSON = new GsonBuilder().create();
     private static final String REG_EX_FOR_OPTIONAL_LIST_INDEX = "(\\[@[\\s\\S]+?]){0,1})";
+    private static final String REG_EX_FOR_LIST_NODE_KEY = "\\[(\\@([^/]*?))+( and)*\\]$";
 
     @Override
     public void addChildDataNode(final String dataspaceName, final String anchorName, final String parentXpath,
@@ -315,8 +318,33 @@
         fragmentRepository.save(parentEntity);
     }
 
+    @Override
+    @Transactional
+    public void deleteListDataNodes(final String dataspaceName, final String anchorName, final String listNodeXpath) {
+        final var parentNodeXpath = listNodeXpath.substring(0, listNodeXpath.lastIndexOf('/'));
+        final var parentEntity = getFragmentByXpath(dataspaceName, anchorName, parentNodeXpath);
+        final var descendantNode = listNodeXpath.substring(listNodeXpath.lastIndexOf('/'));
+        final Matcher descendantNodeHasListNodeKey = Pattern.compile(REG_EX_FOR_LIST_NODE_KEY).matcher(descendantNode);
+
+        final boolean xpathPointsToAValidChildNodeWithKey = parentEntity.getChildFragments().stream().anyMatch(
+            (fragment) -> fragment.getXpath().equals(listNodeXpath));
+
+        final boolean xpathPointsToAValidChildNodeWithoutKey = parentEntity.getChildFragments().stream().anyMatch(
+            (fragment) -> fragment.getXpath().replaceAll(REG_EX_FOR_LIST_NODE_KEY, "").equals(listNodeXpath));
+
+        if ((descendantNodeHasListNodeKey.find() && xpathPointsToAValidChildNodeWithKey)
+            ||
+            (!descendantNodeHasListNodeKey.find() && xpathPointsToAValidChildNodeWithoutKey)) {
+            removeListNodeDescendants(parentEntity, listNodeXpath);
+        } else {
+            throw new DataNodeNotFoundException(parentEntity.getDataspace().getName(),
+                parentEntity.getAnchor().getName(), listNodeXpath);
+        }
+    }
+
     private void removeListNodeDescendants(final FragmentEntity parentFragmentEntity, final String listNodeXpath) {
-        final String listNodeXpathPrefix = listNodeXpath + "[";
+        final Matcher descendantNodeHasListNodeKey = Pattern.compile(REG_EX_FOR_LIST_NODE_KEY).matcher(listNodeXpath);
+        final String listNodeXpathPrefix = listNodeXpath + (descendantNodeHasListNodeKey.find() ? "" : "[");
         if (parentFragmentEntity.getChildFragments()
             .removeIf(fragment -> fragment.getXpath().startsWith(listNodeXpathPrefix))) {
             fragmentRepository.save(parentFragmentEntity);
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 ad8db76..8217a4f 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
@@ -21,7 +21,7 @@
  */
 package org.onap.cps.spi.impl
 
-import org.onap.cps.spi.exceptions.ConcurrencyException
+import org.onap.cps.spi.exceptions.DataValidationException
 
 import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
 import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
@@ -55,7 +55,10 @@
     static final String XPATH_DATA_NODE_WITH_LEAVES = '/parent-100'
     static final long UPDATE_DATA_NODE_FRAGMENT_ID = 4202L
     static final long UPDATE_DATA_NODE_SUB_FRAGMENT_ID = 4203L
-    static final long LIST_DATA_NODE_PARENT_FRAGMENT_ID = 4206L
+    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_CHILD202_FRAGMENT_ID = 4204L
+    static final long LIST_DATA_NODE_PARENT202_FRAGMENT_ID = 4211L
 
     static final DataNode newDataNode = new DataNodeBuilder().build()
     static DataNode existingDataNode
@@ -159,7 +162,7 @@
         when: 'list-node elements added to existing parent node'
             objectUnderTest.addListDataNodes(DATASPACE_NAME, ANCHOR_NAME3, '/parent-201', listNodeCollection)
         then: 'new entries successfully persisted, parent node now contains 5 children (2 new + 3 existing before)'
-            def parentFragment = fragmentRepository.getOne(LIST_DATA_NODE_PARENT_FRAGMENT_ID)
+            def parentFragment = fragmentRepository.getById(LIST_DATA_NODE_PARENT201_FRAGMENT_ID)
             def allChildXpaths = parentFragment.getChildFragments().collect { it.getXpath() }
             assert allChildXpaths.size() == 5
             assert allChildXpaths.containsAll(listNodeXpaths)
@@ -253,7 +256,7 @@
             objectUnderTest.updateDataLeaves(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES,
                     "/parent-200/child-201", ['leaf-value': 'new'])
         then: 'leaves are updated for selected data node'
-            def updatedFragment = fragmentRepository.getOne(UPDATE_DATA_NODE_FRAGMENT_ID)
+            def updatedFragment = fragmentRepository.getById(UPDATE_DATA_NODE_FRAGMENT_ID)
             def updatedLeaves = getLeavesMap(updatedFragment)
             assert updatedLeaves.size() == 1
             assert updatedLeaves.'leaf-value' == 'new'
@@ -284,7 +287,7 @@
         when: 'replace data node tree is performed'
             objectUnderTest.replaceDataNodeTree(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, submittedDataNode)
         then: 'leaves have been updated for selected data node'
-            def updatedFragment = fragmentRepository.getOne(UPDATE_DATA_NODE_FRAGMENT_ID)
+            def updatedFragment = fragmentRepository.getById(UPDATE_DATA_NODE_FRAGMENT_ID)
             def updatedLeaves = getLeavesMap(updatedFragment)
             assert updatedLeaves.size() == 1
             assert updatedLeaves.'leaf-value' == 'new'
@@ -298,12 +301,12 @@
     def 'Replace data node tree with descendants.'() {
         given: 'data node object with leaves updated, having child with old content'
             def submittedDataNode = buildDataNode("/parent-200/child-201", ['leaf-value': 'new'], [
-                    buildDataNode("/parent-200/child-201/grand-child", ['leaf-value': 'original'], [])
+                  buildDataNode("/parent-200/child-201/grand-child", ['leaf-value': 'original'], [])
             ])
         when: 'update is performed including descendants'
             objectUnderTest.replaceDataNodeTree(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, submittedDataNode)
         then: 'leaves have been updated for selected data node'
-            def updatedFragment = fragmentRepository.getOne(UPDATE_DATA_NODE_FRAGMENT_ID)
+            def updatedFragment = fragmentRepository.getById(UPDATE_DATA_NODE_FRAGMENT_ID)
             def updatedLeaves = getLeavesMap(updatedFragment)
             assert updatedLeaves.size() == 1
             assert updatedLeaves.'leaf-value' == 'new'
@@ -323,7 +326,7 @@
         when: 'update is performed including descendants'
             objectUnderTest.replaceDataNodeTree(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, submittedDataNode)
         then: 'leaves have been updated for selected data node'
-            def updatedFragment = fragmentRepository.getOne(UPDATE_DATA_NODE_FRAGMENT_ID)
+            def updatedFragment = fragmentRepository.getById(UPDATE_DATA_NODE_FRAGMENT_ID)
             def updatedLeaves = getLeavesMap(updatedFragment)
             assert updatedLeaves.size() == 1
             assert updatedLeaves.'leaf-value' == 'new'
@@ -343,7 +346,7 @@
         when: 'update is performed including descendants'
             objectUnderTest.replaceDataNodeTree(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, submittedDataNode)
         then: 'leaves have been updated for selected data node'
-            def updatedFragment = fragmentRepository.getOne(UPDATE_DATA_NODE_FRAGMENT_ID)
+            def updatedFragment = fragmentRepository.getById(UPDATE_DATA_NODE_FRAGMENT_ID)
             def updatedLeaves = getLeavesMap(updatedFragment)
             assert updatedLeaves.size() == 1
             assert updatedLeaves.'leaf-value' == 'new'
@@ -362,7 +365,7 @@
             def submittedDataNode = buildDataNode(xpath, ['leaf-name': 'leaf-value'], [])
         when: 'attempt to update data node for #scenario'
             objectUnderTest.replaceDataNodeTree(dataspaceName, anchorName, submittedDataNode)
-        then: 'a #expectedException is thrown'  
+        then: 'a #expectedException is thrown'
             thrown(expectedException)
         where: 'the following data is used'
             scenario                 | dataspaceName  | anchorName                        | xpath                || expectedException
@@ -378,7 +381,7 @@
         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 parentFragment = fragmentRepository.getById(LIST_DATA_NODE_PARENT201_FRAGMENT_ID)
             def allChildXpaths = parentFragment.getChildFragments().collect { it.getXpath() }
             assert allChildXpaths.size() == expectedChildXpaths.size()
             assert allChildXpaths.containsAll(expectedChildXpaths)
@@ -401,6 +404,47 @@
             'parent node does not exist' | '/unknown'      | ['irrelevant'] || DataNodeNotFoundException
     }
 
+    @Sql([CLEAR_DATA, SET_DATA])
+    def 'Delete list-node content of #scenario.'() {
+        given: 'list node data fragments are present in database'
+        when: 'list-node elements deleted within the existing parent node'
+            objectUnderTest.deleteListDataNodes(DATASPACE_NAME, ANCHOR_NAME3, listNodeXpaths)
+        then: 'child list elements are removed as expected, non-list element remains as is'
+            def parentFragment = fragmentRepository.getById(listNodeFragmentID)
+            def allChildXpaths = parentFragment.getChildFragments().collect { it.getXpath() }
+            assert allChildXpaths.size() == expectedChildXpaths.size()
+            assert allChildXpaths.containsAll(expectedChildXpaths)
+        where: 'following parameters were used'
+            scenario                                          | listNodeXpaths                                               | listNodeFragmentID                   || expectedChildXpaths
+            'existing list-node with key'                     | '/parent-203/child-204[@key="A"]'                            | LIST_DATA_NODE_PARENT203_FRAGMENT_ID || ['/parent-203/child-203', '/parent-203/child-204[@key="X"]']
+            'existing list-node with key'                     | '/parent-203/child-204[@key="X"]'                            | LIST_DATA_NODE_PARENT203_FRAGMENT_ID || ['/parent-203/child-203', '/parent-203/child-204[@key="A"]']
+            'existing grand-child list node with keys'        | '/parent-203/child-204[@key="X"]/grand-child-204[@key2="Y"]' | LIST_DATA_NODE_PARENT203_FRAGMENT_ID || ['/parent-203/child-203', '/parent-203/child-204[@key="X"]', '/parent-203/child-204[@key="A"]']
+            'existing list-node with combined keys'           | '/parent-202/child-205[@key="A" and @key2="B"]'              | LIST_DATA_NODE_PARENT202_FRAGMENT_ID || ['/parent-202/child-206[@key="A"]']
+            'existing node with list node variants to delete' | '/parent-203/child-204'                                      | LIST_DATA_NODE_PARENT203_FRAGMENT_ID || ['/parent-203/child-203']
+            'existing grandchild list-node'                   | '/parent-200/child-202/grand-child-202[@key="D"]'            | LIST_DATA_NODE_CHILD202_FRAGMENT_ID  || []
+    }
+
+    @Sql([CLEAR_DATA, SET_DATA])
+    def 'Delete list-node fragment error scenario: #scenario.'() {
+        given: 'list node data fragments are present in database'
+        when: 'list-node elements are deleted under existing parent node'
+            objectUnderTest.deleteListDataNodes(DATASPACE_NAME, ANCHOR_NAME3, listNodeXpaths)
+        then: 'a #expectedException is thrown'
+            thrown(expectedException)
+        where: 'following parameters were used'
+            scenario                                            | listNodeXpaths                                    || expectedException
+            'list parent node does not exist'                   | '/unknown/unknown'                                || DataNodeNotFoundException
+            'list child nodes do not exist'                     | '/parent-200/unknown'                             || DataNodeNotFoundException
+            'list child nodes with key does not exist'          | '/parent-200/unknown[@key="C"]'                   || DataNodeNotFoundException
+            'list grandchild nodes parent does not exist'       | '/parent-200/unknown/unknown'                     || DataNodeNotFoundException
+            'non-existing parent with existing list-node'       | '/unknown/child-204'                              || DataNodeNotFoundException
+            'non-existing parent with existing list-node & key' | '/unknown/child-204[@key="A"]'                    || DataNodeNotFoundException
+            'valid with non existing key'                       | '/parent-200/child-202/grand-child-202[@key="A"]' || DataNodeNotFoundException
+            'child list node without key'                       | '/parent-200/child-204/grand-child-204'           || DataNodeNotFoundException
+            'valid list node with invalid key'                  | '/parent-203/child-204[@key="C"]'                 || DataNodeNotFoundException
+
+    }
+
     static Collection<DataNode> buildDataNodeCollection(xpaths) {
         return xpaths.collect { new DataNodeBuilder().withXpath(it).build() }
     }
diff --git a/cps-ri/src/test/resources/data/fragment.sql b/cps-ri/src/test/resources/data/fragment.sql
index d7109f2..886e6e1 100755
--- a/cps-ri/src/test/resources/data/fragment.sql
+++ b/cps-ri/src/test/resources/data/fragment.sql
@@ -49,8 +49,16 @@
     (4202, 1001, 3003, 4201, '/parent-200/child-201', '{"leaf-value": "original"}'),
     (4203, 1001, 3003, 4202, '/parent-200/child-201/grand-child', '{"leaf-value": "original"}'),
     (4204, 1001, 3003, 4201, '/parent-200/child-202', '{"common-leaf-name": "common-leaf value", "common-leaf-name-int" : 5}'),
-    (4205, 1001, 3003, 4204, '/parent-200/child-202/grand-child-202', '{"common-leaf-name": "common-leaf value", "common-leaf-name-int" : 5}'),
+    (4205, 1001, 3003, 4204, '/parent-200/child-202/grand-child-202[@key="D"]', '{"common-leaf-name": "common-leaf value", "common-leaf-name-int" : 5}'),
     (4206, 1001, 3003, null, '/parent-201', '{"leaf-value": "original"}'),
     (4207, 1001, 3003, 4206, '/parent-201/child-203', '{}'),
     (4208, 1001, 3003, 4206, '/parent-201/child-204[@key="A"]', '{"key": "A"}'),
-    (4209, 1001, 3003, 4206, '/parent-201/child-204[@key="X"]', '{"key": "X"}');
+    (4209, 1001, 3003, 4206, '/parent-201/child-204[@key="X"]', '{"key": "X"}'),
+    (4211, 1001, 3003, null, '/parent-202', '{"leaf-value": "original"}'),
+    (4212, 1001, 3003, 4211, '/parent-202/child-205[@key="A" and @key2="B"]', '{"key": "A", "key2": "B"}'),
+    (4213, 1001, 3003, 4211, '/parent-202/child-206[@key="A"]', '{"key": "A"}'),
+    (4214, 1001, 3003, null, '/parent-203', '{"leaf-value": "original"}'),
+    (4215, 1001, 3003, 4214, '/parent-203/child-203', '{}'),
+    (4216, 1001, 3003, 4214, '/parent-203/child-204[@key="A"]', '{"key": "A"}'),
+    (4217, 1001, 3003, 4214, '/parent-203/child-204[@key="X"]', '{"key": "X"}'),
+    (4218, 1001, 3003, 4217, '/parent-203/child-204[@key="X"]/grand-child-204[@key2="Y"]', '{"key": "X", "key2": "Y"}');
\ No newline at end of file