From a79c9f1bdf335843c29a425da53c15b5e353e5a3 Mon Sep 17 00:00:00 2001 From: DylanB95EST Date: Fri, 29 Oct 2021 17:33:06 +0100 Subject: Clean Up Code around List Nodes 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 --- .../api/impl/NetworkCmProxyDataServiceImpl.java | 6 +- .../impl/NetworkCmProxyDataServiceImplSpec.groovy | 20 +- cps-rest/docs/openapi/cpsData.yml | 22 +-- cps-rest/docs/openapi/openapi.yml | 2 +- .../cps/rest/controller/DataRestController.java | 16 +- .../rest/controller/DataRestControllerSpec.groovy | 45 ++--- .../spi/impl/CpsDataPersistenceServiceImpl.java | 170 ++++++++++------- ...CpsDataPersistenceServiceIntegrationSpec.groovy | 209 ++++++++++++--------- cps-ri/src/test/resources/data/fragment.sql | 7 +- .../main/java/org/onap/cps/api/CpsDataService.java | 34 ++-- .../org/onap/cps/api/impl/CpsDataServiceImpl.java | 47 ++--- .../onap/cps/spi/CpsDataPersistenceService.java | 41 ++-- .../spi/exceptions/DataNodeNotFoundException.java | 15 ++ .../main/java/org/onap/cps/utils/DataMapUtils.java | 6 +- .../cps/api/impl/CpsDataServiceImplSpec.groovy | 39 ++-- .../org/onap/cps/model/DataNodeBuilderSpec.groovy | 4 +- .../cps/spi/exceptions/CpsExceptionsSpec.groovy | 7 + 17 files changed, 389 insertions(+), 301 deletions(-) diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImpl.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImpl.java index 11d1a89652..7020c74210 100755 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImpl.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImpl.java @@ -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()); diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplSpec.groovy index 1bad8ce0ba..ae252b870e 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplSpec.groovy @@ -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' diff --git a/cps-rest/docs/openapi/cpsData.yml b/cps-rest/docs/openapi/cpsData.yml index d456f44e6c..ca21df53db 100644 --- a/cps-rest/docs/openapi/cpsData.yml +++ b/cps-rest/docs/openapi/cpsData.yml @@ -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' diff --git a/cps-rest/docs/openapi/openapi.yml b/cps-rest/docs/openapi/openapi.yml index f9881fb151..76bdb80c23 100644 --- a/cps-rest/docs/openapi/openapi.yml +++ b/cps-rest/docs/openapi/openapi.yml @@ -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' diff --git a/cps-rest/src/main/java/org/onap/cps/rest/controller/DataRestController.java b/cps-rest/src/main/java/org/onap/cps/rest/controller/DataRestController.java index 7db4e5a1b6..f29ead9e9f 100755 --- a/cps-rest/src/main/java/org/onap/cps/rest/controller/DataRestController.java +++ b/cps-rest/src/main/java/org/onap/cps/rest/controller/DataRestController.java @@ -60,9 +60,9 @@ public class DataRestController implements CpsDataApi { } @Override - public ResponseEntity addListNodeElements(final String parentNodeXpath, + public ResponseEntity 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 replaceListNodeElements(final String parentNodeXpath, + public ResponseEntity 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 deleteListNodeElements(final String dataspaceName, final String anchorName, - final String listNodeXpath, final String observedTimestamp) { + public ResponseEntity 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); diff --git a/cps-rest/src/test/groovy/org/onap/cps/rest/controller/DataRestControllerSpec.groovy b/cps-rest/src/test/groovy/org/onap/cps/rest/controller/DataRestControllerSpec.groovy index a54f3bc95d..06f2f5795f 100755 --- a/cps-rest/src/test/groovy/org/onap/cps/rest/controller/DataRestControllerSpec.groovy +++ b/cps-rest/src/test/groovy/org/onap/cps/rest/controller/DataRestControllerSpec.groovy @@ -22,14 +22,6 @@ 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 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 2397d31792..8dc6c2f69c 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 @@ -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 dataNodes) { final FragmentEntity parentFragment = getFragmentByXpath(dataspaceName, anchorName, parentNodeXpath); final List 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 existingChildrenByXpath = existingFragmentEntity.getChildFragments() .stream().collect(Collectors.toMap(FragmentEntity::getXpath, childFragmentEntity -> childFragmentEntity)); final Collection 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 replacementDataNodes) { + public void replaceListContent(final String dataspaceName, final String anchorName, final String parentNodeXpath, + final Collection newListElements) { final FragmentEntity parentEntity = getFragmentByXpath(dataspaceName, anchorName, parentNodeXpath); - final String listNodeXpathPrefix = getListNodeXpathPrefix(replacementDataNodes); + final String listElementXpathPrefix = getListElementXpathPrefix(newListElements); final Map existingListElementFragmentEntitiesByXPath = - extractListElementFragmentEntitiesByXPath(parentEntity.getChildFragments(), listNodeXpathPrefix); - removeExistingListElements(parentEntity.getChildFragments(), existingListElementFragmentEntitiesByXPath); + extractListElementFragmentEntitiesByXPath(parentEntity.getChildFragments(), listElementXpathPrefix); + deleteListElements(parentEntity.getChildFragments(), existingListElementFragmentEntitiesByXPath); final Set 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 fragmentEntities, final Map existingListElementFragmentEntitiesByXPath) { fragmentEntities.removeAll(existingListElementFragmentEntitiesByXPath.values()); } - private static String getListNodeXpathPrefix(final Collection replacementDataNodes) { - final String firstChildNodeXpath = replacementDataNodes.iterator().next().getXpath(); + private static String getListElementXpathPrefix(final Collection 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 existingListNodeElementsByXpath) { - return !existingListNodeElementsByXpath.containsKey(replacementDataNode.getXpath()); + final Map 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 extractListElementFragmentEntitiesByXPath( - final Set childEntities, final String listNodeXpathPrefix) { + final Set 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); } diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceIntegrationSpec.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceIntegrationSpec.groovy index 144b18b535..85e1155cff 100755 --- a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceIntegrationSpec.groovy +++ b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceIntegrationSpec.groovy @@ -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 buildDataNodeCollection(xpaths) { + static Collection toDataNodes(xpaths) { return xpaths.collect { new DataNodeBuilder().withXpath(it).build() } } diff --git a/cps-ri/src/test/resources/data/fragment.sql b/cps-ri/src/test/resources/data/fragment.sql index f6e887e017..5d3a328327 100755 --- a/cps-ri/src/test/resources/data/fragment.sql +++ b/cps-ri/src/test/resources/data/fragment.sql @@ -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 diff --git a/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java b/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java index 31a7517340..e6cb65fa78 100644 --- a/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java +++ b/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java @@ -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 diff --git a/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java b/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java index 7b3567ed3c..44a17f89dd 100755 --- a/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java +++ b/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java @@ -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 dataNodesCollection = - buildDataNodeCollectionFromJson(dataspaceName, anchorName, parentNodeXpath, jsonData); - cpsDataPersistenceService.addListDataNodes(dataspaceName, anchorName, parentNodeXpath, dataNodesCollection); + final Collection 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 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 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 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 buildDataNodeCollectionFromJson(final String dataspaceName, final String anchorName, - final String parentNodeXpath, final String jsonData) { + private Collection 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; diff --git a/cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java b/cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java index bf8dd1a073..b8c472f277 100644 --- a/cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java +++ b/cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java @@ -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 dataNodes); + void addListElements(@NonNull String dataspaceName, @NonNull String anchorName, @NonNull String parentNodeXpath, + @NonNull Collection 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 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 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. diff --git a/cps-service/src/main/java/org/onap/cps/spi/exceptions/DataNodeNotFoundException.java b/cps-service/src/main/java/org/onap/cps/spi/exceptions/DataNodeNotFoundException.java index 848d5692c1..b717a2b183 100755 --- a/cps-service/src/main/java/org/onap/cps/spi/exceptions/DataNodeNotFoundException.java +++ b/cps-service/src/main/java/org/onap/cps/spi/exceptions/DataNodeNotFoundException.java @@ -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. * diff --git a/cps-service/src/main/java/org/onap/cps/utils/DataMapUtils.java b/cps-service/src/main/java/org/onap/cps/utils/DataMapUtils.java index 3ee6afb719..71a95f1ca0 100644 --- a/cps-service/src/main/java/org/onap/cps/utils/DataMapUtils.java +++ b/cps-service/src/main/java/org/onap/cps/utils/DataMapUtils.java @@ -58,7 +58,7 @@ public class DataMapUtils { return ImmutableMap.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("]"); } } diff --git a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy index 6a0a4649a6..2bd44824a6 100644 --- a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy @@ -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) } diff --git a/cps-service/src/test/groovy/org/onap/cps/model/DataNodeBuilderSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/model/DataNodeBuilderSpec.groovy index 2751d55075..7af1ed41c7 100644 --- a/cps-service/src/test/groovy/org/onap/cps/model/DataNodeBuilderSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/model/DataNodeBuilderSpec.groovy @@ -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) diff --git a/cps-service/src/test/groovy/org/onap/cps/spi/exceptions/CpsExceptionsSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/spi/exceptions/CpsExceptionsSpec.groovy index 8a0e0c82e7..4243c18c24 100755 --- a/cps-service/src/test/groovy/org/onap/cps/spi/exceptions/CpsExceptionsSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/spi/exceptions/CpsExceptionsSpec.groovy @@ -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 -- cgit 1.2.3-korg