Create list-node elements (part1): CPS service and persistence layers

+ fix integrity violation exception exposed out of persistence layer
+ refactor CpsDataServiceImplSpec to eliminate repeated code

Issue-ID: CPS-360
Change-Id: Id70341fe54bf3c31af661f6aae04a7a80f4a1e9d
Signed-off-by: Ruslan Kashapov <ruslan.kashapov@pantheon.tech>
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 343a088..ae399a1 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
@@ -27,6 +27,7 @@
 import com.google.common.collect.ImmutableSet.Builder;
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
@@ -73,7 +74,30 @@
         final var fragmentEntity =
             toFragmentEntity(parentFragment.getDataspace(), parentFragment.getAnchor(), dataNode);
         parentFragment.getChildFragments().add(fragmentEntity);
-        fragmentRepository.save(parentFragment);
+        try {
+            fragmentRepository.save(parentFragment);
+        } catch (final DataIntegrityViolationException exception) {
+            throw AlreadyDefinedException.forDataNode(dataNode.getXpath(), anchorName, exception);
+        }
+    }
+
+    @Override
+    public void addListDataNodes(final String dataspaceName, final String anchorName, final String parentNodeXpath,
+        final Collection<DataNode> dataNodes) {
+        final FragmentEntity parentFragment = getFragmentByXpath(dataspaceName, anchorName, parentNodeXpath);
+        final List<FragmentEntity> newFragmentEntities =
+            dataNodes.stream().map(
+                dataNode -> toFragmentEntity(parentFragment.getDataspace(), parentFragment.getAnchor(), dataNode)
+            ).collect(Collectors.toUnmodifiableList());
+        parentFragment.getChildFragments().addAll(newFragmentEntities);
+        try {
+            fragmentRepository.save(parentFragment);
+        } catch (final DataIntegrityViolationException exception) {
+            final List<String> conflictXpaths = dataNodes.stream()
+                .map(DataNode::getXpath)
+                .collect(Collectors.toList());
+            throw AlreadyDefinedException.forDataNodes(conflictXpaths, anchorName, exception);
+        }
     }
 
     @Override
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 f632e02..0f0b1b4 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
@@ -20,6 +20,9 @@
  */
 package org.onap.cps.spi.impl
 
+import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
+import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
+
 import com.google.common.collect.ImmutableSet
 import com.google.gson.Gson
 import com.google.gson.GsonBuilder
@@ -32,14 +35,10 @@
 import org.onap.cps.spi.model.DataNode
 import org.onap.cps.spi.model.DataNodeBuilder
 import org.springframework.beans.factory.annotation.Autowired
-import org.springframework.dao.DataIntegrityViolationException
 import org.springframework.test.context.jdbc.Sql
 
 import javax.validation.ConstraintViolationException
 
-import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
-import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
-
 class CpsDataPersistenceServiceSpec extends CpsPersistenceSpecBase {
 
     @Autowired
@@ -53,6 +52,7 @@
     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 DataNode newDataNode = new DataNodeBuilder().build()
     static DataNode existingDataNode
@@ -145,7 +145,36 @@
         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 || DataIntegrityViolationException
+            'already existing child' | XPATH_DATA_NODE_WITH_DESCENDANTS | existingChildDataNode || AlreadyDefinedException
+    }
+
+    @Sql([CLEAR_DATA, SET_DATA])
+    def 'Add list-node fragment with multiple elements.'() {
+        given: 'list node data fragment as a collection of data nodes'
+            def listNodeXpaths = ['/parent-201/child-204[@key="B"]', '/parent-201/child-204[@key="C"]']
+            def listNodeCollection = buildDataNodeCollection(listNodeXpaths)
+        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 allChildXpaths = parentFragment.getChildFragments().collect { it.getXpath() }
+            assert allChildXpaths.size() == 5
+            assert allChildXpaths.containsAll(listNodeXpaths)
+    }
+
+    @Sql([CLEAR_DATA, SET_DATA])
+    def 'Add 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 added to existing parent node'
+            objectUnderTest.addListDataNodes(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
+            'already existing fragment'  | '/parent-201'   | ['/parent-201/child-204[@key="A"]'] || AlreadyDefinedException
+
     }
 
     static def createDataNodeTree(String... xpaths) {
@@ -175,10 +204,10 @@
             assert result.getChildDataNodes().size() == 0
             assertLeavesMaps(result.getLeaves(), expectedLeavesByXpathMap[XPATH_DATA_NODE_WITH_LEAVES])
         where: 'the following data is used'
-            scenario                      | inputXPath
-            'some xpath'                  |'/parent-100'
-            'root xpath'                  |'/'
-            'empty xpath'                 |''
+            scenario      | inputXPath
+            'some xpath'  | '/parent-100'
+            'root xpath'  | '/'
+            'empty xpath' | ''
     }
 
     @Sql([CLEAR_DATA, SET_DATA])
@@ -196,10 +225,10 @@
             mappedResult.forEach(
                     (xPath, dataNode) -> assertLeavesMaps(dataNode.getLeaves(), expectedLeavesByXpathMap[xPath]))
         where: 'the following data is used'
-            scenario                      | inputXPath
-            'some xpath'                  |'/parent-100'
-            'root xpath'                  |'/'
-            'empty xpath'                 |''
+            scenario      | inputXPath
+            'some xpath'  | '/parent-100'
+            'root xpath'  | '/'
+            'empty xpath' | ''
     }
 
     @Sql([CLEAR_DATA, SET_DATA])
@@ -299,6 +328,10 @@
             'non-existing xpath'     | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | 'NON-EXISTING XPATH' || DataNodeNotFoundException
     }
 
+    static Collection<DataNode> buildDataNodeCollection(xpaths) {
+        return xpaths.collect { new DataNodeBuilder().withXpath(it).build() }
+    }
+
     static DataNode buildDataNode(xpath, leaves, childDataNodes) {
         return new DataNodeBuilder().withXpath(xpath).withLeaves(leaves).withChildDataNodes(childDataNodes).build()
     }
@@ -323,7 +356,7 @@
     def static treeToFlatMapByXpath(Map<String, DataNode> flatMap, DataNode dataNodeTree) {
         flatMap.put(dataNodeTree.getXpath(), dataNodeTree)
         dataNodeTree.getChildDataNodes()
-            .forEach(childDataNode -> treeToFlatMapByXpath(flatMap, childDataNode))
+                .forEach(childDataNode -> treeToFlatMapByXpath(flatMap, childDataNode))
         return flatMap
     }
 
diff --git a/cps-ri/src/test/resources/data/fragment.sql b/cps-ri/src/test/resources/data/fragment.sql
index 3e2ae81..1897185 100755
--- a/cps-ri/src/test/resources/data/fragment.sql
+++ b/cps-ri/src/test/resources/data/fragment.sql
@@ -29,6 +29,6 @@
     (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}'),
     (4206, 1001, 3003, null, '/parent-201', '{"leaf-value": "original"}'),
-    (4207, 1001, 3003, 4206, '/parent-201/child-202', '{"common-leaf-name": "common-leaf other value", "common-leaf-name-int" : 5}'),
-    (4208, 1001, 3003, 4206, '/parent-201/child-203[@key1="A" and @key2=1]', '{"key1": "A", "key2" : 1, "other-leaf" : "leaf value"}'),
-    (4209, 1001, 3003, 4206, '/parent-201/child-203[@key1="A" and @key2=2]', '{"key1": "A", "key2" : 2, "other-leaf" : "other value"}');
\ No newline at end of file
+    (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"}');
\ No newline at end of file