Clean Up Code around List Nodes 41/125441/7
authorDylanB95EST <dylan.byrne@est.tech>
Fri, 29 Oct 2021 16:33:06 +0000 (17:33 +0100)
committerDylanB95EST <dylan.byrne@est.tech>
Tue, 2 Nov 2021 11:59:14 +0000 (11:59 +0000)
Make sure code refers clearly to List (whole) nodes or List elements
incl. method names, parameter names,  test descriptions etc.

Issue-ID: CPS-756
Change-Id: Ic9dae6565c0e84c1ba4c2d6e891d3ea307f589da
Signed-off-by: DylanB95EST <dylan.byrne@est.tech>
17 files changed:
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImpl.java
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplSpec.groovy
cps-rest/docs/openapi/cpsData.yml
cps-rest/docs/openapi/openapi.yml
cps-rest/src/main/java/org/onap/cps/rest/controller/DataRestController.java
cps-rest/src/test/groovy/org/onap/cps/rest/controller/DataRestControllerSpec.groovy
cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java
cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceIntegrationSpec.groovy
cps-ri/src/test/resources/data/fragment.sql
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/main/java/org/onap/cps/spi/exceptions/DataNodeNotFoundException.java
cps-service/src/main/java/org/onap/cps/utils/DataMapUtils.java
cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy
cps-service/src/test/groovy/org/onap/cps/model/DataNodeBuilderSpec.groovy
cps-service/src/test/groovy/org/onap/cps/spi/exceptions/CpsExceptionsSpec.groovy

index 11d1a89..7020c74 100755 (executable)
@@ -135,7 +135,7 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService
 
     @Override
     public void addListNodeElements(final String cmHandle, final String parentNodeXpath, final String jsonData) {
-        cpsDataService.saveListNodeData(NF_PROXY_DATASPACE_NAME, cmHandle, parentNodeXpath, jsonData, NO_TIMESTAMP);
+        cpsDataService.saveListElements(NF_PROXY_DATASPACE_NAME, cmHandle, parentNodeXpath, jsonData, NO_TIMESTAMP);
     }
 
     @Override
