From a15c0e5b58f16c3ab4a7c7610ac8c4a191e5e051 Mon Sep 17 00:00:00 2001 From: mpriyank Date: Fri, 28 Jan 2022 16:33:56 +0530 Subject: [PATCH] Core logic to update,add or remove cmHandle properties Issue-ID: CPS-837 Change-Id: Ia078b6a0291ae916931259a309dd592b0554da28 Signed-off-by: mpriyank --- cps-ncmp-rest/docs/openapi/components.yaml | 10 ++ .../api/impl/NetworkCmProxyDataServiceImpl.java | 44 ++---- .../NetworkCmProxyDataServicePropertyHandler.java | 171 +++++++++++++++++++++ .../api/impl/constants/DmiRegistryConstants.java | 42 +++++ ...tworkCmProxyDataServiceImplModelSyncSpec.groovy | 6 +- ...rkCmProxyDataServiceImplRegistrationSpec.groovy | 45 ++++-- .../impl/NetworkCmProxyDataServiceImplSpec.groovy | 3 +- ...orkCmProxyDataServicePropertyHandlerSpec.groovy | 136 ++++++++++++++++ .../main/java/org/onap/cps/api/CpsDataService.java | 16 +- .../org/onap/cps/api/impl/CpsDataServiceImpl.java | 14 +- 10 files changed, 435 insertions(+), 52 deletions(-) create mode 100644 cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandler.java create mode 100644 cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/constants/DmiRegistryConstants.java create mode 100644 cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandlerSpec.groovy diff --git a/cps-ncmp-rest/docs/openapi/components.yaml b/cps-ncmp-rest/docs/openapi/components.yaml index 22453f367..cda6ca3ac 100644 --- a/cps-ncmp-rest/docs/openapi/components.yaml +++ b/cps-ncmp-rest/docs/openapi/components.yaml @@ -50,6 +50,16 @@ components: $ref: '#/components/schemas/RestCmHandle' updatedCmHandles: type: array + example: + cmHandle: my-cm-handle + cmHandleProperties: + add-my-property: add-property + update-my-property: updated-property + delete-my-property: '~' + publicCmHandleProperties: + add-my-property: add-property + update-my-property: updated-property + delete-my-property: '~' items: $ref: '#/components/schemas/RestCmHandle' removedCmHandles: 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 6b6e6960a..38f8e1707 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 @@ -23,11 +23,15 @@ package org.onap.cps.ncmp.api.impl; +import static org.onap.cps.ncmp.api.impl.constants.DmiRegistryConstants.NCMP_DATASPACE_NAME; +import static org.onap.cps.ncmp.api.impl.constants.DmiRegistryConstants.NCMP_DMI_REGISTRY_ANCHOR; +import static org.onap.cps.ncmp.api.impl.constants.DmiRegistryConstants.NCMP_DMI_REGISTRY_PARENT; +import static org.onap.cps.ncmp.api.impl.constants.DmiRegistryConstants.NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME; +import static org.onap.cps.ncmp.api.impl.constants.DmiRegistryConstants.NO_TIMESTAMP; import static org.onap.cps.ncmp.api.impl.operations.DmiRequestBody.OperationEnum; import static org.onap.cps.spi.CascadeDeleteAllowed.CASCADE_DELETE_ALLOWED; import com.fasterxml.jackson.core.JsonProcessingException; -import java.time.OffsetDateTime; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -59,14 +63,6 @@ import org.springframework.stereotype.Service; @RequiredArgsConstructor public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService { - private static final String NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME = "NFP-Operational"; - - private static final String NCMP_DATASPACE_NAME = "NCMP-Admin"; - - private static final String NCMP_DMI_REGISTRY_ANCHOR = "ncmp-dmi-registry"; - - private static final OffsetDateTime NO_TIMESTAMP = null; - private final CpsDataService cpsDataService; private final JsonObjectMapper jsonObjectMapper; @@ -79,6 +75,8 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService private final CpsAdminService cpsAdminService; + private final NetworkCmProxyDataServicePropertyHandler networkCmProxyDataServicePropertyHandler; + @Override public void updateDmiRegistrationAndSyncModule(final DmiPluginRegistration dmiPluginRegistration) { dmiPluginRegistration.validateDmiPluginRegistration(); @@ -92,8 +90,11 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService if (dmiPluginRegistration.getRemovedCmHandles() != null) { parseAndRemoveCmHandlesInDmiRegistration(dmiPluginRegistration); } - } catch (final JsonProcessingException e) { - handleJsonProcessingException(dmiPluginRegistration, e); + } catch (final JsonProcessingException | DataNodeNotFoundException e) { + final String errorMessage = String.format( + "Error occurred while processing the CM-handle registration request [%s] ,caused by : [%s]", + dmiPluginRegistration, e.getMessage()); + throw new DataValidationException(errorMessage, e.getMessage(), e); } } @@ -177,11 +178,7 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService } private void parseAndUpdateCmHandlesInDmiRegistration(final DmiPluginRegistration dmiPluginRegistration) { - final PersistenceCmHandlesList updatedPersistenceCmHandlesList = - getUpdatedPersistenceCmHandlesList(dmiPluginRegistration, dmiPluginRegistration.getUpdatedCmHandles()); - final String cmHandlesAsJson = jsonObjectMapper.asJsonString(updatedPersistenceCmHandlesList); - cpsDataService.updateNodeLeavesAndExistingDescendantLeaves(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, - "/dmi-registry", cmHandlesAsJson, NO_TIMESTAMP); + networkCmProxyDataServicePropertyHandler.updateCmHandleProperties(dmiPluginRegistration.getUpdatedCmHandles()); } private PersistenceCmHandlesList getUpdatedPersistenceCmHandlesList( @@ -194,18 +191,10 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService updatedCmHandles); } - private static void handleJsonProcessingException(final DmiPluginRegistration dmiPluginRegistration, - final JsonProcessingException e) { - final String message = "Parsing error occurred while processing DMI Plugin Registration" - + dmiPluginRegistration; - log.error(message); - throw new DataValidationException(message, e.getMessage(), e); - } - private void registerAndSyncNewCmHandles(final PersistenceCmHandlesList persistenceCmHandlesList) { final String cmHandleJsonData = jsonObjectMapper.asJsonString(persistenceCmHandlesList); - cpsDataService.saveListElements(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, "/dmi-registry", - cmHandleJsonData, NO_TIMESTAMP); + cpsDataService.saveListElements(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, NCMP_DMI_REGISTRY_PARENT, + cmHandleJsonData, NO_TIMESTAMP); for (final PersistenceCmHandle persistenceCmHandle : persistenceCmHandlesList.getPersistenceCmHandles()) { syncModulesAndCreateAnchor(persistenceCmHandle); @@ -277,7 +266,4 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService cpsAdminService.createAnchor(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, persistenceCmHandle.getId(), persistenceCmHandle.getId()); } - - - } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandler.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandler.java new file mode 100644 index 000000000..359921349 --- /dev/null +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandler.java @@ -0,0 +1,171 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022 Nordix Foundation + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps.ncmp.api.impl; + +import static org.onap.cps.ncmp.api.impl.NetworkCmProxyDataServicePropertyHandler.PropertyType.DMI_PROPERTY; +import static org.onap.cps.ncmp.api.impl.NetworkCmProxyDataServicePropertyHandler.PropertyType.PUBLIC_PROPERTY; +import static org.onap.cps.ncmp.api.impl.constants.DmiRegistryConstants.NCMP_DATASPACE_NAME; +import static org.onap.cps.ncmp.api.impl.constants.DmiRegistryConstants.NCMP_DMI_REGISTRY_ANCHOR; +import static org.onap.cps.ncmp.api.impl.constants.DmiRegistryConstants.NCMP_DMI_REGISTRY_PARENT; +import static org.onap.cps.ncmp.api.impl.constants.DmiRegistryConstants.NO_TIMESTAMP; + +import com.google.common.collect.ImmutableMap; +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.onap.cps.api.CpsDataService; +import org.onap.cps.ncmp.api.models.CmHandle; +import org.onap.cps.spi.FetchDescendantsOption; +import org.onap.cps.spi.exceptions.DataNodeNotFoundException; +import org.onap.cps.spi.model.DataNode; +import org.onap.cps.spi.model.DataNodeBuilder; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +//Accepting the security hotspot as the string checked is generated from inside code and not user input. +@SuppressWarnings("squid:S5852") +public class NetworkCmProxyDataServicePropertyHandler { + + private static final String CM_HANDLE_XPATH_TEMPLATE = NCMP_DMI_REGISTRY_PARENT + "/cm-handles[@id='%s']"; + + private final CpsDataService cpsDataService; + + /** + * Iterates over incoming cmHandles and update the dataNodes based on the updated attributes. + * The attributes which are not passed will remain as is. + * + * @param cmHandles collection of cmHandles + */ + public void updateCmHandleProperties(final Collection cmHandles) throws DataNodeNotFoundException { + for (final CmHandle cmHandle : cmHandles) { + try { + final String cmHandleXpath = String.format(CM_HANDLE_XPATH_TEMPLATE, cmHandle.getCmHandleID()); + final DataNode existingCmHandleDataNode = + cpsDataService.getDataNode(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, cmHandleXpath, + FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS); + processUpdates(existingCmHandleDataNode, cmHandle); + } catch (final DataNodeNotFoundException e) { + log.error("Unable to find dataNode for cmHandleId : {} , caused by : {}", cmHandle.getCmHandleID(), + e.getMessage()); + throw e; + } + } + } + + private void processUpdates(final DataNode existingCmHandleDataNode, final CmHandle incomingCmHandle) { + if (!incomingCmHandle.getPublicProperties().isEmpty()) { + updateProperties(existingCmHandleDataNode, PUBLIC_PROPERTY, incomingCmHandle.getPublicProperties()); + } + if (!incomingCmHandle.getDmiProperties().isEmpty()) { + updateProperties(existingCmHandleDataNode, DMI_PROPERTY, incomingCmHandle.getDmiProperties()); + } + } + + private void updateProperties(final DataNode existingCmHandleDataNode, final PropertyType propertyType, + final Map incomingProperties) { + final Collection replacementPropertyDataNodes = + getReplacementDataNodes(existingCmHandleDataNode, propertyType, incomingProperties); + replacementPropertyDataNodes.addAll( + getUnchangedPropertyDataNodes(existingCmHandleDataNode, propertyType, incomingProperties)); + if (replacementPropertyDataNodes.isEmpty()) { + removeAllProperties(existingCmHandleDataNode, propertyType); + } else { + cpsDataService.replaceListContent(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, + existingCmHandleDataNode.getXpath(), replacementPropertyDataNodes, NO_TIMESTAMP); + } + } + + private void removeAllProperties(final DataNode existingCmHandleDataNode, final PropertyType propertyType) { + existingCmHandleDataNode.getChildDataNodes().forEach(dataNode -> { + final Matcher matcher = propertyType.propertyXpathPattern.matcher(dataNode.getXpath()); + if (matcher.find()) { + log.info("Deleting dataNode with xpath : [{}]", dataNode.getXpath()); + cpsDataService.deleteDataNode(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, dataNode.getXpath(), + NO_TIMESTAMP); + } + }); + } + + private Collection getUnchangedPropertyDataNodes(final DataNode existingCmHandleDataNode, + final PropertyType propertyType, final Map incomingProperties) { + final Collection unchangedPropertyDataNodes = new HashSet<>(); + for (final DataNode existingPropertyDataNode : existingCmHandleDataNode.getChildDataNodes()) { + final Matcher matcher = propertyType.propertyXpathPattern.matcher(existingPropertyDataNode.getXpath()); + if (matcher.find()) { + final String keyName = matcher.group(2); + if (!incomingProperties.containsKey(keyName)) { + unchangedPropertyDataNodes.add(existingPropertyDataNode); + } + } + } + return unchangedPropertyDataNodes; + } + + private Collection getReplacementDataNodes(final DataNode existingCmHandleDataNode, + final PropertyType propertyType, final Map incomingProperties) { + final Collection replacementPropertyDataNodes = new HashSet<>(); + incomingProperties.forEach((updatedAttributeKey, updatedAttributeValue) -> { + final String propertyXpath = getAttributeXpath(existingCmHandleDataNode, propertyType, updatedAttributeKey); + if (updatedAttributeValue != null) { + log.info("Creating a new DataNode with xpath {} , key : {} and value : {}", propertyXpath, + updatedAttributeKey, updatedAttributeValue); + replacementPropertyDataNodes.add( + buildDataNode(propertyXpath, updatedAttributeKey, updatedAttributeValue)); + } + }); + return replacementPropertyDataNodes; + } + + private String getAttributeXpath(final DataNode cmHandle, final PropertyType propertyType, + final String attributeKey) { + return cmHandle.getXpath() + "/" + propertyType.xpathPrefix + String.format("[@name='%s']", attributeKey); + } + + private DataNode buildDataNode(final String xpath, final String attributeKey, final String attributeValue) { + final Map updatedLeaves = new LinkedHashMap<>(1); + updatedLeaves.put("name", attributeKey); + updatedLeaves.put("value", attributeValue); + log.debug("Building a new node with xpath {} with leaves (name : {} , value : {})", xpath, attributeKey, + attributeValue); + return new DataNodeBuilder().withXpath(xpath).withLeaves(ImmutableMap.copyOf(updatedLeaves)).build(); + } + + enum PropertyType { + DMI_PROPERTY("additional-properties"), PUBLIC_PROPERTY("public-properties"); + + private static final String LIST_INDEX_PATTERN = "\\[@(\\w+)[^\\/]'([^']+)']"; + + final String xpathPrefix; + final Pattern propertyXpathPattern; + + PropertyType(final String xpathPrefix) { + this.xpathPrefix = xpathPrefix; + this.propertyXpathPattern = Pattern.compile(xpathPrefix + LIST_INDEX_PATTERN); + } + } +} diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/constants/DmiRegistryConstants.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/constants/DmiRegistryConstants.java new file mode 100644 index 000000000..c29c725d7 --- /dev/null +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/constants/DmiRegistryConstants.java @@ -0,0 +1,42 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022 Nordix Foundation + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps.ncmp.api.impl.constants; + +import java.time.OffsetDateTime; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +/** + * DmiRegistryConstants class to be strictly used for DMI Related constants only. + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class DmiRegistryConstants { + + public static final String NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME = "NFP-Operational"; + + public static final String NCMP_DATASPACE_NAME = "NCMP-Admin"; + + public static final String NCMP_DMI_REGISTRY_ANCHOR = "ncmp-dmi-registry"; + + public static final String NCMP_DMI_REGISTRY_PARENT = "/dmi-registry"; + + public static final OffsetDateTime NO_TIMESTAMP = null; +} diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplModelSyncSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplModelSyncSpec.groovy index 67108a5fe..3af4fc00e 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplModelSyncSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplModelSyncSpec.groovy @@ -32,14 +32,16 @@ import spock.lang.Specification class NetworkCmProxyDataServiceImplModelSyncSpec extends Specification { + def nullCpsDataService = null def mockJsonObjectMapper = Mock(JsonObjectMapper) def mockCpsModuleService = Mock(CpsModuleService) def mockCpsAdminService = Mock(CpsAdminService) def mockDmiModelOperations = Mock(DmiModelOperations) def mockDmiDataOperations = Mock(DmiDataOperations) + def nullNetworkCmProxyDataServicePropertyHandler = null - def objectUnderTest = new NetworkCmProxyDataServiceImpl(null, mockJsonObjectMapper, mockDmiDataOperations, mockDmiModelOperations, - mockCpsModuleService, mockCpsAdminService) + def objectUnderTest = new NetworkCmProxyDataServiceImpl(nullCpsDataService, mockJsonObjectMapper, mockDmiDataOperations, mockDmiModelOperations, + mockCpsModuleService, mockCpsAdminService, nullNetworkCmProxyDataServicePropertyHandler) def expectedDataspaceName = 'NFP-Operational' diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplRegistrationSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplRegistrationSpec.groovy index 9f1203d64..00fda149f 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplRegistrationSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplRegistrationSpec.groovy @@ -26,7 +26,6 @@ import org.onap.cps.api.CpsAdminService import org.onap.cps.api.CpsDataService import org.onap.cps.api.CpsModuleService import org.onap.cps.ncmp.api.impl.exception.DmiRequestException -import org.onap.cps.ncmp.api.impl.exception.NcmpException import org.onap.cps.ncmp.api.impl.operations.DmiDataOperations import org.onap.cps.ncmp.api.impl.operations.DmiModelOperations import org.onap.cps.ncmp.api.models.CmHandle @@ -53,16 +52,17 @@ class NetworkCmProxyDataServiceImplRegistrationSpec extends Specification { def mockCpsAdminService = Mock(CpsAdminService) def mockDmiModelOperations = Mock(DmiModelOperations) def mockDmiDataOperations = Mock(DmiDataOperations) + def mockNetworkCmProxyDataServicePropertyHandler = Mock(NetworkCmProxyDataServicePropertyHandler) def noTimestamp = null def 'Register or re-register a DMI Plugin for the given cm-handle(s) with #scenario process.'() { given: 'a registration' def objectUnderTest = getObjectUnderTestWithModelSyncDisabled() - def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin:'my-server') + def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server') persistenceCmHandle.cmHandleID = '123' persistenceCmHandle.dmiProperties = [dmiProp1: 'dmiValue1', dmiProp2: 'dmiValue2'] - persistenceCmHandle.publicProperties = [publicProp1: 'publicValue1', publicProp2: 'publicValue2' ] + persistenceCmHandle.publicProperties = [publicProp1: 'publicValue1', publicProp2: 'publicValue2'] dmiPluginRegistration.createdCmHandles = createdCmHandles dmiPluginRegistration.updatedCmHandles = updatedCmHandles dmiPluginRegistration.removedCmHandles = removedCmHandles @@ -74,22 +74,21 @@ class NetworkCmProxyDataServiceImplRegistrationSpec extends Specification { objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration) then: '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' - expectedCallsToUpdateNode * mockCpsDataService.updateNodeLeavesAndExistingDescendantLeaves('NCMP-Admin', - 'ncmp-dmi-registry', '/dmi-registry', expectedJsonData, noTimestamp) + '/dmi-registry', expectedJsonData, noTimestamp) + and: 'update data node leaves is called with correct parameters' + expectedCallsToPropertyHandler * mockNetworkCmProxyDataServicePropertyHandler.updateCmHandleProperties(updatedCmHandles) and: 'delete schema set is invoked with the correct parameters' expectedCallsToDeleteSchemaSetAndListElement * mockCpsModuleService.deleteSchemaSet('NFP-Operational', 'cmHandle001', CASCADE_DELETE_ALLOWED) and: 'delete list or list element is invoked with the correct parameters' expectedCallsToDeleteSchemaSetAndListElement * mockCpsDataService.deleteListOrListElement('NCMP-Admin', - 'ncmp-dmi-registry', "/dmi-registry/cm-handles[@id='cmHandle001']", noTimestamp) + 'ncmp-dmi-registry', "/dmi-registry/cm-handles[@id='cmHandle001']", noTimestamp) where: - scenario | createdCmHandles | updatedCmHandles | removedCmHandles || expectedCallsToSaveNode | expectedCallsToUpdateNode | expectedCallsToDeleteSchemaSetAndListElement - 'create' | [persistenceCmHandle] | [] | [] || 1 | 0 | 0 - 'update' | [] | [persistenceCmHandle] | [] || 0 | 1 | 0 - 'delete' | [] | [] | cmHandlesArray || 0 | 0 | 1 - 'create, update and delete' | [persistenceCmHandle] | [persistenceCmHandle] | cmHandlesArray || 1 | 1 | 1 - 'no valid data' | null | null | null || 0 | 0 | 0 + scenario | createdCmHandles | updatedCmHandles | removedCmHandles || expectedCallsToSaveNode | expectedCallsToDeleteSchemaSetAndListElement | expectedCallsToPropertyHandler + 'create' | [persistenceCmHandle] | [] | [] || 1 | 0 | 1 + 'update' | [] | [persistenceCmHandle] | [] || 0 | 0 | 1 + 'delete' | [] | [] | cmHandlesArray || 0 | 1 | 1 + 'create, update and delete' | [persistenceCmHandle] | [persistenceCmHandle] | cmHandlesArray || 1 | 1 | 1 + 'no valid data' | null | null | null || 0 | 0 | 0 } def 'Register a DMI Plugin for the given cm-handle(s) without DMI properties.'() { @@ -194,9 +193,25 @@ class NetworkCmProxyDataServiceImplRegistrationSpec extends Specification { 'only data DMI plugin' | '' | '' | 'service1' || 'Cannot register just a Data or Model plugin service name' } + def 'Exception thrown on CM-Handle registration update request'() { + given: 'a CM-handle registration' + def objectUnderTest = getObjectUnderTestWithModelSyncDisabled() + and: 'dmi plugin registration input update request' + def dmiPluginReg = new DmiPluginRegistration(); + dmiPluginReg.dmiPlugin = 'onap.dmap.plugin'; + dmiPluginReg.updatedCmHandles = [new CmHandle(cmHandleID: 'unknownHandle')] + and: 'update data node leaves is unable to find data node' + mockNetworkCmProxyDataServicePropertyHandler.updateCmHandleProperties(*_) >> { throw new DataNodeNotFoundException('NCMP-Admin', 'ncmp-dmi-registry') } + when: 'update dmi registration is called' + objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginReg) + then: 'data validation exception is thrown' + def exceptionThrown = thrown(DataValidationException.class) + assert exceptionThrown.getDetails().contains('DataNode not found') + } + def getObjectUnderTestWithModelSyncDisabled() { def objectUnderTest = Spy(new NetworkCmProxyDataServiceImpl(mockCpsDataService, spiedJsonObjectMapper, mockDmiDataOperations, mockDmiModelOperations, - mockCpsModuleService, mockCpsAdminService)) + mockCpsModuleService, mockCpsAdminService, mockNetworkCmProxyDataServicePropertyHandler)) objectUnderTest.syncModulesAndCreateAnchor(*_) >> null return objectUnderTest } 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 9c79d4fcf..6d7bdefb8 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 @@ -51,9 +51,10 @@ class NetworkCmProxyDataServiceImplSpec extends Specification { def spiedJsonObjectMapper = Spy(new JsonObjectMapper(new ObjectMapper())) def mockDmiModelOperations = Mock(DmiModelOperations) def mockDmiDataOperations = Mock(DmiDataOperations) + def nullNetworkCmProxyDataServicePropertyHandler = null def objectUnderTest = new NetworkCmProxyDataServiceImpl(mockCpsDataService, spiedJsonObjectMapper, mockDmiDataOperations, mockDmiModelOperations, - mockCpsModuleService, mockCpsAdminService) + mockCpsModuleService, mockCpsAdminService, nullNetworkCmProxyDataServicePropertyHandler) def cmHandleXPath = "/dmi-registry/cm-handles[@id='testCmHandle']" diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandlerSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandlerSpec.groovy new file mode 100644 index 000000000..5bdb744b2 --- /dev/null +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandlerSpec.groovy @@ -0,0 +1,136 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022 Nordix Foundation + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps.ncmp.api.impl + +import org.onap.cps.api.CpsDataService +import org.onap.cps.ncmp.api.models.CmHandle +import org.onap.cps.spi.FetchDescendantsOption +import org.onap.cps.spi.exceptions.DataNodeNotFoundException +import org.onap.cps.spi.exceptions.DataValidationException +import org.onap.cps.spi.model.DataNode +import org.onap.cps.spi.model.DataNodeBuilder +import spock.lang.Specification + +class NetworkCmProxyDataServicePropertyHandlerSpec extends Specification { + + def mockCpsDataService = Mock(CpsDataService) + + def objectUnderTest = new NetworkCmProxyDataServicePropertyHandler(mockCpsDataService) + def dataspaceName = 'NCMP-Admin' + def anchorName = 'ncmp-dmi-registry' + def static cmHandleId = 'myHandle1' + def static cmHandleXpath = "/dmi-registry/cm-handles[@id='${cmHandleId}']" + def noTimeStamp = null + + def static propertyDataNodes = [new DataNodeBuilder().withXpath("/dmi-registry/cm-handles[@id='${cmHandleId}']/additional-properties[@name='additionalProp1']").withLeaves(['name': 'additionalProp1', 'value': 'additionalValue1']).build(), + new DataNodeBuilder().withXpath("/dmi-registry/cm-handles[@id='${cmHandleId}']/additional-properties[@name='additionalProp2']").withLeaves(['name': 'additionalProp2', 'value': 'additionalValue2']).build(), + new DataNodeBuilder().withXpath("/dmi-registry/cm-handles[@id='${cmHandleId}']/public-properties[@name='publicProp3']").withLeaves(['name': 'publicProp3', 'value': 'publicValue3']).build(), + new DataNodeBuilder().withXpath("/dmi-registry/cm-handles[@id='${cmHandleId}']/public-properties[@name='publicProp4']").withLeaves(['name': 'publicProp4', 'value': 'publicValue4']).build()] + def static cmHandleDataNode = new DataNode(xpath: cmHandleXpath, childDataNodes: propertyDataNodes) + + def 'Update CM Handle Public Properties: #scenario'() { + given: 'the CPS service return a CM handle' + mockCpsDataService.getDataNode(dataspaceName, anchorName, cmHandleXpath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> cmHandleDataNode + and: 'an update cm handle request with public properties updates' + def cmHandleUpdateRequest = [new CmHandle(cmHandleID: cmHandleId, publicProperties: updatedPublicProperties)] + when: 'update data node leaves is called with the update request' + objectUnderTest.updateCmHandleProperties(cmHandleUpdateRequest) + then: 'the replace list method is called with correct params' + 1 * mockCpsDataService.replaceListContent(dataspaceName, anchorName, cmHandleXpath, _, noTimeStamp) >> { args -> + { + assert args[3].leaves.size() == expectedPropertiesAfterUpdate.size() + assert args[3].leaves.containsAll(convertToProperties(expectedPropertiesAfterUpdate)) + } + } + where: 'following public properties updates are made' + scenario | updatedPublicProperties || expectedPropertiesAfterUpdate + 'property added' | ['newPubProp1': 'pub-val'] || [['publicProp3': 'publicValue3'], ['publicProp4': 'publicValue4'], ['newPubProp1': 'pub-val']] + 'property updated' | ['publicProp4': 'newPubVal'] || [['publicProp3': 'publicValue3'], ['publicProp4': 'newPubVal']] + 'property removed' | ['publicProp4': null] || [['publicProp3': 'publicValue3']] + 'property ignored(value is null)' | ['pub-prop': null] || [['publicProp3': 'publicValue3'], ['publicProp4': 'publicValue4']] + } + + def 'Update DMI Properties: #scenario'() { + given: 'the CPS service return a CM handle' + mockCpsDataService.getDataNode(dataspaceName, anchorName, cmHandleXpath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> cmHandleDataNode + and: 'an update cm handle request with DMI properties updates' + def cmHandleUpdateRequest = [new CmHandle(cmHandleID: cmHandleId, dmiProperties: updatedDmiProperties)] + when: 'update data node leaves is called with the update request' + objectUnderTest.updateCmHandleProperties(cmHandleUpdateRequest) + then: 'replace list method should is called with correct params' + expectedCallsToReplaceMethod * mockCpsDataService.replaceListContent(dataspaceName, anchorName, cmHandleXpath, _, noTimeStamp) >> { args -> + { + assert args[3].leaves.size() == expectedPropertiesAfterUpdate.size() + assert args[3].leaves.containsAll(convertToProperties(expectedPropertiesAfterUpdate)) + } + } + where: 'following DMI properties updates are made' + scenario | updatedDmiProperties || expectedPropertiesAfterUpdate | expectedCallsToReplaceMethod + 'property added' | ['newAdditionalProp1': 'add-value'] || [['additionalProp1': 'additionalValue1'], ['additionalProp2': 'additionalValue2'], ['newAdditionalProp1': 'add-value']] | 1 + 'property updated' | ['additionalProp1': 'newValue'] || [['additionalProp2': 'additionalValue2'], ['additionalProp1': 'newValue']] | 1 + 'property removed' | ['additionalProp1': null] || [['additionalProp2': 'additionalValue2']] | 1 + 'property ignored(value is null)' | ['new-prop': null] || [['additionalProp1': 'additionalValue1'], ['additionalProp2': 'additionalValue2']] | 1 + 'no property changes' | [:] || [['additionalProp1': 'additionalValue1'], ['additionalProp2': 'additionalValue2']] | 0 + } + + def 'Update CM Handle Properties, remove all properties: #scenario'() { + given: 'the CPS service return a CM handle' + def cmHandleDataNode = new DataNode(xpath: cmHandleXpath, childDataNodes: originalPropertyDataNodes) + mockCpsDataService.getDataNode(dataspaceName, anchorName, cmHandleXpath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> cmHandleDataNode + and: 'an update cm handle request that removes all public properties(existing and non-existing)' + def cmHandleUpdateRequest = [new CmHandle(cmHandleID: cmHandleId, publicProperties: ['publicProp3': null, 'publicProp4': null])] + when: 'update data node leaves is called with the update request' + objectUnderTest.updateCmHandleProperties(cmHandleUpdateRequest) + then: 'the replace list method is not called' + 0 * mockCpsDataService.replaceListContent(*_) + then: 'delete data node will be called for any existing property' + expectedCallsToDeleteDataNode * mockCpsDataService.deleteDataNode(dataspaceName, anchorName, _, noTimeStamp) >> { arg -> + { + assert arg[2].contains("@name='publicProp") + } + } + where: 'following public properties updates are made' + scenario | originalPropertyDataNodes || expectedCallsToDeleteDataNode + '2 original properties, both removed' | propertyDataNodes || 2 + 'no original properties' | [] || 0 + } + + def 'Exception thrown when we try to update cmHandle'() { + given: 'cm handles request' + def cmHandleUpdateRequest = [new CmHandle(cmHandleID: cmHandleId, publicProperties: [:], dmiProperties: [:])] + and: 'data node cannot be found' + mockCpsDataService.getDataNode(*_) >> { throw new DataNodeNotFoundException(dataspaceName, anchorName, cmHandleXpath) } + when: 'update data node leaves is called using correct parameters' + objectUnderTest.updateCmHandleProperties(cmHandleUpdateRequest) + then: 'data validation exception is thrown' + def exceptionThrown = thrown(DataValidationException.class) + assert exceptionThrown.getMessage().contains('DataNode not found') + } + + def convertToProperties(expectedPropertiesAfterUpdateAsMap) { + def properties = [].withDefault { [:] } + expectedPropertiesAfterUpdateAsMap.forEach(property -> + property.forEach((key, val) -> { + properties.add(['name': key, 'value': val]) + })) + return properties + } +} 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 d2482d50a..cdd417bd8 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 @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2020 Nordix Foundation + * Copyright (C) 2020-2022 Nordix Foundation * Modifications Copyright (C) 2021 Pantheon.tech * Modifications Copyright (C) 2021-2022 Bell Canada * ================================================================================ @@ -23,6 +23,7 @@ package org.onap.cps.api; import java.time.OffsetDateTime; +import java.util.Collection; import org.onap.cps.spi.FetchDescendantsOption; import org.onap.cps.spi.model.DataNode; @@ -116,6 +117,19 @@ public interface CpsDataService { void replaceListContent(String dataspaceName, String anchorName, String parentNodeXpath, String jsonData, OffsetDateTime observedTimestamp); + /** + * Replaces list content by removing all existing elements and inserting the given new elements as data nodes + * under given parent, anchor and dataspace. + * + * @param dataspaceName dataspace-name + * @param anchorName anchor name + * @param parentNodeXpath parent node xpath + * @param dataNodes datanodes representing the updated data + * @param observedTimestamp observedTimestamp + */ + void replaceListContent(String dataspaceName, String anchorName, String parentNodeXpath, + Collection dataNodes, OffsetDateTime observedTimestamp); + /** * Deletes data node for given anchor and dataspace. * 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 af06e5fc1..aae355d50 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 @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2021 Nordix Foundation + * Copyright (C) 2021-2022 Nordix Foundation * Modifications Copyright (C) 2020-2022 Bell Canada. * Modifications Copyright (C) 2021 Pantheon.tech * ================================================================================ @@ -118,10 +118,16 @@ public class CpsDataServiceImpl implements CpsDataService { @Override public void replaceListContent(final String dataspaceName, final String anchorName, final String parentNodeXpath, - final String jsonData, final OffsetDateTime observedTimestamp) { + final String jsonData, final OffsetDateTime observedTimestamp) { final Collection newListElements = - buildDataNodes(dataspaceName, anchorName, parentNodeXpath, jsonData); - cpsDataPersistenceService.replaceListContent(dataspaceName, anchorName, parentNodeXpath, newListElements); + buildDataNodes(dataspaceName, anchorName, parentNodeXpath, jsonData); + replaceListContent(dataspaceName, anchorName, parentNodeXpath, newListElements, observedTimestamp); + } + + @Override + public void replaceListContent(final String dataspaceName, final String anchorName, final String parentNodeXpath, + final Collection dataNodes, final OffsetDateTime observedTimestamp) { + cpsDataPersistenceService.replaceListContent(dataspaceName, anchorName, parentNodeXpath, dataNodes); processDataUpdatedEventAsync(dataspaceName, anchorName, observedTimestamp, parentNodeXpath, Operation.UPDATE); } -- 2.16.6