Core logic to update,add or remove cmHandle properties 16/126916/23
authormpriyank <priyank.maheshwari@est.tech>
Fri, 28 Jan 2022 11:03:56 +0000 (16:33 +0530)
committerRenu Kumari <renu.kumari@bell.ca>
Wed, 16 Feb 2022 13:31:29 +0000 (13:31 +0000)
Issue-ID: CPS-837
Change-Id: Ia078b6a0291ae916931259a309dd592b0554da28
Signed-off-by: mpriyank <priyank.maheshwari@est.tech>
cps-ncmp-rest/docs/openapi/components.yaml
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImpl.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandler.java [new file with mode: 0644]
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/constants/DmiRegistryConstants.java [new file with mode: 0644]
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplModelSyncSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplRegistrationSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandlerSpec.groovy [new file with mode: 0644]
cps-service/src/main/java/org/onap/cps/api/CpsDataService.java
cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java

index 22453f3..cda6ca3 100644 (file)
@@ -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:
index 6b6e696..38f8e17 100755 (executable)
 
 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 (file)
index 0000000..3599213
--- /dev/null
@@ -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<CmHandle> 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<String, String> incomingProperties) {
+        final Collection<DataNode> 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<DataNode> getUnchangedPropertyDataNodes(final DataNode existingCmHandleDataNode,
+            final PropertyType propertyType, final Map<String, String> incomingProperties) {
+        final Collection<DataNode> 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<DataNode> getReplacementDataNodes(final DataNode existingCmHandleDataNode,
+            final PropertyType propertyType, final Map<String, String> incomingProperties) {
+        final Collection<DataNode> 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<String, String> 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 (file)
index 0000000..c29c725
--- /dev/null
@@ -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;
+}
index 67108a5..3af4fc0 100644 (file)
@@ -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'
 
index 9f1203d..00fda14 100644 (file)
@@ -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
     }
index 9c79d4f..6d7bdef 100644 (file)
@@ -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 (file)
index 0000000..5bdb744
--- /dev/null
@@ -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
+    }
+}
index d2482d5..cdd417b 100644 (file)
@@ -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<DataNode> dataNodes, OffsetDateTime observedTimestamp);
+
     /**
      * Deletes data node for given anchor and dataspace.
      *
index af06e5f..aae355d 100755 (executable)
@@ -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<DataNode> 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<DataNode> dataNodes, final OffsetDateTime observedTimestamp) {
+        cpsDataPersistenceService.replaceListContent(dataspaceName, anchorName, parentNodeXpath, dataNodes);
         processDataUpdatedEventAsync(dataspaceName, anchorName, observedTimestamp, parentNodeXpath, Operation.UPDATE);
     }