@@ -340,7 +340,7 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService
     private void registerAndSyncNewCmHandles(final PersistenceCmHandlesList persistenceCmHandlesList)
         throws JsonProcessingException  {
         final String cmHandleJsonData = objectMapper.writeValueAsString(persistenceCmHandlesList);
-        cpsDataService.saveListNodeData(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, "/dmi-registry",
+        cpsDataService.saveListElements(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, "/dmi-registry",
             cmHandleJsonData, NO_TIMESTAMP);
 
         for (final PersistenceCmHandle persistenceCmHandle : persistenceCmHandlesList.getPersistenceCmHandles()) {
@@ -369,7 +369,7 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService
     private void parseAndRemoveCmHandlesInDmiRegistration(final DmiPluginRegistration dmiPluginRegistration) {
         for (final String cmHandle : dmiPluginRegistration.getRemovedCmHandles()) {
             try {
-                cpsDataService.deleteListNodeData(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,
+                cpsDataService.deleteListOrListElement(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,
                     "/dmi-registry/cm-handles[@id='" + cmHandle + "']", NO_TIMESTAMP);
             } catch (final DataNodeNotFoundException e) {
                 log.warn("Datanode {} not deleted message {}", cmHandle, e.getMessage());
index 1bad8ce..ae252b8 100644 (file)
@@ -119,7 +119,7 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
         when: 'addListNodeElements is invoked'
             objectUnderTest.addListNodeElements(cmHandle, xpath, jsonData)
         then: 'the CPS service method is invoked once with the expected parameters'
-            1 * mockCpsDataService.saveListNodeData(expectedDataspaceName, cmHandle, xpath, jsonData, noTimestamp)
+            1 * mockCpsDataService.saveListElements(expectedDataspaceName, cmHandle, xpath, jsonData, noTimestamp)
     }
 
     def 'Update data node leaves.'() {
@@ -155,18 +155,18 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
             def expectedJsonData = '{"cm-handles":[{"id":"123","dmi-service-name":"my-server","additional-properties":[{"name":"name1","value":"value1"},{"name":"name2","value":"value2"}]}]}'
         when: 'registration is updated'
             objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
-        then: 'the CPS save list node data is invoked with the expected parameters'
-            expectedCallsToSaveNode * mockCpsDataService.saveListNodeData('NCMP-Admin', 'ncmp-dmi-registry',
+        then: 'cps save list elements is invoked with the expected parameters'
+            expectedCallsToSaveNode * mockCpsDataService.saveListElements('NCMP-Admin', 'ncmp-dmi-registry',
                 '/dmi-registry', expectedJsonData, noTimestamp)
-        and: 'update Node and Child Data Nodes is invoked with correct parameters'
+        and: 'update node and child data nodes is invoked with correct parameters'
             expectedCallsToUpdateNode * mockCpsDataService.updateNodeLeavesAndExistingDescendantLeaves('NCMP-Admin',
                 'ncmp-dmi-registry', '/dmi-registry', expectedJsonData, noTimestamp)
-        and : 'delete list data node is invoked with the correct parameters'
-            expectedCallsToDeleteListDataNode * mockCpsDataService.deleteListNodeData('NCMP-Admin',
+        and : 'delete list or list element is invoked with the correct parameters'
+            expectedCallsToDeleteListElement * mockCpsDataService.deleteListOrListElement('NCMP-Admin',
                 'ncmp-dmi-registry', "/dmi-registry/cm-handles[@id='cmHandle001']", noTimestamp)
 
         where:
-            scenario                        | createdCmHandles      | updatedCmHandles      | removedCmHandles || expectedCallsToSaveNode   | expectedCallsToUpdateNode | expectedCallsToDeleteListDataNode
+            scenario                        | createdCmHandles      | updatedCmHandles      | removedCmHandles || expectedCallsToSaveNode   | expectedCallsToUpdateNode | expectedCallsToDeleteListElement
             'create'                        | [persistenceCmHandle] | []                    | []               || 1                         | 0                         | 0
             'update'                        | []                    | [persistenceCmHandle] | []               || 0                         | 1                         | 0
             'delete'                        | []                    | []                    | cmHandlesArray   || 0                         | 0                         | 1
@@ -185,8 +185,8 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
             def expectedJsonData = '{"cm-handles":[{"id":"123","dmi-service-name":"my-server","additional-properties":[]}]}'
         when: 'registration is updated'
             objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
-        then: 'the CPS save list node data is invoked with the expected parameters'
-            1 * mockCpsDataService.saveListNodeData('NCMP-Admin', 'ncmp-dmi-registry',
+        then: 'the cps save list element is invoked with the expected parameters'
+            1 * mockCpsDataService.saveListElements('NCMP-Admin', 'ncmp-dmi-registry',
                 '/dmi-registry', expectedJsonData, noTimestamp)
     }
 
@@ -214,7 +214,7 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
             def dmiPluginRegistration = new DmiPluginRegistration()
             dmiPluginRegistration.removedCmHandles = ['some cm handle']
         and: 'an JSON processing exception occurs'
-            mockCpsDataService.deleteListNodeData(*_) >>  { throw (new DataNodeNotFoundException('','')) }
+            mockCpsDataService.deleteListOrListElement(*_) >>  { throw (new DataNodeNotFoundException('','')) }
         when: 'registration is updated'
             objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
         then: 'no exception is thrown'
index d456f44..ca21df5 100644 (file)
@@ -46,13 +46,13 @@ nodeByDataspaceAndAnchor:
         $ref: 'components.yml#/components/responses/NotFound'
     x-codegen-request-body-name: xpath
 
-listNodeByDataspaceAndAnchor:
+listElementByDataspaceAndAnchor:
   post:
-    description: Add list-node child elements to existing node for a given anchor and dataspace
+    description: Add list element(s) to a list for a given anchor and dataspace
     tags:
       - cps-data
-    summary: Add list-node child element(s) under existing parent node
-    operationId: addListNodeElements
+    summary: Add list element(s)
+    operationId: addListElements
     parameters:
       - $ref: 'components.yml#/components/parameters/dataspaceNameInPath'
       - $ref: 'components.yml#/components/parameters/anchorNameInPath'
@@ -75,11 +75,11 @@ listNodeByDataspaceAndAnchor:
         $ref: 'components.yml#/components/responses/Forbidden'
 
   put:
-    description: Replace list-node child elements under existing node for a given anchor and dataspace
+    description: Replace list content under a given parent, anchor and dataspace
     tags:
       - cps-data
-    summary: Replace list-node child element(s) under existing parent node
-    operationId: replaceListNodeElements
+    summary: Replace list content
+    operationId: replaceListContent
     parameters:
       - $ref: 'components.yml#/components/parameters/dataspaceNameInPath'
       - $ref: 'components.yml#/components/parameters/anchorNameInPath'
@@ -102,11 +102,11 @@ listNodeByDataspaceAndAnchor:
         $ref: 'components.yml#/components/responses/Forbidden'
 
   delete:
-    description: Delete list-node child elements under existing node for a given anchor and dataspace
+    description: Delete one or all list element(s) for a given anchor and dataspace
     tags:
       - cps-data
-    summary: Delete list-node child element(s) under existing parent node
-    operationId: deleteListNodeElements
+    summary: Delete one or all list element(s)
+    operationId: deleteListOrListElement
     parameters:
       - $ref: 'components.yml#/components/parameters/dataspaceNameInPath'
       - $ref: 'components.yml#/components/parameters/anchorNameInPath'
@@ -202,4 +202,4 @@ nodesByDataspaceAndAnchor:
       '401':
         $ref: 'components.yml#/components/responses/Unauthorized'
       '403':
-        $ref: 'components.yml#/components/responses/Forbidden'
\ No newline at end of file
+        $ref: 'components.yml#/components/responses/Forbidden'
index f9881fb..76bdb80 100644 (file)
@@ -66,7 +66,7 @@ paths:
     $ref: 'cpsData.yml#/nodesByDataspaceAndAnchor'
 
   /v1/dataspaces/{dataspace-name}/anchors/{anchor-name}/list-nodes:
-    $ref: 'cpsData.yml#/listNodeByDataspaceAndAnchor'
+    $ref: 'cpsData.yml#/listElementByDataspaceAndAnchor'
 
   /v1/dataspaces/{dataspace-name}/anchors/{anchor-name}/nodes/query:
     $ref: 'cpsQuery.yml#/nodesByDataspaceAndAnchorAndCpsPath'
index 7db4e5a..f29ead9 100755 (executable)
@@ -60,9 +60,9 @@ public class DataRestController implements CpsDataApi {
     }
 
     @Override
-    public ResponseEntity<String> addListNodeElements(final String parentNodeXpath,
+    public ResponseEntity<String> addListElements(final String parentNodeXpath,
         final String dataspaceName, final String anchorName, final String jsonData, final String observedTimestamp) {
-        cpsDataService.saveListNodeData(dataspaceName, anchorName, parentNodeXpath, jsonData,
+        cpsDataService.saveListElements(dataspaceName, anchorName, parentNodeXpath, jsonData,
             toOffsetDateTime(observedTimestamp));
         return new ResponseEntity<>(HttpStatus.CREATED);
     }
@@ -94,19 +94,19 @@ public class DataRestController implements CpsDataApi {
     }
 
     @Override
-    public ResponseEntity<String> replaceListNodeElements(final String parentNodeXpath,
+    public ResponseEntity<String> replaceListContent(final String parentNodeXpath,
         final String dataspaceName, final String anchorName, final String jsonData,
         final String observedTimestamp) {
-        cpsDataService.replaceListNodeData(dataspaceName, anchorName, parentNodeXpath, jsonData,
+        cpsDataService.replaceListContent(dataspaceName, anchorName, parentNodeXpath, jsonData,
             toOffsetDateTime(observedTimestamp));
         return new ResponseEntity<>(HttpStatus.OK);
     }
 
     @Override
-    public ResponseEntity<Void> deleteListNodeElements(final String dataspaceName, final String anchorName,
-        final String listNodeXpath, final String observedTimestamp) {
+    public ResponseEntity<Void> deleteListOrListElement(final String dataspaceName, final String anchorName,
+        final String listElementXpath, final String observedTimestamp) {
         cpsDataService
-            .deleteListNodeData(dataspaceName, anchorName, listNodeXpath, toOffsetDateTime(observedTimestamp));
+            .deleteListOrListElement(dataspaceName, anchorName, listElementXpath, toOffsetDateTime(observedTimestamp));
         return new ResponseEntity<>(HttpStatus.NO_CONTENT);
     }
 
@@ -114,7 +114,7 @@ public class DataRestController implements CpsDataApi {
         return ROOT_XPATH.equals(xpath);
     }
 
-    private OffsetDateTime toOffsetDateTime(final String datetTimestamp) {
+    private static OffsetDateTime toOffsetDateTime(final String datetTimestamp) {
         try {
             return StringUtils.isEmpty(datetTimestamp)
                 ? null : OffsetDateTime.parse(datetTimestamp, ISO_TIMESTAMP_FORMATTER);
index a54f3bc..06f2f57 100755 (executable)
 
 package org.onap.cps.rest.controller
 
-import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
-import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
-import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete
-import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
-import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch
-import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
-import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put
-
 import org.onap.cps.api.CpsDataService
 import org.onap.cps.spi.model.DataNode
 import org.onap.cps.spi.model.DataNodeBuilder
@@ -44,6 +36,14 @@ import org.springframework.test.web.servlet.MockMvc
 import spock.lang.Shared
 import spock.lang.Specification
 
+import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
+import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put
+
 @WebMvcTest(DataRestController)
 class DataRestControllerSpec extends Specification {
 
@@ -145,11 +145,11 @@ class DataRestControllerSpec extends Specification {
             'without observed-timestamp' | null
     }
 
-    def 'Create list node child elements #scenario.'() {
+    def 'Save list elements #scenario.'() {
         given: 'parent node xpath and json data inputs'
             def parentNodeXpath = 'parent node xpath'
             def jsonData = 'json data'
-        when: 'post is invoked list-node endpoint'
+        when: 'list-node endpoint is invoked with post (create) operation'
             def postRequestBuilder = post("$dataNodeBaseEndpoint/anchors/$anchorName/list-nodes")
                 .contentType(MediaType.APPLICATION_JSON)
                 .param('xpath', parentNodeXpath)
@@ -160,7 +160,7 @@ class DataRestControllerSpec extends Specification {
         then: 'a created response is returned'
             response.status == expectedHttpStatus.value()
         then: 'the java API was called with the correct parameters'
-            expectedApiCount * mockCpsDataService.saveListNodeData(dataspaceName, anchorName, parentNodeXpath, jsonData,
+            expectedApiCount * mockCpsDataService.saveListElements(dataspaceName, anchorName, parentNodeXpath, jsonData,
                 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
         where:
             scenario                          | observedTimestamp              || expectedApiCount | expectedHttpStatus
@@ -303,22 +303,19 @@ class DataRestControllerSpec extends Specification {
             'with invalid observed-timestamp' | 'invalid'                      || 0                | HttpStatus.BAD_REQUEST
     }
 
-    def 'Replace list node child elements.'() {
-        given: 'parent node xpath and json data inputs'
-            def parentNodeXpath = 'parent node xpath'
-            def jsonData = 'json data'
-        when: 'put is invoked list-node endpoint'
+    def 'Replace list content #scenario.'() {
+        when: 'list-nodes endpoint is invoked with put (update) operation'
             def putRequestBuilder = put("$dataNodeBaseEndpoint/anchors/$anchorName/list-nodes")
                 .contentType(MediaType.APPLICATION_JSON)
-                .param('xpath', parentNodeXpath)
-                .content(jsonData)
+                .param('xpath', 'parent xpath')
+                .content('json data')
             if (observedTimestamp != null)
                 putRequestBuilder.param('observed-timestamp', observedTimestamp)
             def response = mvc.perform(putRequestBuilder).andReturn().response
         then: 'a success response is returned'
             response.status == expectedHttpStatus.value()
         and: 'the java API was called with the correct parameters'
-            expectedApiCount * mockCpsDataService.replaceListNodeData(dataspaceName, anchorName, parentNodeXpath, jsonData,
+            expectedApiCount * mockCpsDataService.replaceListContent(dataspaceName, anchorName, 'parent xpath', 'json data',
                 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
         where:
             scenario                          | observedTimestamp              || expectedApiCount | expectedHttpStatus
@@ -327,19 +324,17 @@ class DataRestControllerSpec extends Specification {
             'with invalid observed-timestamp' | 'invalid'                      || 0                | HttpStatus.BAD_REQUEST
     }
 
-    def 'Delete list node child elements. #scenario'() {
-        given: 'list node xpath'
-            def listNodeXpath = 'list node xpath'
-        when: 'delete is invoked list-node endpoint'
+    def 'Delete list element #scenario.'() {
+        when: 'list-nodes endpoint is invoked with delete operation'
             def deleteRequestBuilder = delete("$dataNodeBaseEndpoint/anchors/$anchorName/list-nodes")
-                .param('xpath', listNodeXpath)
+                .param('xpath', 'list element xpath')
             if (observedTimestamp != null)
                 deleteRequestBuilder.param('observed-timestamp', observedTimestamp)
             def response = mvc.perform(deleteRequestBuilder).andReturn().response
         then: 'a success response is returned'
             response.status == expectedHttpStatus.value()
         and: 'the java API was called with the correct parameters'
-            expectedApiCount * mockCpsDataService.deleteListNodeData(dataspaceName, anchorName, listNodeXpath,
+            expectedApiCount * mockCpsDataService.deleteListOrListElement(dataspaceName, anchorName, 'list element xpath',
                 { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
         where:
             scenario                          | observedTimestamp              || expectedApiCount | expectedHttpStatus
index 2397d31..8dc6c2f 100644 (file)
@@ -91,7 +91,7 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
 
     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 = "\\[(\\@([^/]*?)){0,99}( and)*\\]$";
+    private static final String REG_EX_FOR_LIST_ELEMENT_KEY_PREDICATE = "\\[(\\@([^/]*?)){0,99}( and)*\\]$";
 
     @Override
     public void addChildDataNode(final String dataspaceName, final String anchorName, final String parentXpath,
@@ -108,7 +108,7 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
     }
 
     @Override
-    public void addListDataNodes(final String dataspaceName, final String anchorName, final String parentNodeXpath,
+    public void addListElements(final String dataspaceName, final String anchorName, final String parentNodeXpath,
         final Collection<DataNode> dataNodes) {
         final FragmentEntity parentFragment = getFragmentByXpath(dataspaceName, anchorName, parentNodeXpath);
         final List<FragmentEntity> newFragmentEntities =
@@ -296,23 +296,23 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
     }
 
     private static void replaceDataNodeTree(final FragmentEntity existingFragmentEntity,
-                                            final DataNode submittedDataNode) {
+                                            final DataNode newDataNode) {
 
-        existingFragmentEntity.setAttributes(GSON.toJson(submittedDataNode.getLeaves()));
+        existingFragmentEntity.setAttributes(GSON.toJson(newDataNode.getLeaves()));
 
         final Map<String, FragmentEntity> existingChildrenByXpath = existingFragmentEntity.getChildFragments()
             .stream().collect(Collectors.toMap(FragmentEntity::getXpath, childFragmentEntity -> childFragmentEntity));
 
         final Collection<FragmentEntity> updatedChildFragments = new HashSet<>();
 
-        for (final DataNode submittedChildDataNode : submittedDataNode.getChildDataNodes()) {
+        for (final DataNode newDataNodeChild : newDataNode.getChildDataNodes()) {
             final FragmentEntity childFragment;
-            if (isNewDataNode(submittedChildDataNode, existingChildrenByXpath)) {
+            if (isNewDataNode(newDataNodeChild, existingChildrenByXpath)) {
                 childFragment = convertToFragmentWithAllDescendants(
-                    existingFragmentEntity.getDataspace(), existingFragmentEntity.getAnchor(), submittedChildDataNode);
+                    existingFragmentEntity.getDataspace(), existingFragmentEntity.getAnchor(), newDataNodeChild);
             } else {
-                childFragment = existingChildrenByXpath.get(submittedChildDataNode.getXpath());
-                replaceDataNodeTree(childFragment, submittedChildDataNode);
+                childFragment = existingChildrenByXpath.get(newDataNodeChild.getXpath());
+                replaceDataNodeTree(childFragment, newDataNodeChild);
             }
             updatedChildFragments.add(childFragment);
         }
@@ -322,19 +322,19 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
 
     @Override
     @Transactional
-    public void replaceListDataNodes(final String dataspaceName, final String anchorName, final String parentNodeXpath,
-                                     final Collection<DataNode> replacementDataNodes) {
+    public void replaceListContent(final String dataspaceName, final String anchorName, final String parentNodeXpath,
+                                   final Collection<DataNode> newListElements) {
         final FragmentEntity parentEntity = getFragmentByXpath(dataspaceName, anchorName, parentNodeXpath);
-        final String listNodeXpathPrefix = getListNodeXpathPrefix(replacementDataNodes);
+        final String listElementXpathPrefix = getListElementXpathPrefix(newListElements);
         final Map<String, FragmentEntity> existingListElementFragmentEntitiesByXPath =
-            extractListElementFragmentEntitiesByXPath(parentEntity.getChildFragments(), listNodeXpathPrefix);
-        removeExistingListElements(parentEntity.getChildFragments(), existingListElementFragmentEntitiesByXPath);
+            extractListElementFragmentEntitiesByXPath(parentEntity.getChildFragments(), listElementXpathPrefix);
+        deleteListElements(parentEntity.getChildFragments(), existingListElementFragmentEntitiesByXPath);
         final Set<FragmentEntity> updatedChildFragmentEntities = new HashSet<>();
-        for (final DataNode replacementDataNode : replacementDataNodes) {
-            final FragmentEntity existingListNodeElementEntity =
-                existingListElementFragmentEntitiesByXPath.get(replacementDataNode.getXpath());
-            final FragmentEntity entityToBeAdded = getFragmentForReplacement(parentEntity, replacementDataNode,
-                existingListNodeElementEntity);
+        for (final DataNode newListElement : newListElements) {
+            final FragmentEntity existingListElementEntity =
+                existingListElementFragmentEntitiesByXPath.get(newListElement.getXpath());
+            final FragmentEntity entityToBeAdded = getFragmentForReplacement(parentEntity, newListElement,
+                existingListElementEntity);
 
             updatedChildFragmentEntities.add(entityToBeAdded);
         }
@@ -342,85 +342,111 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
         fragmentRepository.save(parentEntity);
     }
 
-    private static void removeExistingListElements(
+
+    @Override
+    @Transactional
+    public void deleteListDataNode(final String dataspaceName, final String anchorName,
+                                   final String targetXpath) {
+        deleteDataNode(dataspaceName, anchorName, targetXpath, true);
+    }
+
+    @Override
+    @Transactional
+    public void deleteDataNode(final String dataspaceName, final String anchorName, final String targetXpath) {
+        deleteDataNode(dataspaceName, anchorName, targetXpath, false);
+    }
+
+    private void deleteDataNode(final String dataspaceName, final String anchorName, final String targetXpath,
+                                final boolean onlySupportListNodeDeletion) {
+        final String parentNodeXpath = targetXpath.substring(0, targetXpath.lastIndexOf('/'));
+        final FragmentEntity parentFragmentEntity = getFragmentByXpath(dataspaceName, anchorName, parentNodeXpath);
+        final String lastXpathElement = targetXpath.substring(targetXpath.lastIndexOf('/'));
+        final boolean isListElement = Pattern.compile(REG_EX_FOR_LIST_ELEMENT_KEY_PREDICATE)
+            .matcher(lastXpathElement).find();
+        boolean targetExist;
+        if (isListElement) {
+            targetExist = deleteDataNode(parentFragmentEntity, targetXpath);
+        } else {
+            targetExist = deleteAllListElements(parentFragmentEntity, targetXpath);
+            final boolean tryToDeleteDataNode = !targetExist && !onlySupportListNodeDeletion;
+            if (tryToDeleteDataNode) {
+                targetExist = deleteDataNode(parentFragmentEntity, targetXpath);
+            }
+        }
+        if (!targetExist) {
+            final String additionalInformation = onlySupportListNodeDeletion
+                ? "The target is probably not a List." : "";
+            throw new DataNodeNotFoundException(parentFragmentEntity.getDataspace().getName(),
+                parentFragmentEntity.getAnchor().getName(), targetXpath, additionalInformation);
+        }
+    }
+
+    private boolean deleteDataNode(final FragmentEntity parentFragmentEntity, final String targetXpath) {
+        if (parentFragmentEntity.getChildFragments()
+            .removeIf(fragment -> fragment.getXpath().equals(targetXpath))) {
+            fragmentRepository.save(parentFragmentEntity);
+            return true;
+        }
+        return false;
+    }
+
+
+    private boolean deleteAllListElements(final FragmentEntity parentFragmentEntity, final String listXpath) {
+        final String deleteTargetXpathPrefix = listXpath + "[";
+        if (parentFragmentEntity.getChildFragments()
+            .removeIf(fragment -> fragment.getXpath().startsWith(deleteTargetXpathPrefix))) {
+            fragmentRepository.save(parentFragmentEntity);
+            return true;
+        }
+        return false;
+    }
+
+    private static void deleteListElements(
         final Collection<FragmentEntity> fragmentEntities,
         final Map<String, FragmentEntity> existingListElementFragmentEntitiesByXPath) {
         fragmentEntities.removeAll(existingListElementFragmentEntitiesByXPath.values());
     }
 
-    private static String getListNodeXpathPrefix(final Collection<DataNode> replacementDataNodes) {
-        final String firstChildNodeXpath = replacementDataNodes.iterator().next().getXpath();
+    private static String getListElementXpathPrefix(final Collection<DataNode> newListElements) {
+        final String firstChildNodeXpath = newListElements.iterator().next().getXpath();
         return firstChildNodeXpath.substring(0, firstChildNodeXpath.lastIndexOf("[") + 1);
     }
 
     private static FragmentEntity getFragmentForReplacement(final FragmentEntity parentEntity,
-                                                            final DataNode replacementDataNode,
-                                                            final FragmentEntity existingListNodeElementEntity) {
-        if (existingListNodeElementEntity == null) {
+                                                            final DataNode newListElement,
+                                                            final FragmentEntity existingListElementEntity) {
+        if (existingListElementEntity == null) {
             return convertToFragmentWithAllDescendants(
-                parentEntity.getDataspace(), parentEntity.getAnchor(), replacementDataNode);
+                parentEntity.getDataspace(), parentEntity.getAnchor(), newListElement);
         }
-        if (replacementDataNode.getChildDataNodes().isEmpty()) {
-            copyAttributesFromReplacementDataNode(existingListNodeElementEntity, replacementDataNode);
-            existingListNodeElementEntity.getChildFragments().clear();
+        if (newListElement.getChildDataNodes().isEmpty()) {
+            copyAttributesFromNewListElement(existingListElementEntity, newListElement);
+            existingListElementEntity.getChildFragments().clear();
         } else {
-            replaceDataNodeTree(existingListNodeElementEntity, replacementDataNode);
+            replaceDataNodeTree(existingListElementEntity, newListElement);
         }
-        return existingListNodeElementEntity;
+        return existingListElementEntity;
     }
 
     private static boolean isNewDataNode(final DataNode replacementDataNode,
-                                         final Map<String, FragmentEntity> existingListNodeElementsByXpath) {
-        return !existingListNodeElementsByXpath.containsKey(replacementDataNode.getXpath());
+                                         final Map<String, FragmentEntity> existingListElementsByXpath) {
+        return !existingListElementsByXpath.containsKey(replacementDataNode.getXpath());
     }
 
-    private static void copyAttributesFromReplacementDataNode(final FragmentEntity existingListNodeElementEntity,
-                                                              final DataNode replacementDataNode) {
+    private static void copyAttributesFromNewListElement(final FragmentEntity existingListElementEntity,
+                                                         final DataNode newListElement) {
         final FragmentEntity replacementFragmentEntity =
-            FragmentEntity.builder().attributes(GSON.toJson(replacementDataNode.getLeaves())).build();
-        existingListNodeElementEntity.setAttributes(replacementFragmentEntity.getAttributes());
+            FragmentEntity.builder().attributes(GSON.toJson(newListElement.getLeaves())).build();
+        existingListElementEntity.setAttributes(replacementFragmentEntity.getAttributes());
     }
 
     private static Map<String, FragmentEntity> extractListElementFragmentEntitiesByXPath(
-        final Set<FragmentEntity> childEntities, final String listNodeXpathPrefix) {
+        final Set<FragmentEntity> childEntities, final String listElementXpathPrefix) {
         return childEntities.stream()
-            .filter(fragmentEntity -> fragmentEntity.getXpath().startsWith(listNodeXpathPrefix))
+            .filter(fragmentEntity -> fragmentEntity.getXpath().startsWith(listElementXpathPrefix))
             .collect(Collectors.toMap(FragmentEntity::getXpath, fragmentEntity -> fragmentEntity));
     }
 
-    @Override
-    @Transactional
-    public void deleteListDataNodes(final String dataspaceName, final String anchorName, final String listNodeXpath) {
-        final String parentNodeXpath = listNodeXpath.substring(0, listNodeXpath.lastIndexOf('/'));
-        final FragmentEntity parentEntity = getFragmentByXpath(dataspaceName, anchorName, parentNodeXpath);
-        final String 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 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);
-        }
-    }
-
     private static boolean isRootXpath(final String xpath) {
         return "/".equals(xpath) || "".equals(xpath);
     }
index 144b18b..85e1155 100755 (executable)
@@ -32,7 +32,6 @@ import org.onap.cps.spi.exceptions.DataNodeNotFoundException
 import org.onap.cps.spi.exceptions.DataspaceNotFoundException
 import org.onap.cps.spi.model.DataNode
 import org.onap.cps.spi.model.DataNodeBuilder
-import org.spockframework.util.CollectionUtil
 import org.springframework.beans.factory.annotation.Autowired
 import org.springframework.test.context.jdbc.Sql
 
@@ -157,20 +156,20 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
     }
 
     @Sql([CLEAR_DATA, SET_DATA])
-    def 'Add list-node fragment with multiple elements including an element with a child datanode.'() {
+    def 'Add multiple list elements including an element with a child datanode.'() {
         given: 'two new data nodes for an existing list'
-            def listNodeXpaths = ['/parent-201/child-204[@key="B"]', '/parent-201/child-204[@key="C"]']
-            def listNodeCollection = buildDataNodeCollection(listNodeXpaths)
+            def listElementXpaths = ['/parent-201/child-204[@key="B"]', '/parent-201/child-204[@key="C"]']
+            def listElements = toDataNodes(listElementXpaths)
         and: 'a child node for one of the new data nodes'
             def childDataNode = buildDataNode('/parent-201/child-204[@key="C"]/grand-child-204[@key2="Z"]', [leave:'value'], [])
-            listNodeCollection.iterator().next().childDataNodes = [childDataNode]
+            listElements[0].childDataNodes = [childDataNode]
         when: 'the data nodes (list elements) are added to existing parent node'
-            objectUnderTest.addListDataNodes(DATASPACE_NAME, ANCHOR_NAME3, '/parent-201', listNodeCollection)
+            objectUnderTest.addListElements(DATASPACE_NAME, ANCHOR_NAME3, '/parent-201', listElements)
         then: 'new entries successfully persisted, parent node now contains 5 children (2 new + 3 existing before)'
             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)
+            assert allChildXpaths.containsAll(listElementXpaths)
         and: 'the child node of the new list entry is also present'
             def dataspaceEntity = dataspaceRepository.getByName(DATASPACE_NAME)
             def anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, ANCHOR_NAME3)
@@ -179,15 +178,15 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
     }
 
     @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)
+    def 'Add list element error scenario: #scenario.'() {
+        given: 'list element as a collection of data nodes'
+            def listElementCollection = toDataNodes(listElementXpaths)
+        when: 'attempt to add list elements to parent node'
+            objectUnderTest.addListElements(DATASPACE_NAME, ANCHOR_NAME3, parentNodeXpath, listElementCollection)
         then: 'a #expectedException is thrown'
             thrown(expectedException)
         where: 'following parameters were used'
-            scenario                     | parentNodeXpath | listNodeXpaths                      || expectedException
+            scenario                     | parentNodeXpath | listElementXpaths                   || expectedException
             'parent node does not exist' | '/unknown'      | ['irrelevant']                      || DataNodeNotFoundException
             'already existing fragment'  | '/parent-201'   | ['/parent-201/child-204[@key="A"]'] || AlreadyDefinedException
 
@@ -385,40 +384,40 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
     }
 
     @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, parentXpath, listNodeCollection)
-        then: 'child list elements are updated as expected, non-list element remains as is'
-            def parentFragment = fragmentRepository.getById(listNodeFragmentID)
+    def 'Replace list content of #scenario.'() {
+        given: 'list element as a collection of data nodes'
+            def listElementCollection = toDataNodes(listElementXpaths)
+        when: 'list elements are replaced within the existing parent node'
+            objectUnderTest.replaceListContent(DATASPACE_NAME, ANCHOR_NAME3, parentXpath, listElementCollection)
+        then: 'list elements are updated as expected, non-list element remains as is'
+            def parentFragment = fragmentRepository.getById(listElementFragmentID)
             def allChildXpaths = parentFragment.getChildFragments().collect { it.getXpath() }
             assert allChildXpaths.size() == expectedChildXpaths.size()
             assert allChildXpaths.containsAll(expectedChildXpaths)
         where: 'following parameters were used'
-            scenario                                                       | listNodeXpaths                                                             |parentXpath              |listNodeFragmentID                     || expectedChildXpaths
-            'existing list node with non existing key'                     | ['/parent-201/child-204[@key="B"]']                                        | '/parent-201'           | LIST_DATA_NODE_PARENT201_FRAGMENT_ID  || ['/parent-201/child-203', '/parent-201/child-204[@key="B"]']
-            'non existing list node with non existing key'                 | ['/parent-201/child-205[@key="1"]']                                        | '/parent-201'           | LIST_DATA_NODE_PARENT201_FRAGMENT_ID  || ['/parent-201/child-203', '/parent-201/child-204[@key="A"]', '/parent-201/child-204[@key="X"]', '/parent-201/child-205[@key="1"]']
-            'existing list node with 1 existing key'                       | ['/parent-201/child-204[@key="X"]']                                        | '/parent-201'           | LIST_DATA_NODE_PARENT201_FRAGMENT_ID  || ['/parent-201/child-203', '/parent-201/child-204[@key="X"]']
-            'existing list-node with combined keys'                        | ['/parent-202/child-205[@key="A"]']                                        | '/parent-202'           | LIST_DATA_NODE_PARENT202_FRAGMENT_ID  || ['/parent-202/child-206[@key="A"]', '/parent-202/child-205[@key="A"]']
-            'existing grandchild list-node'                                | ['/parent-200/child-202/grand-child-202[@key="E"]']                        | '/parent-200/child-202' | LIST_DATA_NODE_CHILD202_FRAGMENT_ID   || ['/parent-200/child-202/grand-child-202[@key="E"]']
-            'existing list node with two list nodes'                       | ['/parent-201/child-204[@key="new X"]', '/parent-201/child-204[@key="Y"]'] | '/parent-201'           | LIST_DATA_NODE_PARENT201_FRAGMENT_ID  || ['/parent-201/child-203', '/parent-201/child-204[@key="new X"]', '/parent-201/child-204[@key="Y"]']
-            'existing list node with compounded list node'                 | ['/parent-202/child-205[@key="A" and @key2="B"]']                          | '/parent-202'           | LIST_DATA_NODE_PARENT202_FRAGMENT_ID  || ['/parent-202/child-206[@key="A"]', '/parent-202/child-205[@key="A" and @key2="B"]']
-            'existing list node with list node with parent with key value' | ['/parent-204[@key="L"]/child-210[@key="N"]']                              | '/parent-204[@key="L"]' | LIST_DATA_NODE_PARENT204_FRAGMENT_ID  || ['/parent-204[@key="L"]/child-210[@key="N"]']
+            scenario                                                    | listElementXpaths                                                          | parentXpath             | listElementFragmentID                 || expectedChildXpaths
+            'existing list element with non existing key'               | ['/parent-201/child-204[@key="B"]']                                        | '/parent-201'           | LIST_DATA_NODE_PARENT201_FRAGMENT_ID  || ['/parent-201/child-203', '/parent-201/child-204[@key="B"]']
+            'non existing list element with non existing key'           | ['/parent-201/child-205[@key="1"]']                                        | '/parent-201'           | LIST_DATA_NODE_PARENT201_FRAGMENT_ID  || ['/parent-201/child-203', '/parent-201/child-204[@key="A"]', '/parent-201/child-204[@key="X"]', '/parent-201/child-205[@key="1"]']
+            'list element with 1 existing key'                          | ['/parent-201/child-204[@key="X"]']                                        | '/parent-201'           | LIST_DATA_NODE_PARENT201_FRAGMENT_ID  || ['/parent-201/child-203', '/parent-201/child-204[@key="X"]']
+            'list element with combined keys'                           | ['/parent-202/child-205[@key="A"]']                                        | '/parent-202'           | LIST_DATA_NODE_PARENT202_FRAGMENT_ID  || ['/parent-202/child-206[@key="A"]', '/parent-202/child-205[@key="A"]']
+            'grandchild list element'                                   | ['/parent-200/child-202/grand-child-202[@key="E"]']                        | '/parent-200/child-202' | LIST_DATA_NODE_CHILD202_FRAGMENT_ID   || ['/parent-200/child-202/grand-child-202[@key="E"]']
+            'list element with two list elements'                       | ['/parent-201/child-204[@key="new X"]', '/parent-201/child-204[@key="Y"]'] | '/parent-201'           | LIST_DATA_NODE_PARENT201_FRAGMENT_ID  || ['/parent-201/child-203', '/parent-201/child-204[@key="new X"]', '/parent-201/child-204[@key="Y"]']
+            'list element with compounded list element'                 | ['/parent-202/child-205[@key="A" and @key2="B"]']                          | '/parent-202'           | LIST_DATA_NODE_PARENT202_FRAGMENT_ID  || ['/parent-202/child-206[@key="A"]', '/parent-202/child-205[@key="A" and @key2="B"]']
+            'list element with list element with parent with key value' | ['/parent-204[@key="L"]/child-210[@key="N"]']                              | '/parent-204[@key="L"]' | LIST_DATA_NODE_PARENT204_FRAGMENT_ID  || ['/parent-204[@key="L"]/child-210[@key="N"]']
     }
 
     @Sql([CLEAR_DATA, SET_DATA])
-    def 'Replace list-node that has children with #scenario'() {
-        given: 'list node data fragment with child data node fragments'
-            def grandChildDataNodes = buildDataNodeCollection(grandChildXpaths)
-            def listNode = new DataNodeBuilder().withXpath(childXpath).withChildDataNodes(grandChildDataNodes).build()
-        when: 'list-node elements replaced within the existing parent node'
-            objectUnderTest.replaceListDataNodes(DATASPACE_NAME, ANCHOR_NAME3, parentXpath, [ listNode ])
-        then: 'child list elements are updated as expected with non-list elements remaining as is'
-            def parentFragment = fragmentRepository.getById(listNodeFragmentId)
+    def 'Replace list content that has #scenario'() {
+        given: 'list element with child list element as a collection of data nodes'
+            def grandChildDataNodes = toDataNodes(grandChildXpaths)
+            def listElementCollection = new DataNodeBuilder().withXpath(childXpath).withChildDataNodes(grandChildDataNodes).build()
+        when: 'list elements replaced within the existing parent node'
+            objectUnderTest.replaceListContent(DATASPACE_NAME, ANCHOR_NAME3, parentXpath, [listElementCollection ])
+        then: 'list elements are updated as expected with non-list elements remaining as is'
+            def parentFragment = fragmentRepository.getById(listElementFragmentId)
             def allChildXpaths = parentFragment.getChildFragments().collect { it.getXpath() }
-            assert allChildXpaths.size() == expectedChildXpaths.size()
-            assert allChildXpaths.containsAll(expectedChildXpaths)
+            assert allChildXpaths.size() == expectedRemainingChildXpaths.size()
+            assert allChildXpaths.containsAll(expectedRemainingChildXpaths)
         and: 'grandchild list elements are updated as expected'
             def allGrandChildXpaths = parentFragment.getChildFragments().collect(){
                 it.getChildFragments().collect(){
@@ -429,21 +428,20 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
             assert grandChildXpathsToList.size() == expectedGrandChildXpaths.size()
             assert grandChildXpathsToList.containsAll(expectedGrandChildXpaths)
         where: 'the following parameters are used'
-            scenario                                                    | parentXpath   | childXpath                        | grandChildXpaths                                                                                | expectedChildXpaths                                            | listNodeFragmentId
-            'existing grandchild of list node'                          | '/parent-203' | '/parent-203/child-204[@key="X"]' | ['/parent-203/child-204/grandchild[@key="2"]']                                                  | ['/parent-203/child-203', '/parent-203/child-204[@key="X"]']   | LIST_DATA_NODE_PARENT203_FRAGMENT_ID
-            'existing grandchild of list node with two new nodes'       | '/parent-203' | '/parent-203/child-204[@key="X"]' | ['/parent-203/child-204/grandchild[@key="2"]' , '/parent-203/child-204/grandchild[@key="3"]']   | ['/parent-203/child-203', '/parent-203/child-204[@key="X"]']   | LIST_DATA_NODE_PARENT203_FRAGMENT_ID
-            'existing grandchild with compound list node'               | '/parent-205' | '/parent-205/child-205[@key="X"]' | ['/parent-205/child-205/grand-child-206[@key="Y" and @key2="Z"]']                               | ['/parent-205/child-205', '/parent-205/child-205[@key="X"]']   | LIST_DATA_NODE_PARENT205_FRAGMENT_ID
-            'two existing list node with a new node'                    | '/parent-205' | '/parent-205/child-205[@key="X"]' | ['/parent-205/child-205/grandchild[@key="A"]']                                                  | ['/parent-205/child-205', '/parent-205/child-205[@key="X"]']   | LIST_DATA_NODE_PARENT205_FRAGMENT_ID
+            scenario                                  | parentXpath   | childXpath                        | grandChildXpaths                                                                              | listElementFragmentId                || expectedRemainingChildXpaths
+            'grandchild of list'                      | '/parent-203' | '/parent-203/child-204[@key="X"]' | ['/parent-203/child-204/grandchild[@key="2"]']                                                | LIST_DATA_NODE_PARENT203_FRAGMENT_ID || ['/parent-203/child-203', '/parent-203/child-204[@key="X"]']
+            'grandchild of list with two new element' | '/parent-203' | '/parent-203/child-204[@key="X"]' | ['/parent-203/child-204/grandchild[@key="2"]' , '/parent-203/child-204/grandchild[@key="3"]'] | LIST_DATA_NODE_PARENT203_FRAGMENT_ID || ['/parent-203/child-203', '/parent-203/child-204[@key="X"]']
+            'grandchild with compound list elements'  | '/parent-205' | '/parent-205/child-205[@key="X"]' | ['/parent-205/child-205/grand-child-206[@key="Y" and @key2="Z"]']                             | LIST_DATA_NODE_PARENT205_FRAGMENT_ID || ['/parent-205/child-205', '/parent-205/child-205[@key="X"]']
     }
 
     @Sql([CLEAR_DATA, SET_DATA])
-    def 'Replace list-node content of #scenario with grandchildren.'() {
-        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, parentXpath, listNodeCollection)
+    def 'Replace list content of #scenario with grandchildren.'() {
+        given: 'list element as a collection of data nodes'
+            def listElementCollection = toDataNodes(listElementXpaths)
+        when: 'list elements are replaced within the existing parent node'
+            objectUnderTest.replaceListContent(DATASPACE_NAME, ANCHOR_NAME3, parentXpath, listElementCollection)
         then: 'child list elements are updated as expected with non-list elements remaining as is'
-            def parentFragment = fragmentRepository.getById(listNodeFragmentID)
+            def parentFragment = fragmentRepository.getById(listElementFragmentID)
             def allChildXpaths = parentFragment.getChildFragments().collect { it.getXpath() }
             assert allChildXpaths.size() == expectedChildXpaths.size()
             assert allChildXpaths.containsAll(expectedChildXpaths)
@@ -455,67 +453,94 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
             assert allGrandChildXpaths.size() == expectedGrandChildXpaths.size()
             assert allGrandChildXpaths.containsAll(expectedGrandChildXpaths)
         where: 'following parameters were used'
-            scenario                                                       | listNodeXpaths                      | parentXpath   | listNodeFragmentID                   || expectedChildXpaths                                          | expectedGrandChildXpaths
-            'existing list node with existing keys'                        | ['/parent-203/child-204[@key="X"]'] | '/parent-203' | LIST_DATA_NODE_PARENT203_FRAGMENT_ID || ['/parent-203/child-203', '/parent-203/child-204[@key="X"]'] | []
-            'non existing list node with existing keys'                    | ['/parent-203/child-204[@key="V"]'] | '/parent-203' | LIST_DATA_NODE_PARENT203_FRAGMENT_ID || ['/parent-203/child-203', '/parent-203/child-204[@key="V"]'] | []
+            scenario                                       | listElementXpaths                   | parentXpath   | listElementFragmentID                || expectedChildXpaths                                          | expectedGrandChildXpaths
+            'existing list element with existing keys'     | ['/parent-203/child-204[@key="X"]'] | '/parent-203' | LIST_DATA_NODE_PARENT203_FRAGMENT_ID || ['/parent-203/child-203', '/parent-203/child-204[@key="X"]'] | []
+            'non existing list element with existing keys' | ['/parent-203/child-204[@key="V"]'] | '/parent-203' | LIST_DATA_NODE_PARENT203_FRAGMENT_ID || ['/parent-203/child-203', '/parent-203/child-204[@key="V"]'] | []
     }
 
 
     @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)
+    def 'Replace content error scenario: #scenario.'() {
+        given: 'list element as a collection of data nodes'
+            def listElementCollection = toDataNodes(listElementXpaths)
+        when: 'list elements were replaced under existing parent node'
+            objectUnderTest.replaceListContent(DATASPACE_NAME, ANCHOR_NAME3, parentNodeXpath, listElementCollection)
         then: 'a #expectedException is thrown'
             thrown(expectedException)
         where: 'following parameters were used'
-            scenario                     | parentNodeXpath | listNodeXpaths || expectedException
+            scenario                     | parentNodeXpath | listElementXpaths || expectedException
             '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)
+    def 'Delete list scenario: #scenario.'() {
+        when: 'deleting list is executed for: #scenario.'
+            objectUnderTest.deleteListDataNode(DATASPACE_NAME, ANCHOR_NAME3, targetXpaths)
+        then: 'only the expected children remain'
+            def parentFragment = fragmentRepository.getById(parentFragmentId)
+            def remainingChildXpaths = parentFragment.getChildFragments().collect { it.getXpath() }
+            assert remainingChildXpaths.size() == expectedRemainingChildXpaths.size()
+            assert remainingChildXpaths.containsAll(expectedRemainingChildXpaths)
         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  || []
+            scenario                          | targetXpaths                                                 | parentFragmentId                     || expectedRemainingChildXpaths
+            'list element with key'           | '/parent-203/child-204[@key="A"]'                            | LIST_DATA_NODE_PARENT203_FRAGMENT_ID || ['/parent-203/child-203', '/parent-203/child-204[@key="X"]']
+            'list element with combined keys' | '/parent-202/child-205[@key="A" and @key2="B"]'              | LIST_DATA_NODE_PARENT202_FRAGMENT_ID || ['/parent-202/child-206[@key="A"]']
+            'whole list'                      | '/parent-203/child-204'                                      | LIST_DATA_NODE_PARENT203_FRAGMENT_ID || ['/parent-203/child-203']
+            'list element under list element' | '/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"]']
     }
 
     @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)
+    def 'Delete list error scenario: #scenario.'() {
+        when: 'attempting to delete scenario: #scenario.'
+            objectUnderTest.deleteListDataNode(DATASPACE_NAME, ANCHOR_NAME3, targetXpaths)
+        then: 'a DataNodeNotFoundException is thrown'
+            thrown(DataNodeNotFoundException)
         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
+            scenario                                   | targetXpaths
+            'whole list, parent node does not exist'   | '/unknown/some-child'
+            'list element, parent node does not exist' | '/unknown/child-204[@key="A"]'
+            'whole list does not exist'                | '/parent-200/unknown'
+            'list element, list does not exist'        | '/parent-200/unknown[@key="C"]'
+            'list element, element does not exist'     | '/parent-203/child-204[@key="C"]'
+            'valid datanode but not a list'            | '/parent-200/child-202'
+    }
+
+    @Sql([CLEAR_DATA, SET_DATA])
+    def 'Confirm deletion of #scenario.'() {
+        given: 'a valid data node'
+            def dataNode
+            def dataNodeXpath
+        when: 'data nodes are deleted'
+            objectUnderTest.deleteDataNode(DATASPACE_NAME, ANCHOR_NAME3, xpathForDeletion)
+        then: 'verify data nodes are removed'
+            try {
+                dataNode = objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_NAME3, getDataNodesXpaths, INCLUDE_ALL_DESCENDANTS)
+                dataNodeXpath = dataNode.getXpath()
+                assert dataNodeXpath == expectedXpaths
+            } catch (DataNodeNotFoundException) {
+                assert dataNodeXpath == expectedXpaths
+            }
+        where: 'following parameters were used'
+            scenario                                | xpathForDeletion                                   | getDataNodesXpaths                                || expectedXpaths
+            'child of target'                       | '/parent-206/child-206'                            | '/parent-206/child-206'                           || null
+            'child data node, parent still exists'  | '/parent-206/child-206'                            | '/parent-206'                                     || '/parent-206'
+            'list element'                          | '/parent-206/child-206/grand-child-206[@key="A"]'  | '/parent-206/child-206/grand-child-206[@key="A"]' || null
+            'list element, sibling still exists'    | '/parent-206/child-206/grand-child-206[@key="A"]'  | '/parent-206/child-206/grand-child-206[@key="X"]' || '/parent-206/child-206/grand-child-206[@key="X"]'
+    }
 
+    @Sql([CLEAR_DATA, SET_DATA])
+    def 'Delete data node with #scenario.'() {
+        when: 'data node is deleted'
+            objectUnderTest.deleteDataNode(DATASPACE_NAME, ANCHOR_NAME3, datanodeXpath)
+        then: 'a #expectedException is thrown'
+            thrown(DataNodeNotFoundException)
+        where: 'the following parameters were used'
+            scenario                                        | datanodeXpath
+            'valid data node, non existent child node'      | '/parent-203/child-non-existent'
+            'invalid list element'                          | '/parent-206/child-206/grand-child-206@key="A"]'
     }
 
-    static Collection<DataNode> buildDataNodeCollection(xpaths) {
+    static Collection<DataNode> toDataNodes(xpaths) {
         return xpaths.collect { new DataNodeBuilder().withXpath(it).build() }
     }
 
index f6e887e..5d3a328 100755 (executable)
@@ -68,4 +68,9 @@ INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH, ATTRIBUTES)
     (4222, 1001, 3003, 4221, '/parent-205/child-205', '{}'),
     (4223, 1001, 3003, 4221, '/parent-205/child-205[@key="X"]', '{"key": "X"}'),
     (4224, 1001, 3003, 4223, '/parent-205/child-205[@key="X"]/grand-child-206[@key="Y"]', '{"key": "Y", "key2": "Z"}'),
-    (4225, 1001, 3003, 4223, '/parent-205/child-205[@key="X"]/grand-child-206[@key="Y" and @key2="Z"]', '{"key": "Y", "key2": "Z"}');
\ No newline at end of file
+    (4225, 1001, 3003, 4223, '/parent-205/child-205[@key="X"]/grand-child-206[@key="Y" and @key2="Z"]', '{"key": "Y", "key2": "Z"}'),
+    (4226, 1001, 3003, null, '/parent-206', '{"leaf-value": "original"}'),
+    (4227, 1001, 3003, 4226, '/parent-206/child-206', '{}'),
+    (4228, 1001, 3003, 4227, '/parent-206/child-206/grand-child-206', '{}'),
+    (4229, 1001, 3003, 4227, '/parent-206/child-206/grand-child-206[@key="A"]', '{"key": "A"}'),
+    (4230, 1001, 3003, 4227, '/parent-206/child-206/grand-child-206[@key="X"]', '{"key": "X"}');
\ No newline at end of file
index 31a7517..e6cb65f 100644 (file)
@@ -56,17 +56,18 @@ public interface CpsDataService {
         @NonNull String jsonData, OffsetDateTime observedTimestamp);
 
     /**
-     * Persists child data fragment representing list-node (with one or more elements) under existing data node for the
+     * Persists child data fragment representing one or more list 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
+     * @param dataspaceName     dataspace name
+     * @param anchorName        anchor name
+     * @param parentNodeXpath   parent node xpath
+     * @param jsonData          json data representing list element(s)
      * @param observedTimestamp observedTimestamp
      */
-    void saveListNodeData(@NonNull String dataspaceName, @NonNull String anchorName, @NonNull String parentNodeXpath,
-        @NonNull String jsonData, OffsetDateTime observedTimestamp);
+    void saveListElements(@NonNull String dataspaceName, @NonNull String anchorName,
+                              @NonNull String parentNodeXpath,
+                              @NonNull String jsonData, OffsetDateTime observedTimestamp);
 
     /**
      * Retrieves datanode by XPath for given dataspace and anchor.
@@ -106,29 +107,28 @@ public interface CpsDataService {
         @NonNull String jsonData, OffsetDateTime observedTimestamp);
 
     /**
-     * Replaces (if exists) child data fragment representing list-node (with one or more elements) under existing data
-     * node for the given anchor and dataspace.
+     * Replaces list content by removing all existing elements and inserting the given new elements as json
+     * under given parent, anchor and dataspace.
      *
      * @param dataspaceName     dataspace name
      * @param anchorName        anchor name
      * @param parentNodeXpath   parent node xpath
-     * @param jsonData          json data representing list element
+     * @param jsonData          json data representing the new list elements
      * @param observedTimestamp observedTimestamp
      */
-    void replaceListNodeData(@NonNull String dataspaceName, @NonNull String anchorName, @NonNull String parentNodeXpath,
-        @NonNull String jsonData, OffsetDateTime observedTimestamp);
+    void replaceListContent(@NonNull String dataspaceName, @NonNull String anchorName, @NonNull String parentNodeXpath,
+                            @NonNull String jsonData, OffsetDateTime observedTimestamp);
 
     /**
-     * Deletes (if exists) child data fragment representing list-node (with one or more elements) under existing data
-     * node for the given anchor and dataspace.
+     * Deletes a list or a list-element under given anchor and dataspace.
      *
      * @param dataspaceName dataspace name
      * @param anchorName    anchor name
-     * @param listNodeXpath list node xpath
+     * @param listElementXpath list element xpath
      * @param observedTimestamp observedTimestamp
      */
-    void deleteListNodeData(@NonNull String dataspaceName, @NonNull String anchorName, @NonNull String listNodeXpath,
-        OffsetDateTime observedTimestamp);
+    void deleteListOrListElement(@NonNull String dataspaceName, @NonNull String anchorName,
+                             @NonNull String listElementXpath, OffsetDateTime observedTimestamp);
 
     /**
      * Updates leaves of DataNode for given dataspace and anchor using xpath, along with the leaves of each Child Data
index 7b3567e..44a17f8 100755 (executable)
@@ -64,7 +64,7 @@ public class CpsDataServiceImpl implements CpsDataService {
     @Override
     public void saveData(final String dataspaceName, final String anchorName, final String jsonData,
         final OffsetDateTime observedTimestamp) {
-        final var dataNode = buildDataNodeFromJson(dataspaceName, anchorName, ROOT_NODE_XPATH, jsonData);
+        final var dataNode = buildDataNode(dataspaceName, anchorName, ROOT_NODE_XPATH, jsonData);
         cpsDataPersistenceService.storeDataNode(dataspaceName, anchorName, dataNode);
         processDataUpdatedEventAsync(dataspaceName, anchorName, observedTimestamp);
     }
@@ -72,17 +72,18 @@ public class CpsDataServiceImpl implements CpsDataService {
     @Override
     public void saveData(final String dataspaceName, final String anchorName, final String parentNodeXpath,
         final String jsonData, final OffsetDateTime observedTimestamp) {
-        final var dataNode = buildDataNodeFromJson(dataspaceName, anchorName, parentNodeXpath, jsonData);
+        final var dataNode = buildDataNode(dataspaceName, anchorName, parentNodeXpath, jsonData);
         cpsDataPersistenceService.addChildDataNode(dataspaceName, anchorName, parentNodeXpath, dataNode);
         processDataUpdatedEventAsync(dataspaceName, anchorName, observedTimestamp);
     }
 
     @Override
-    public void saveListNodeData(final String dataspaceName, final String anchorName,
+    public void saveListElements(final String dataspaceName, final String anchorName,
         final String parentNodeXpath, final String jsonData, final OffsetDateTime observedTimestamp) {
-        final Collection<DataNode> dataNodesCollection =
-            buildDataNodeCollectionFromJson(dataspaceName, anchorName, parentNodeXpath, jsonData);
-        cpsDataPersistenceService.addListDataNodes(dataspaceName, anchorName, parentNodeXpath, dataNodesCollection);
+        final Collection<DataNode> listElementDataNodeCollection =
+            buildDataNodes(dataspaceName, anchorName, parentNodeXpath, jsonData);
+        cpsDataPersistenceService.addListElements(dataspaceName, anchorName, parentNodeXpath,
+            listElementDataNodeCollection);
         processDataUpdatedEventAsync(dataspaceName, anchorName, observedTimestamp);
     }
 
@@ -95,7 +96,7 @@ public class CpsDataServiceImpl implements CpsDataService {
     @Override
     public void updateNodeLeaves(final String dataspaceName, final String anchorName, final String parentNodeXpath,
         final String jsonData, final OffsetDateTime observedTimestamp) {
-        final var dataNode = buildDataNodeFromJson(dataspaceName, anchorName, parentNodeXpath, jsonData);
+        final var dataNode = buildDataNode(dataspaceName, anchorName, parentNodeXpath, jsonData);
         cpsDataPersistenceService
             .updateDataLeaves(dataspaceName, anchorName, dataNode.getXpath(), dataNode.getLeaves());
         processDataUpdatedEventAsync(dataspaceName, anchorName, observedTimestamp);
@@ -107,7 +108,8 @@ public class CpsDataServiceImpl implements CpsDataService {
         final String dataNodeUpdatesAsJson,
         final OffsetDateTime observedTimestamp) {
         final Collection<DataNode> dataNodeUpdates =
-            buildDataNodeCollectionFromJson(dataspaceName, anchorName, parentNodeXpath, dataNodeUpdatesAsJson);
+            buildDataNodes(dataspaceName, anchorName,
+                parentNodeXpath, dataNodeUpdatesAsJson);
         for (final DataNode dataNodeUpdate : dataNodeUpdates) {
             processDataNodeUpdate(dataspaceName, anchorName, dataNodeUpdate);
         }
@@ -117,30 +119,29 @@ public class CpsDataServiceImpl implements CpsDataService {
     @Override
     public void replaceNodeTree(final String dataspaceName, final String anchorName, final String parentNodeXpath,
         final String jsonData, final OffsetDateTime observedTimestamp) {
-        final var dataNode = buildDataNodeFromJson(dataspaceName, anchorName, parentNodeXpath, jsonData);
+        final var dataNode = buildDataNode(dataspaceName, anchorName, parentNodeXpath, jsonData);
         cpsDataPersistenceService.replaceDataNodeTree(dataspaceName, anchorName, dataNode);
         processDataUpdatedEventAsync(dataspaceName, anchorName, observedTimestamp);
     }
 
     @Override
-    public void replaceListNodeData(final String dataspaceName, final String anchorName, final String parentNodeXpath,
-        final String jsonData, final OffsetDateTime observedTimestamp) {
-        final Collection<DataNode> dataNodes =
-            buildDataNodeCollectionFromJson(dataspaceName, anchorName, parentNodeXpath, jsonData);
-        cpsDataPersistenceService.replaceListDataNodes(dataspaceName, anchorName, parentNodeXpath, dataNodes);
+    public void replaceListContent(final String dataspaceName, final String anchorName, final String parentNodeXpath,
+                                   final String jsonData, final OffsetDateTime observedTimestamp) {
+        final Collection<DataNode> newListElements =
+            buildDataNodes(dataspaceName, anchorName, parentNodeXpath, jsonData);
+        cpsDataPersistenceService.replaceListContent(dataspaceName, anchorName, parentNodeXpath, newListElements);
         processDataUpdatedEventAsync(dataspaceName, anchorName, observedTimestamp);
     }
 
     @Override
-    public void deleteListNodeData(final String dataspaceName, final String anchorName, final String listNodeXpath,
+    public void deleteListOrListElement(final String dataspaceName, final String anchorName, final String listNodeXpath,
         final OffsetDateTime observedTimestamp) {
-        cpsDataPersistenceService.deleteListDataNodes(dataspaceName, anchorName, listNodeXpath);
+        cpsDataPersistenceService.deleteListDataNode(dataspaceName, anchorName, listNodeXpath);
         processDataUpdatedEventAsync(dataspaceName, anchorName, observedTimestamp);
     }
 
-
-    private DataNode buildDataNodeFromJson(final String dataspaceName, final String anchorName,
-        final String parentNodeXpath, final String jsonData) {
+    private DataNode buildDataNode(final String dataspaceName, final String anchorName,
+                                   final String parentNodeXpath, final String jsonData) {
 
         final var anchor = cpsAdminService.getAnchor(dataspaceName, anchorName);
         final var schemaContext = getSchemaContext(dataspaceName, anchor.getSchemaSetName());
@@ -157,8 +158,10 @@ public class CpsDataServiceImpl implements CpsDataService {
             .build();
     }
 
-    private Collection<DataNode> buildDataNodeCollectionFromJson(final String dataspaceName, final String anchorName,
-        final String parentNodeXpath, final String jsonData) {
+    private Collection<DataNode> buildDataNodes(final String dataspaceName,
+                                                final String anchorName,
+                                                final String parentNodeXpath,
+                                                final String jsonData) {
 
         final var anchor = cpsAdminService.getAnchor(dataspaceName, anchorName);
         final var schemaContext = getSchemaContext(dataspaceName, anchor.getSchemaSetName());
@@ -169,7 +172,7 @@ public class CpsDataServiceImpl implements CpsDataService {
             .withNormalizedNodeTree(normalizedNode)
             .buildCollection();
         if (dataNodes.isEmpty()) {
-            throw new DataValidationException("Invalid list data.", "List node is empty.");
+            throw new DataValidationException("Invalid data.", "No data nodes provided");
         }
         return dataNodes;
 
index bf8dd1a..b8c472f 100644 (file)
@@ -55,16 +55,16 @@ public interface CpsDataPersistenceService {
         @NonNull DataNode dataNode);
 
     /**
-     * Adds list node child elements to a Fragment.
+     * Adds list child elements to a Fragment.
      *
-     * @param dataspaceName   dataspace name
-     * @param anchorName      anchor name
-     * @param parentNodeXpath parent node xpath
-     * @param dataNodes       collection of data nodes representing list node elements
+     * @param dataspaceName          dataspace name
+     * @param anchorName             anchor name
+     * @param parentNodeXpath        parent node xpath
+     * @param listElementsCollection collection of data nodes representing list elements
      */
 
-    void addListDataNodes(@NonNull String dataspaceName, @NonNull String anchorName, @NonNull String parentNodeXpath,
-        @NonNull Collection<DataNode> dataNodes);
+    void addListElements(@NonNull String dataspaceName, @NonNull String anchorName, @NonNull String parentNodeXpath,
+        @NonNull Collection<DataNode> listElementsCollection);
 
     /**
      * Retrieves datanode by XPath for given dataspace and anchor.
@@ -101,25 +101,36 @@ public interface CpsDataPersistenceService {
     void replaceDataNodeTree(@NonNull String dataspaceName, @NonNull String anchorName, @NonNull DataNode dataNode);
 
     /**
-     * Replaces existing list data node content including descendants.
+     * Replaces list content by removing all existing elements and inserting the given new elements
+     * under given parent, anchor and dataspace.
      *
      * @param dataspaceName   dataspace name
      * @param anchorName      anchor name
      * @param parentNodeXpath parent node xpath
-     * @param dataNodes       collection of data nodes representing list node elements
+     * @param newListElements collection of data nodes representing the new list content
+     */
+    void replaceListContent(@NonNull String dataspaceName, @NonNull String anchorName,
+                            @NonNull String parentNodeXpath, @NonNull Collection<DataNode> newListElements);
+
+    /**
+     * Deletes any dataNode, yang container or yang list or yang list element.
+     *
+     * @param dataspaceName   dataspace name
+     * @param anchorName      anchor name
+     * @param targetXpath     xpath to list or list element (include [@key=value] to delete a single list element)
      */
-    void replaceListDataNodes(@NonNull String dataspaceName, @NonNull String anchorName,
-        @NonNull String parentNodeXpath, @NonNull Collection<DataNode> dataNodes);
+    void deleteDataNode(@NonNull String dataspaceName, @NonNull String anchorName,
+                        @NonNull String targetXpath);
 
     /**
-     * Deletes existing list data node content including descendants.
+     * Deletes existing a single list element or the whole list.
      *
      * @param dataspaceName   dataspace name
      * @param anchorName      anchor name
-     * @param listNodeXpath   list node xpath
+     * @param targetXpath     xpath to list or list element (include [@key=value] to delete a single list element)
      */
-    void deleteListDataNodes(@NonNull String dataspaceName, @NonNull String anchorName,
-                              @NonNull String listNodeXpath);
+    void deleteListDataNode(@NonNull String dataspaceName, @NonNull String anchorName,
+                                 @NonNull String targetXpath);
 
     /**
      * Get a datanode by cps path.
index 848d569..b717a2b 100755 (executable)
@@ -28,6 +28,21 @@ public class DataNodeNotFoundException extends DataValidationException {
 
     private static final long serialVersionUID = 7786740001662205407L;
 
+    /**
+     * Constructor.
+     *
+     * @param dataspaceName         the name of the dataspace
+     * @param anchorName            the anchor name
+     * @param xpath                 datanode xpath
+     * @param additionalInformation additional information
+     */
+    public DataNodeNotFoundException(final String dataspaceName, final String anchorName, final String xpath,
+                                     final String additionalInformation) {
+        super("DataNode not found", String
+            .format("DataNode with xpath %s was not found for anchor %s and dataspace %s, %s.", xpath,
+                anchorName, dataspaceName, additionalInformation));
+    }
+
     /**
      * Constructor.
      *
index 3ee6afb..71a95f1 100644 (file)
@@ -58,7 +58,7 @@ public class DataMapUtils {
         return ImmutableMap.<String, Object>builder()
             .putAll(
                 dataNodes.stream()
-                    .filter(dataNode -> isListNode(dataNode.getXpath()))
+                    .filter(dataNode -> isListElement(dataNode.getXpath()))
                     .collect(groupingBy(
                         dataNode -> getNodeIdentifier(dataNode.getXpath()),
                         mapping(DataMapUtils::toDataMap, toUnmodifiableList())
@@ -86,10 +86,10 @@ public class DataMapUtils {
     }
 
     private static boolean isContainerNode(final String xpath) {
-        return !isListNode(xpath);
+        return !isListElement(xpath);
     }
 
-    private static boolean isListNode(final String xpath) {
+    private static boolean isListElement(final String xpath) {
         return xpath.endsWith("]");
     }
 }
index 6a0a464..2bd4482 100644 (file)
@@ -22,7 +22,6 @@
 
 package org.onap.cps.api.impl
 
-import java.time.OffsetDateTime
 import org.onap.cps.TestUtils
 import org.onap.cps.api.CpsAdminService
 import org.onap.cps.api.CpsModuleService
@@ -36,6 +35,8 @@ import org.onap.cps.yang.YangTextSchemaSourceSet
 import org.onap.cps.yang.YangTextSchemaSourceSetBuilder
 import spock.lang.Specification
 
+import java.time.OffsetDateTime
+
 class CpsDataServiceImplSpec extends Specification {
     def mockCpsDataPersistenceService = Mock(CpsDataPersistenceService)
     def mockCpsAdminService = Mock(CpsAdminService)
@@ -84,14 +85,14 @@ class CpsDataServiceImplSpec extends Specification {
             1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, observedTimestamp)
     }
 
-    def 'Saving list-node data fragment under existing node.'() {
+    def 'Saving list element data fragment under existing node.'() {
         given: 'schema set for given anchor and dataspace references test-tree model'
             setupSchemaSetMocks('test-tree.yang')
-        when: 'save data method is invoked with list-node json data'
+        when: 'save data method is invoked with list element json data'
             def jsonData = '{"branch": [{"name": "A"}, {"name": "B"}]}'
-            objectUnderTest.saveListNodeData(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
+            objectUnderTest.saveListElements(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
         then: 'the persistence service method is invoked with correct parameters'
-            1 * mockCpsDataPersistenceService.addListDataNodes(dataspaceName, anchorName, '/test-tree',
+            1 * mockCpsDataPersistenceService.addListElements(dataspaceName, anchorName, '/test-tree',
                 { dataNodeCollection ->
                     {
                         assert dataNodeCollection.size() == 2
@@ -104,12 +105,12 @@ class CpsDataServiceImplSpec extends Specification {
             1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, observedTimestamp)
     }
 
-    def 'Saving empty list-node data fragment.'() {
+    def 'Saving empty list element data fragment.'() {
         given: 'schema set for given anchor and dataspace references test-tree model'
             setupSchemaSetMocks('test-tree.yang')
-        when: 'save data method is invoked with empty list-node data fragment'
+        when: 'save data method is invoked with an empty list'
             def jsonData = '{"branch": []}'
-            objectUnderTest.saveListNodeData(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
+            objectUnderTest.saveListElements(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
         then: 'invalid data exception is thrown'
             thrown(DataValidationException)
     }
@@ -185,14 +186,14 @@ 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.'() {
+    def 'Replace list content data fragment under parent 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'
+        when: 'replace list data method is invoked with list element json data'
             def jsonData = '{"branch": [{"name": "A"}, {"name": "B"}]}'
-            objectUnderTest.replaceListNodeData(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
+            objectUnderTest.replaceListContent(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
         then: 'the persistence service method is invoked with correct parameters'
-            1 * mockCpsDataPersistenceService.replaceListDataNodes(dataspaceName, anchorName, '/test-tree',
+            1 * mockCpsDataPersistenceService.replaceListContent(dataspaceName, anchorName, '/test-tree',
                 { dataNodeCollection ->
                     {
                         assert dataNodeCollection.size() == 2
@@ -205,23 +206,23 @@ class CpsDataServiceImplSpec extends Specification {
             1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, observedTimestamp)
     }
 
-    def 'Replace with empty list-node data fragment.'() {
+    def 'Replace whole list content with empty list element.'() {
         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'
+        when: 'replace list data method is invoked with empty list'
             def jsonData = '{"branch": []}'
-            objectUnderTest.replaceListNodeData(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
+            objectUnderTest.replaceListContent(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp)
         then: 'invalid data exception is thrown'
             thrown(DataValidationException)
     }
 
-    def 'Delete list-node data fragment under existing node.'() {
+    def 'Delete list element under existing node.'() {
         given: 'schema set for given anchor and dataspace references test-tree model'
             setupSchemaSetMocks('test-tree.yang')
-        when: 'delete list data method is invoked with list-node json data'
-            objectUnderTest.deleteListNodeData(dataspaceName, anchorName, '/test-tree/branch', observedTimestamp)
+        when: 'delete list data method is invoked with list element json data'
+            objectUnderTest.deleteListOrListElement(dataspaceName, anchorName, '/test-tree/branch', observedTimestamp)
         then: 'the persistence service method is invoked with correct parameters'
-            1 * mockCpsDataPersistenceService.deleteListDataNodes(dataspaceName, anchorName, '/test-tree/branch')
+            1 * mockCpsDataPersistenceService.deleteListDataNode(dataspaceName, anchorName, '/test-tree/branch')
         and: 'data updated event is sent to notification service'
             1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, observedTimestamp)
     }
index 2751d55..7af1ed4 100644 (file)
@@ -140,9 +140,9 @@ class DataNodeBuilderSpec extends Specification {
         given: 'a schema context for expected model'
             def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('test-tree.yang')
             def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent) getSchemaContext()
-        and: 'parent node xpath referencing parent of list-node element'
+        and: 'parent node xpath referencing parent of list element'
             def parentNodeXpath = "/test-tree"
-        and: 'the json data fragment (list-node element) parsed into normalized node object'
+        and: 'the json data fragment (list element) parsed into normalized node object'
             def normalizedNode = YangUtils.parseJsonData(jsonData, schemaContext, parentNodeXpath)
         when: 'the normalized node is converted to a data node collection'
             def result = new DataNodeBuilder().withNormalizedNodeTree(normalizedNode)
index 8a0e0c8..4243c18 100755 (executable)
@@ -30,6 +30,7 @@ class CpsExceptionsSpec extends Specification {
     def providedMessage = 'some message'
     def providedDetails = 'some details'
     def xpath = 'some xpath'
+    def additionalInformation = 'some information'
 
     def 'Creating an exception that the Anchor already exist.'() {
         given: 'an exception dat the Anchor already exist is created'
@@ -133,6 +134,12 @@ class CpsExceptionsSpec extends Specification {
                     == "DataNode not found for anchor ${anchorName} and dataspace ${dataspaceName}."
     }
 
+    def 'Creating a exception that a datanode with a specified xpath with additional information does not exist.'() {
+        expect: 'the exception details contains the correct message with dataspace name and anchor.'
+        (new DataNodeNotFoundException(dataspaceName, anchorName, xpath, additionalInformation)).details
+                == "DataNode with xpath ${xpath} was not found for anchor ${anchorName} and dataspace ${dataspaceName}, ${additionalInformation}."
+    }
+
     def 'Creating a exception that a dataspace already exists.'() {
         expect: 'the exception details contains the correct message with dataspace name.'
             (AlreadyDefinedException.forDataspace(dataspaceName, rootCause)).details