Use DB for checking Alternate IDs 11/137211/8
authorhalil.cakal <halil.cakal@est.tech>
Mon, 12 Feb 2024 11:41:00 +0000 (11:41 +0000)
committerhalil.cakal <halil.cakal@est.tech>
Fri, 16 Feb 2024 10:10:27 +0000 (10:10 +0000)
- do NOT use hazelcast cache
- introduced AlternateIcChecker
- use checks during Inital registration

- TODO: remvoe Hazalcast Cache (once perf test are OK)
- TODO: use checks during UPDATE registration
- TODO: error reporting during registration

Issue-Id: CPS-2049

Change-Id: I0adcac52a7a49e26301758eafc4684152ddfcaf5
Signed-off-by: halil.cakal <halil.cakal@est.tech>
12 files changed:
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
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/inventory/InventoryPersistence.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/inventory/InventoryPersistenceImpl.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/AlternateIdChecker.java [new file with mode: 0644]
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/CmHandleIdMapper.java [deleted file]
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
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/inventory/InventoryPersistenceImplSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/AlternateIdCheckerSpec.groovy [new file with mode: 0644]
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/CmHandleIdMapperSpec.groovy [deleted file]

index 05b83b9..7622e7c 100755 (executable)
@@ -48,7 +48,6 @@ import java.util.Set;
 import java.util.stream.Collectors;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
-import org.apache.commons.lang3.StringUtils;
 import org.onap.cps.api.CpsDataService;
 import org.onap.cps.ncmp.api.NcmpResponseStatus;
 import org.onap.cps.ncmp.api.NetworkCmProxyCmHandleQueryService;
@@ -66,7 +65,7 @@ import org.onap.cps.ncmp.api.impl.operations.DmiDataOperations;
 import org.onap.cps.ncmp.api.impl.operations.OperationType;
 import org.onap.cps.ncmp.api.impl.trustlevel.TrustLevel;
 import org.onap.cps.ncmp.api.impl.trustlevel.TrustLevelManager;
-import org.onap.cps.ncmp.api.impl.utils.CmHandleIdMapper;
+import org.onap.cps.ncmp.api.impl.utils.AlternateIdChecker;
 import org.onap.cps.ncmp.api.impl.utils.CmHandleQueryConditions;
 import org.onap.cps.ncmp.api.impl.utils.InventoryQueryConditions;
 import org.onap.cps.ncmp.api.impl.utils.YangDataConverter;
@@ -106,37 +105,25 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService
     private final IMap<String, Object> moduleSyncStartedOnCmHandles;
     private final Map<String, TrustLevel> trustLevelPerDmiPlugin;
     private final TrustLevelManager trustLevelManager;
-    private final CmHandleIdMapper cmHandleIdMapper;
+    private final AlternateIdChecker alternateIdChecker;
     private final Map<String, Collection<ModuleReference>> moduleSetTagCache;
 
     @Override
     public DmiPluginRegistrationResponse updateDmiRegistrationAndSyncModule(
         final DmiPluginRegistration dmiPluginRegistration) {
-        cacheAlternateIds(dmiPluginRegistration.getCreatedCmHandles());
+
         dmiPluginRegistration.validateDmiPluginRegistration();
         final DmiPluginRegistrationResponse dmiPluginRegistrationResponse = new DmiPluginRegistrationResponse();
 
         setTrustLevelPerDmiPlugin(dmiPluginRegistration);
 
-        if (!dmiPluginRegistration.getRemovedCmHandles().isEmpty()) {
-            dmiPluginRegistrationResponse.setRemovedCmHandles(
-                parseAndProcessDeletedCmHandlesInRegistration(dmiPluginRegistration.getRemovedCmHandles()));
-        }
+        processRemovedCmHandles(dmiPluginRegistration, dmiPluginRegistrationResponse);
 
-        if (!dmiPluginRegistration.getCreatedCmHandles().isEmpty()) {
-            dmiPluginRegistrationResponse.setCreatedCmHandles(
-                parseAndProcessCreatedCmHandlesInRegistration(dmiPluginRegistration));
-        }
-        if (!dmiPluginRegistration.getUpdatedCmHandles().isEmpty()) {
-            dmiPluginRegistrationResponse.setUpdatedCmHandles(
-                networkCmProxyDataServicePropertyHandler
-                    .updateCmHandleProperties(dmiPluginRegistration.getUpdatedCmHandles()));
-        }
-        if (dmiPluginRegistration.getUpgradedCmHandles() != null
-            && !dmiPluginRegistration.getUpgradedCmHandles().getCmHandles().isEmpty()) {
-            dmiPluginRegistrationResponse.setUpgradedCmHandles(
-                parseAndProcessUpgradedCmHandlesInRegistration(dmiPluginRegistration));
-        }
+        processCreatedCmHandles(dmiPluginRegistration, dmiPluginRegistrationResponse);
+
+        processUpdatedCmHandles(dmiPluginRegistration, dmiPluginRegistrationResponse);
+
+        processUpgradedCmHandles(dmiPluginRegistration, dmiPluginRegistrationResponse);
 
         return dmiPluginRegistrationResponse;
     }
@@ -329,21 +316,24 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService
      * @return cm-handle registration response for create cm-handle requests.
      */
     public List<CmHandleRegistrationResponse> parseAndProcessCreatedCmHandlesInRegistration(
-        final DmiPluginRegistration dmiPluginRegistration) {
+        final DmiPluginRegistration dmiPluginRegistration, final Collection<String> acceptedCmHandleIds) {
         final List<NcmpServiceCmHandle> cmHandlesToBeCreated = dmiPluginRegistration.getCreatedCmHandles();
         final Map<String, TrustLevel> initialTrustLevelPerCmHandleId = new HashMap<>(cmHandlesToBeCreated.size());
         final List<YangModelCmHandle> yangModelCmHandles = new ArrayList<>(cmHandlesToBeCreated.size());
         cmHandlesToBeCreated
                 .forEach(cmHandle -> {
-                    final YangModelCmHandle yangModelCmHandle = YangModelCmHandle.toYangModelCmHandle(
-                            dmiPluginRegistration.getDmiPlugin(),
-                            dmiPluginRegistration.getDmiDataPlugin(),
-                            dmiPluginRegistration.getDmiModelPlugin(),
-                            cmHandle,
-                            cmHandle.getModuleSetTag(),
-                            cmHandle.getAlternateId());
-                    yangModelCmHandles.add(yangModelCmHandle);
-                    initialTrustLevelPerCmHandleId.put(cmHandle.getCmHandleId(), cmHandle.getRegistrationTrustLevel());
+                    if (acceptedCmHandleIds.contains(cmHandle.getCmHandleId())) {
+                        final YangModelCmHandle yangModelCmHandle = YangModelCmHandle.toYangModelCmHandle(
+                                dmiPluginRegistration.getDmiPlugin(),
+                                dmiPluginRegistration.getDmiDataPlugin(),
+                                dmiPluginRegistration.getDmiModelPlugin(),
+                                cmHandle,
+                                cmHandle.getModuleSetTag(),
+                                cmHandle.getAlternateId());
+                        yangModelCmHandles.add(yangModelCmHandle);
+                        initialTrustLevelPerCmHandleId.put(cmHandle.getCmHandleId(),
+                            cmHandle.getRegistrationTrustLevel());
+                    }
                 });
         return registerNewCmHandles(yangModelCmHandles, initialTrustLevelPerCmHandleId);
     }
@@ -382,17 +372,47 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService
 
         yangModelCmHandles.removeIf(yangModelCmHandle -> notDeletedCmHandles.contains(yangModelCmHandle.getId()));
         updateCmHandleStateBatch(yangModelCmHandles, CmHandleState.DELETED);
-        removeEntriesFromAlternateIdCache(yangModelCmHandles);
 
         return cmHandleRegistrationResponses;
     }
 
-    private void removeEntriesFromAlternateIdCache(final Collection<YangModelCmHandle> yangModelCmHandles) {
-        for (final YangModelCmHandle yangModelCmHandle : yangModelCmHandles) {
-            cmHandleIdMapper.removeMapping(yangModelCmHandle.getId());
+    private void processRemovedCmHandles(final DmiPluginRegistration dmiPluginRegistration,
+                                         final DmiPluginRegistrationResponse dmiPluginRegistrationResponse) {
+        if (!dmiPluginRegistration.getRemovedCmHandles().isEmpty()) {
+            dmiPluginRegistrationResponse.setRemovedCmHandles(
+                parseAndProcessDeletedCmHandlesInRegistration(dmiPluginRegistration.getRemovedCmHandles()));
         }
     }
 
+    private void processCreatedCmHandles(final DmiPluginRegistration dmiPluginRegistration,
+                                         final DmiPluginRegistrationResponse dmiPluginRegistrationResponse) {
+        final Collection<String> acceptedCmHandleIds = alternateIdChecker
+            .getIdsOfCmHandlesWithAcceptableAlternateId(dmiPluginRegistration.getCreatedCmHandles());
+        if (!acceptedCmHandleIds.isEmpty()) {
+            dmiPluginRegistrationResponse.setCreatedCmHandles(
+                parseAndProcessCreatedCmHandlesInRegistration(dmiPluginRegistration, acceptedCmHandleIds));
+        }
+    }
+
+    private void processUpdatedCmHandles(final DmiPluginRegistration dmiPluginRegistration,
+                                         final DmiPluginRegistrationResponse dmiPluginRegistrationResponse) {
+        if (!dmiPluginRegistration.getUpdatedCmHandles().isEmpty()) {
+            dmiPluginRegistrationResponse.setUpdatedCmHandles(
+                networkCmProxyDataServicePropertyHandler
+                    .updateCmHandleProperties(dmiPluginRegistration.getUpdatedCmHandles()));
+        }
+    }
+
+    private void processUpgradedCmHandles(final DmiPluginRegistration dmiPluginRegistration,
+                                          final DmiPluginRegistrationResponse dmiPluginRegistrationResponse) {
+        if (dmiPluginRegistration.getUpgradedCmHandles() != null
+            && !dmiPluginRegistration.getUpgradedCmHandles().getCmHandles().isEmpty()) {
+            dmiPluginRegistrationResponse.setUpgradedCmHandles(
+                parseAndProcessUpgradedCmHandlesInRegistration(dmiPluginRegistration));
+        }
+    }
+
+
     protected List<CmHandleRegistrationResponse> parseAndProcessUpgradedCmHandlesInRegistration(
         final DmiPluginRegistration dmiPluginRegistration) {
 
@@ -549,12 +569,6 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService
         }
     }
 
-    private void cacheAlternateIds(final Collection<NcmpServiceCmHandle> ncmpServiceCmHandles) {
-        for (final NcmpServiceCmHandle ncmpServiceCmHandle : ncmpServiceCmHandles) {
-            if (!StringUtils.isEmpty(ncmpServiceCmHandle.getAlternateId())) {
-                cmHandleIdMapper.addMapping(ncmpServiceCmHandle.getCmHandleId(), ncmpServiceCmHandle.getAlternateId());
-            }
-        }
-    }
+
 
 }
index 13b3fca..84075a4 100644 (file)
@@ -45,7 +45,7 @@ import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.onap.cps.api.CpsDataService;
 import org.onap.cps.ncmp.api.impl.inventory.InventoryPersistence;
-import org.onap.cps.ncmp.api.impl.utils.CmHandleIdMapper;
+import org.onap.cps.ncmp.api.impl.utils.AlternateIdChecker;
 import org.onap.cps.ncmp.api.impl.utils.YangDataConverter;
 import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle;
 import org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse;
@@ -67,7 +67,7 @@ public class NetworkCmProxyDataServicePropertyHandler {
     private final InventoryPersistence inventoryPersistence;
     private final CpsDataService cpsDataService;
     private final JsonObjectMapper jsonObjectMapper;
-    private final CmHandleIdMapper cmHandleIdMapper;
+    private final AlternateIdChecker alternateIdChecker;
 
     /**
      * Iterates over incoming ncmpServiceCmHandles and update the dataNodes based on the updated attributes.
@@ -81,8 +81,8 @@ public class NetworkCmProxyDataServicePropertyHandler {
         for (final NcmpServiceCmHandle ncmpServiceCmHandle : ncmpServiceCmHandles) {
             final String cmHandleId = ncmpServiceCmHandle.getCmHandleId();
             try {
-                final DataNode existingCmHandleDataNode = inventoryPersistence.getCmHandleDataNode(cmHandleId)
-                        .iterator().next();
+                final DataNode existingCmHandleDataNode = inventoryPersistence
+                    .getCmHandleDataNodeByCmHandleId(cmHandleId).iterator().next();
                 updateAlternateId(existingCmHandleDataNode, ncmpServiceCmHandle);
                 processUpdates(existingCmHandleDataNode, ncmpServiceCmHandle);
                 cmHandleRegistrationResponses.add(CmHandleRegistrationResponse.createSuccessResponse(cmHandleId));
@@ -105,17 +105,14 @@ public class NetworkCmProxyDataServicePropertyHandler {
 
     private void updateAlternateId(final DataNode existingCmHandleDataNode,
                                    final NcmpServiceCmHandle ncmpServiceCmHandle) {
+        final YangModelCmHandle yangModelCmHandle =
+            YangDataConverter.convertCmHandleToYangModel(existingCmHandleDataNode,
+                ncmpServiceCmHandle.getCmHandleId());
+        final String currentAlternateId = yangModelCmHandle.getAlternateId();
         final String newAlternateId = ncmpServiceCmHandle.getAlternateId();
-        if (cmHandleIdMapper.addMapping(ncmpServiceCmHandle.getCmHandleId(), newAlternateId)) {
-            try {
-                final YangModelCmHandle yangModelCmHandle =
-                        YangDataConverter.convertCmHandleToYangModel(existingCmHandleDataNode,
-                                ncmpServiceCmHandle.getCmHandleId());
-                setAndUpdateAlternateId(yangModelCmHandle, newAlternateId);
-            } catch (final Exception e) {
-                cmHandleIdMapper.removeMapping(ncmpServiceCmHandle.getCmHandleId());
-                throw e;
-            }
+        if (alternateIdChecker.canApplyAlternateId(ncmpServiceCmHandle.getCmHandleId(),
+            currentAlternateId, newAlternateId)) {
+            setAndUpdateAlternateId(yangModelCmHandle, newAlternateId);
         }
     }
 
index dcd0368..e230b3f 100644 (file)
@@ -114,12 +114,20 @@ public interface InventoryPersistence extends NcmpPersistence {
     void saveCmHandleBatch(List<YangModelCmHandle> yangModelCmHandles);
 
     /**
-     * Get data node of given cm handle.
+     * Get data node with the given cm handle id.
      *
      * @param cmHandleId cmHandle ID
      * @return data node
      */
-    Collection<DataNode> getCmHandleDataNode(String cmHandleId);
+    Collection<DataNode> getCmHandleDataNodeByCmHandleId(String cmHandleId);
+
+    /**
+     * Get data node with the given alternate id.
+     *
+     * @param alternateId alternate ID
+     * @return data node
+     */
+    DataNode getCmHandleDataNodeByAlternateId(String alternateId);
 
     /**
      * Get collection of data nodes of given cm handles.
index 3b70786..08ab15e 100644 (file)
@@ -22,6 +22,8 @@
 
 package org.onap.cps.ncmp.api.impl.inventory;
 
+import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS;
+
 import com.google.common.collect.Lists;
 import java.time.OffsetDateTime;
 import java.util.ArrayList;
@@ -37,6 +39,7 @@ import org.onap.cps.api.CpsModuleService;
 import org.onap.cps.ncmp.api.impl.utils.YangDataConverter;
 import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle;
 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.ModuleDefinition;
@@ -54,6 +57,7 @@ public class InventoryPersistenceImpl extends NcmpPersistenceImpl implements Inv
     private final CpsModuleService cpsModuleService;
     private final CpsAnchorService cpsAnchorService;
     private final CpsValidator cpsValidator;
+    private final CmHandleQueries cmHandleQueries;
 
     /**
      * initialize an inventory persistence object.
@@ -66,18 +70,19 @@ public class InventoryPersistenceImpl extends NcmpPersistenceImpl implements Inv
      */
     public InventoryPersistenceImpl(final JsonObjectMapper jsonObjectMapper, final CpsDataService cpsDataService,
                                     final CpsModuleService cpsModuleService, final CpsValidator cpsValidator,
-                                    final CpsAnchorService cpsAnchorService) {
+                                    final CpsAnchorService cpsAnchorService, final CmHandleQueries cmHandleQueries) {
         super(jsonObjectMapper, cpsDataService, cpsModuleService, cpsValidator);
         this.cpsModuleService = cpsModuleService;
         this.cpsAnchorService = cpsAnchorService;
         this.cpsValidator = cpsValidator;
+        this.cmHandleQueries = cmHandleQueries;
     }
 
 
     @Override
     public CompositeState getCmHandleState(final String cmHandleId) {
         final DataNode stateAsDataNode = cpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,
-                        createCmHandleXPath(cmHandleId) + "/state", FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS)
+                        getXPathForCmHandleById(cmHandleId) + "/state", FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS)
                 .iterator().next();
         cpsValidator.validateNameCharacters(cmHandleId);
         return new CompositeStateBuilder().fromDataNode(stateAsDataNode).build();
@@ -87,14 +92,14 @@ public class InventoryPersistenceImpl extends NcmpPersistenceImpl implements Inv
     public void saveCmHandleState(final String cmHandleId, final CompositeState compositeState) {
         final String cmHandleJsonData = createStateJsonData(jsonObjectMapper.asJsonString(compositeState));
         cpsDataService.updateDataNodeAndDescendants(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,
-                createCmHandleXPath(cmHandleId), cmHandleJsonData, OffsetDateTime.now());
+                getXPathForCmHandleById(cmHandleId), cmHandleJsonData, OffsetDateTime.now());
     }
 
     @Override
     public void saveCmHandleStateBatch(final Map<String, CompositeState> cmHandleStatePerCmHandleId) {
         final Map<String, String> cmHandlesJsonDataMap = new HashMap<>();
         cmHandleStatePerCmHandleId.forEach((cmHandleId, compositeState) -> cmHandlesJsonDataMap.put(
-                createCmHandleXPath(cmHandleId),
+                getXPathForCmHandleById(cmHandleId),
                 createStateJsonData(jsonObjectMapper.asJsonString(compositeState))));
         cpsDataService.updateDataNodesAndDescendants(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,
                 cmHandlesJsonDataMap, OffsetDateTime.now());
@@ -103,7 +108,7 @@ public class InventoryPersistenceImpl extends NcmpPersistenceImpl implements Inv
     @Override
     public YangModelCmHandle getYangModelCmHandle(final String cmHandleId) {
         cpsValidator.validateNameCharacters(cmHandleId);
-        final DataNode dataNode = getCmHandleDataNode(cmHandleId).iterator().next();
+        final DataNode dataNode = getCmHandleDataNodeByCmHandleId(cmHandleId).iterator().next();
         return YangDataConverter.convertCmHandleToYangModel(dataNode, cmHandleId);
     }
 
@@ -158,14 +163,26 @@ public class InventoryPersistenceImpl extends NcmpPersistenceImpl implements Inv
     }
 
     @Override
-    public Collection<DataNode> getCmHandleDataNode(final String cmHandleId) {
-        return this.getDataNode(createCmHandleXPath(cmHandleId));
+    public Collection<DataNode> getCmHandleDataNodeByCmHandleId(final String cmHandleId) {
+        return this.getDataNode(getXPathForCmHandleById(cmHandleId));
+    }
+
+    @Override
+    public DataNode getCmHandleDataNodeByAlternateId(final String alternateId) {
+        final String xPathForCmHandleByAlternateId = getXPathForCmHandleByAlternateId(alternateId);
+        final Collection<DataNode> dataNodes = cmHandleQueries
+            .queryNcmpRegistryByCpsPath(xPathForCmHandleByAlternateId, OMIT_DESCENDANTS);
+        if (dataNodes.isEmpty()) {
+            throw new DataNodeNotFoundException(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,
+                xPathForCmHandleByAlternateId);
+        }
+        return dataNodes.iterator().next();
     }
 
     @Override
     public Collection<DataNode> getCmHandleDataNodes(final Collection<String> cmHandleIds) {
         final Collection<String> xpaths = new ArrayList<>(cmHandleIds.size());
-        cmHandleIds.forEach(cmHandleId -> xpaths.add(createCmHandleXPath(cmHandleId)));
+        cmHandleIds.forEach(cmHandleId -> xpaths.add(getXPathForCmHandleById(cmHandleId)));
         return this.getDataNodes(xpaths);
     }
 
@@ -174,10 +191,14 @@ public class InventoryPersistenceImpl extends NcmpPersistenceImpl implements Inv
         return cpsAnchorService.queryAnchorNames(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, moduleNamesForQuery);
     }
 
-    private static String createCmHandleXPath(final String cmHandleId) {
+    private static String getXPathForCmHandleById(final String cmHandleId) {
         return NCMP_DMI_REGISTRY_PARENT + "/cm-handles[@id='" + cmHandleId + "']";
     }
 
+    private static String getXPathForCmHandleByAlternateId(final String alternateId) {
+        return NCMP_DMI_REGISTRY_PARENT + "/cm-handles[@alternate-id='" + alternateId + "']";
+    }
+
     private static String createStateJsonData(final String state) {
         return "{\"state\":" + state + "}";
     }
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/AlternateIdChecker.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/AlternateIdChecker.java
new file mode 100644 (file)
index 0000000..1be1a90
--- /dev/null
@@ -0,0 +1,142 @@
+/*
+ * ============LICENSE_START========================================================
+ * Copyright (c) 2024 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.utils;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.onap.cps.ncmp.api.impl.inventory.InventoryPersistence;
+import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle;
+import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle;
+import org.onap.cps.spi.exceptions.DataNodeNotFoundException;
+import org.springframework.stereotype.Service;
+
+@Service
+@Slf4j
+@RequiredArgsConstructor
+public class AlternateIdChecker {
+
+    private final InventoryPersistence inventoryPersistence;
+
+    private static final String NO_CURRENT_ALTERNATE_ID = "";
+
+    /**
+     * Check if the alternate can be applied to the given cm handle (id).
+     * Conditions:
+     * - proposed alternate is blank (it wil be ignored)
+     * - proposed alternate is same as current (no change)
+     * - proposed alternate is not in use for a different cm handle (in the DB)
+     *
+     * @param cmHandleId cm handle id
+     * @param proposedAlternateId proposed alternate id
+     * @return true if the new alternate id not in use or equal to current alternate id, false otherwise
+     */
+    public boolean canApplyAlternateId(final String cmHandleId, final String proposedAlternateId) {
+        String currentAlternateId = "";
+        try {
+            final YangModelCmHandle yangModelCmHandle = inventoryPersistence.getYangModelCmHandle(cmHandleId);
+            currentAlternateId = yangModelCmHandle.getAlternateId();
+        } catch (final DataNodeNotFoundException dataNodeNotFoundException) {
+            // work with blank current alternate id
+        }
+        return this.canApplyAlternateId(cmHandleId, currentAlternateId, proposedAlternateId);
+    }
+
+    /**
+     * Check if the alternate can be applied to the given cm handle.
+     * Conditions:
+     * - proposed alternate is blank (it wil be ignored)
+     * - proposed alternate is same as current (no change)
+     * - proposed alternate is not in use for a different cm handle (in the DB)
+     *
+     * @param cmHandleId   cm handle id
+     * @param currentAlternateId current alternate id
+     * @param proposedAlternateId new alternate id
+     * @return true if the new alternate id not in use or equal to current alternate id, false otherwise
+     */
+    public boolean canApplyAlternateId(final String cmHandleId,
+                                       final String currentAlternateId,
+                                       final String proposedAlternateId) {
+        if (StringUtils.isBlank(currentAlternateId)) {
+            if (alternateIdAlreadyInDb(proposedAlternateId)) {
+                log.warn("Alternate id update ignored, cannot update cm handle {}, alternate id is already "
+                    + "assigned to a different cm handle", cmHandleId);
+                return false;
+            }
+            return true;
+        }
+        if (currentAlternateId.equals(proposedAlternateId)) {
+            return true;
+        }
+        log.warn("Alternate id update ignored, cannot update cm handle {}, already has an alternate id of {}",
+            cmHandleId, currentAlternateId);
+        return false;
+    }
+
+    /**
+     * Check all alternate ids of a batch of NEW cm handles.
+     * Includes cross-checks in the batch itself for duplicates. Only the first entry encountered wil be accepted.
+     * This method can only be used for NEW cm handle registrations NOT for updating existing ones
+     *
+     * @param newNcmpServiceCmHandles the proposed new cm handles
+     * @return collection of cm handles ids which are acceptable
+     */
+    public Collection<String> getIdsOfCmHandlesWithAcceptableAlternateId(
+                                    final Collection<NcmpServiceCmHandle> newNcmpServiceCmHandles) {
+        final Set<String> acceptedAlternateIds = new HashSet<>(newNcmpServiceCmHandles.size());
+        final Collection<String> acceptedCmHandleIds = new ArrayList<>(newNcmpServiceCmHandles.size());
+        for (final NcmpServiceCmHandle ncmpServiceCmHandle : newNcmpServiceCmHandles) {
+            final String cmHandleId = ncmpServiceCmHandle.getCmHandleId();
+            final String proposedAlternateId = ncmpServiceCmHandle.getAlternateId();
+            final boolean isAcceptable;
+            if (StringUtils.isEmpty(proposedAlternateId)) {
+                isAcceptable = true;
+            } else {
+                if (acceptedAlternateIds.contains(proposedAlternateId)) {
+                    isAcceptable = false;
+                    log.warn("Alternate id update ignored, cannot update cm handle {}, alternate id is already "
+                        + "assigned to a different cm handle (in this batch)", cmHandleId);
+                } else {
+                    isAcceptable = canApplyAlternateId(cmHandleId, NO_CURRENT_ALTERNATE_ID, proposedAlternateId);
+                }
+            }
+            if (isAcceptable) {
+                acceptedAlternateIds.add(proposedAlternateId);
+                acceptedCmHandleIds.add(cmHandleId);
+            }
+        }
+        return acceptedCmHandleIds;
+    }
+
+    private boolean alternateIdAlreadyInDb(final String alternateId) {
+        try {
+            inventoryPersistence.getCmHandleDataNodeByAlternateId(alternateId);
+        } catch (final DataNodeNotFoundException dataNodeNotFoundException) {
+            return false;
+        }
+        return true;
+    }
+
+}
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/CmHandleIdMapper.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/CmHandleIdMapper.java
deleted file mode 100644 (file)
index a88adbd..0000000
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * ============LICENSE_START========================================================
- * Copyright (c) 2024 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.utils;
-
-import java.util.Map;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.apache.commons.lang3.StringUtils;
-import org.onap.cps.ncmp.api.NetworkCmProxyCmHandleQueryService;
-import org.springframework.stereotype.Service;
-
-@Service
-@Slf4j
-@RequiredArgsConstructor
-public class CmHandleIdMapper {
-
-    private final Map<String, String> alternateIdPerCmHandleId;
-    private final Map<String, String> cmHandleIdPerAlternateId;
-    private final NetworkCmProxyCmHandleQueryService networkCmProxyCmHandleQueryService;
-
-    private boolean cacheIsInitialized = false;
-
-    public String cmHandleIdToAlternateId(final String cmHandleId) {
-        initializeCache();
-        return alternateIdPerCmHandleId.get(cmHandleId);
-    }
-
-    public String alternateIdToCmHandleId(final String alternateId) {
-        initializeCache();
-        return cmHandleIdPerAlternateId.get(alternateId);
-    }
-
-    public boolean addMapping(final String cmHandleId, final String alternateId) {
-        initializeCache();
-        return addMappingWithValidation(cmHandleId, alternateId);
-    }
-
-
-    private boolean addMappingWithValidation(final String cmHandleId, final String alternateId) {
-        if (alternateIdPerCmHandleId.containsKey(cmHandleId)) {
-            final String originalAlternateId = alternateIdPerCmHandleId.get(cmHandleId);
-            if (!originalAlternateId.equals(alternateId)) {
-                log.warn("Alternate id update ignored, cannot update cm handle {}, already has an alternate id of {}",
-                        cmHandleId, originalAlternateId);
-            }
-            return false;
-        }
-        if (StringUtils.isBlank(alternateId)) {
-            return false;
-        }
-        alternateIdPerCmHandleId.put(cmHandleId, alternateId);
-        cmHandleIdPerAlternateId.put(alternateId, cmHandleId);
-        return true;
-    }
-
-    public void removeMapping(final String cmHandleId) {
-        final String alternateId = alternateIdPerCmHandleId.remove(cmHandleId);
-        removeAlternateIdWithValidation(alternateId);
-    }
-
-    private void removeAlternateIdWithValidation(final String alternateId) {
-        if (alternateId != null) {
-            cmHandleIdPerAlternateId.remove(alternateId);
-        }
-    }
-
-    private void initializeCache() {
-        if (!cacheIsInitialized) {
-            networkCmProxyCmHandleQueryService.getAllCmHandles().forEach(cmHandle ->
-                addMappingWithValidation(cmHandle.getCmHandleId(), cmHandle.getAlternateId())
-            );
-            log.info("Alternate ID cache initialized from DB with {} cm handle/alternate id pairs ",
-                    alternateIdPerCmHandleId.size());
-            cacheIsInitialized = true;
-        }
-    }
-}
index c7ac8ab..572adb8 100644 (file)
@@ -21,6 +21,8 @@
 
 package org.onap.cps.ncmp.api.impl
 
+import java.util.stream.Collectors
+
 import static org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.Status
 import static org.onap.cps.ncmp.api.NcmpResponseStatus.CM_HANDLES_NOT_FOUND
 import static org.onap.cps.ncmp.api.NcmpResponseStatus.CM_HANDLE_ALREADY_EXIST
@@ -28,7 +30,7 @@ import static org.onap.cps.ncmp.api.NcmpResponseStatus.CM_HANDLE_INVALID_ID
 import static org.onap.cps.ncmp.api.NcmpResponseStatus.UNKNOWN_ERROR
 
 import org.onap.cps.ncmp.api.impl.trustlevel.TrustLevelManager
-import org.onap.cps.ncmp.api.impl.utils.CmHandleIdMapper
+import org.onap.cps.ncmp.api.impl.utils.AlternateIdChecker
 import org.onap.cps.ncmp.api.models.UpgradedCmHandles
 import com.fasterxml.jackson.databind.ObjectMapper
 import com.hazelcast.map.IMap
@@ -68,10 +70,16 @@ class NetworkCmProxyDataServiceImplRegistrationSpec extends Specification {
     def mockModuleSyncStartedOnCmHandles = Mock(IMap<String, Object>)
     def trustLevelPerDmiPlugin = [:]
     def mockTrustLevelManager = Mock(TrustLevelManager)
-    def mockCmHandleIdMapper = Mock(CmHandleIdMapper)
+    def mockAlternateIdChecker = Mock(AlternateIdChecker)
     def objectUnderTest = getObjectUnderTest()
     def mockModuleSetTagCache = [:]
 
+    def setup() {
+        // always accept all cm handles
+        mockAlternateIdChecker.getIdsOfCmHandlesWithAcceptableAlternateId(_) >>
+            { args -> args[0].stream().map(it -> it.cmHandleId).collect(Collectors.toList()) }
+    }
+
     def 'DMI Registration: Create, Update, Delete & Upgrade operations are processed in the right order'() {
         given: 'a registration with operations of all types'
             def dmiRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server')
@@ -160,7 +168,7 @@ class NetworkCmProxyDataServiceImplRegistrationSpec extends Specification {
         when: 'update registration and sync module is called with correct DMI plugin information'
             objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
         then: 'create cm handles registration and sync modules is called with the correct plugin information'
-            1 * objectUnderTest.parseAndProcessCreatedCmHandlesInRegistration(dmiPluginRegistration)
+            1 * objectUnderTest.parseAndProcessCreatedCmHandlesInRegistration(dmiPluginRegistration, _)
         and: 'dmi is added to the dmi trustLevel map'
             assert trustLevelPerDmiPlugin.size() == 1
             assert trustLevelPerDmiPlugin.containsKey(expectedDmiPluginRegisteredName)
@@ -433,19 +441,19 @@ class NetworkCmProxyDataServiceImplRegistrationSpec extends Specification {
 
     def 'Adding data to alternate id caches.'() {
         given: 'a registration with three CM Handles to be created'
-            def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server',
-                    createdCmHandles: [new NcmpServiceCmHandle(cmHandleId: 'cmhandle1', alternateId: 'my-alternate-id-1')])
+            def ncmpServiceCmHandles = [new NcmpServiceCmHandle(cmHandleId: 'cmhandle1', alternateId: 'my-alternate-id-1')]
+            def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: 'my-server', createdCmHandles: ncmpServiceCmHandles)
         when: 'the DMI plugin registration happens'
             objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration)
         then: 'the new alternate id is added to the cache'
-            1 * mockCmHandleIdMapper.addMapping('cmhandle1', 'my-alternate-id-1')
+            1 * mockAlternateIdChecker.getIdsOfCmHandlesWithAcceptableAlternateId(ncmpServiceCmHandles) >> ['cmhandle1']
     }
 
     def getObjectUnderTest() {
         return Spy(new NetworkCmProxyDataServiceImpl(spiedJsonObjectMapper, mockDmiDataOperations,
                 mockNetworkCmProxyDataServicePropertyHandler, mockInventoryPersistence, mockCmHandleQueries,
                 stubbedNetworkCmProxyCmHandlerQueryService, mockLcmEventsCmHandleStateHandler, mockCpsDataService,
-                mockModuleSyncStartedOnCmHandles, trustLevelPerDmiPlugin, mockTrustLevelManager, mockCmHandleIdMapper, mockModuleSetTagCache))
+                mockModuleSyncStartedOnCmHandles, trustLevelPerDmiPlugin, mockTrustLevelManager, mockAlternateIdChecker, mockModuleSetTagCache))
     }
 
     def addPersistedYangModelCmHandles(ids) {
index c835056..64bedb8 100644 (file)
@@ -32,7 +32,7 @@ import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.PASSTHROUGH_RU
 import static org.onap.cps.ncmp.api.impl.operations.OperationType.CREATE
 import static org.onap.cps.ncmp.api.impl.operations.OperationType.UPDATE
 
-import org.onap.cps.ncmp.api.impl.utils.CmHandleIdMapper
+import org.onap.cps.ncmp.api.impl.utils.AlternateIdChecker
 import com.hazelcast.map.IMap
 import org.onap.cps.ncmp.api.NetworkCmProxyCmHandleQueryService
 import org.onap.cps.ncmp.api.impl.events.lcm.LcmEventsCmHandleStateHandler
@@ -54,7 +54,7 @@ import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle
 import org.onap.cps.ncmp.api.models.DataOperationRequest
 import org.onap.cps.spi.exceptions.CpsException
 import org.onap.cps.spi.model.ConditionProperties
-import spock.lang.Shared
+
 import java.util.stream.Collectors
 import org.onap.cps.utils.JsonObjectMapper
 import com.fasterxml.jackson.databind.ObjectMapper
@@ -80,14 +80,12 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
     def stubModuleSyncStartedOnCmHandles = Stub(IMap<String, Object>)
     def stubTrustLevelPerDmiPlugin = Stub(Map<String, TrustLevel>)
     def mockTrustLevelManager = Mock(TrustLevelManager)
-    def mockCmHandleIdMapper = Mock(CmHandleIdMapper)
+    def mockAlternateIdChecker = Mock(AlternateIdChecker)
     def mockModuleSetTagCache = [:]
 
     def NO_TOPIC = null
     def NO_REQUEST_ID = null
-    @Shared
     def OPTIONS_PARAM = '(a=1,b=2)'
-    @Shared
     def ncmpServiceCmHandle = new NcmpServiceCmHandle(cmHandleId: 'test-cm-handle-id')
 
     def objectUnderTest = new NetworkCmProxyDataServiceImpl(
@@ -102,7 +100,7 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
             stubModuleSyncStartedOnCmHandles,
             stubTrustLevelPerDmiPlugin,
             mockTrustLevelManager,
-            mockCmHandleIdMapper,
+            mockAlternateIdChecker,
             mockModuleSetTagCache)
 
     def cmHandleXPath = "/dmi-registry/cm-handles[@id='testCmHandle']"
@@ -269,7 +267,7 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
             dmiPluginRegistration.createdCmHandles = [ncmpServiceCmHandle]
             mockDmiPluginRegistration.getCreatedCmHandles() >> [ncmpServiceCmHandle]
         when: 'parse and create cm handle in dmi registration then sync module'
-            objectUnderTest.parseAndProcessCreatedCmHandlesInRegistration(mockDmiPluginRegistration)
+            objectUnderTest.parseAndProcessCreatedCmHandlesInRegistration(mockDmiPluginRegistration, ['test-cm-handle-id'])
         then: 'system persists the cm handle state'
             1 * mockLcmEventsCmHandleStateHandler.initiateStateAdvised(_) >> {
                 args -> {
@@ -280,7 +278,7 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
                     }
             }
     }
-    
+
     def 'Execute cm handle id search'() {
         given: 'valid CmHandleQueryApiParameters input'
             def cmHandleQueryApiParameters = new CmHandleQueryApiParameters()
index f94c34c..d822f2e 100644 (file)
 
 package org.onap.cps.ncmp.api.impl
 
-
 import com.fasterxml.jackson.databind.ObjectMapper
-import org.onap.cps.ncmp.api.impl.utils.CmHandleIdMapper
-
-import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DATASPACE_NAME
-import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DMI_REGISTRY_ANCHOR
-import static org.onap.cps.ncmp.api.NcmpResponseStatus.CM_HANDLES_NOT_FOUND
-import static org.onap.cps.ncmp.api.NcmpResponseStatus.CM_HANDLE_INVALID_ID
-import static org.onap.cps.ncmp.api.NcmpResponseStatus.UNKNOWN_ERROR
-import static org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.Status
-
 import org.onap.cps.api.CpsDataService
-import org.onap.cps.utils.JsonObjectMapper
 import org.onap.cps.ncmp.api.impl.inventory.InventoryPersistence
-import org.onap.cps.spi.exceptions.DataValidationException
+import org.onap.cps.ncmp.api.impl.utils.AlternateIdChecker
 import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle
 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 org.onap.cps.utils.JsonObjectMapper
 import spock.lang.Specification
 
+import static org.onap.cps.ncmp.api.NcmpResponseStatus.CM_HANDLES_NOT_FOUND
+import static org.onap.cps.ncmp.api.NcmpResponseStatus.CM_HANDLE_INVALID_ID
+import static org.onap.cps.ncmp.api.NcmpResponseStatus.UNKNOWN_ERROR
+import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DATASPACE_NAME
+import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DMI_REGISTRY_ANCHOR
+import static org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.Status
+
 class NetworkCmProxyDataServicePropertyHandlerSpec extends Specification {
 
     def mockInventoryPersistence = Mock(InventoryPersistence)
     def mockCpsDataService = Mock(CpsDataService)
     def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
-    def mockCmHandleIdMapper = Mock(CmHandleIdMapper)
+    def mockAlternateIdChecker = Mock(AlternateIdChecker)
 
-    def objectUnderTest = new NetworkCmProxyDataServicePropertyHandler(mockInventoryPersistence, mockCpsDataService, jsonObjectMapper, mockCmHandleIdMapper)
+    def objectUnderTest = new NetworkCmProxyDataServicePropertyHandler(mockInventoryPersistence, mockCpsDataService, jsonObjectMapper, mockAlternateIdChecker)
     def static cmHandleId = 'myHandle1'
     def static cmHandleXpath = "/dmi-registry/cm-handles[@id='${cmHandleId}']"
 
@@ -62,7 +60,7 @@ class NetworkCmProxyDataServicePropertyHandlerSpec extends Specification {
 
     def 'Update CM Handle Public Properties: #scenario'() {
         given: 'the CPS service return a CM handle'
-            mockInventoryPersistence.getCmHandleDataNode(cmHandleId) >> cmHandleDataNodeAsCollection
+            mockInventoryPersistence.getCmHandleDataNodeByCmHandleId(cmHandleId) >> cmHandleDataNodeAsCollection
         and: 'an update cm handle request with public properties updates'
             def cmHandleUpdateRequest = [new NcmpServiceCmHandle(cmHandleId: cmHandleId, publicProperties: updatedPublicProperties)]
         when: 'update data node leaves is called with the update request'
@@ -84,7 +82,7 @@ class NetworkCmProxyDataServicePropertyHandlerSpec extends Specification {
 
     def 'Update DMI Properties: #scenario'() {
         given: 'the CPS service return a CM handle'
-            mockInventoryPersistence.getCmHandleDataNode(cmHandleId) >> cmHandleDataNodeAsCollection
+            mockInventoryPersistence.getCmHandleDataNodeByCmHandleId(cmHandleId) >> cmHandleDataNodeAsCollection
         and: 'an update cm handle request with DMI properties updates'
             def cmHandleUpdateRequest = [new NcmpServiceCmHandle(cmHandleId: cmHandleId, dmiProperties: updatedDmiProperties)]
         when: 'update data node leaves is called with the update request'
@@ -108,7 +106,7 @@ class NetworkCmProxyDataServicePropertyHandlerSpec extends Specification {
     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)
-            mockInventoryPersistence.getCmHandleDataNode(cmHandleId) >> [cmHandleDataNode]
+            mockInventoryPersistence.getCmHandleDataNodeByCmHandleId(cmHandleId) >> [cmHandleDataNode]
         and: 'an update cm handle request that removes all public properties(existing and non-existing)'
             def cmHandleUpdateRequest = [new NcmpServiceCmHandle(cmHandleId: cmHandleId, publicProperties: ['publicProp3': null, 'publicProp4': null])]
         when: 'update data node leaves is called with the update request'
@@ -131,7 +129,7 @@ class NetworkCmProxyDataServicePropertyHandlerSpec extends Specification {
         given: 'cm handles request'
             def cmHandleUpdateRequest = [new NcmpServiceCmHandle(cmHandleId: cmHandleId, publicProperties: [:], dmiProperties: [:])]
         and: 'data node cannot be found'
-            mockInventoryPersistence.getCmHandleDataNode(*_) >> { throw exception }
+            mockInventoryPersistence.getCmHandleDataNodeByCmHandleId(*_) >> { throw exception }
         when: 'update data node leaves is called using correct parameters'
             def response = objectUnderTest.updateCmHandleProperties(cmHandleUpdateRequest)
         then: 'one failed registration response'
@@ -156,7 +154,7 @@ class NetworkCmProxyDataServicePropertyHandlerSpec extends Specification {
                                          new NcmpServiceCmHandle(cmHandleId: cmHandleId, publicProperties: ['publicProp1': "value"], dmiProperties: [:]),
                                          new NcmpServiceCmHandle(cmHandleId: cmHandleId, publicProperties: ['publicProp1': "value"], dmiProperties: [:])]
         and: 'data node can be found for 1st and 3rd cm-handle but not for 2nd cm-handle'
-            mockInventoryPersistence.getCmHandleDataNode(*_) >> cmHandleDataNodeAsCollection >> {
+            mockInventoryPersistence.getCmHandleDataNodeByCmHandleId(*_) >> cmHandleDataNodeAsCollection >> {
                 throw new DataNodeNotFoundException(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR) } >> cmHandleDataNodeAsCollection
         when: 'update data node leaves is called using correct parameters'
             def cmHandleResponseList = objectUnderTest.updateCmHandleProperties(cmHandleUpdateRequest)
@@ -194,7 +192,7 @@ class NetworkCmProxyDataServicePropertyHandlerSpec extends Specification {
                     { args ->
                         assert args[3].contains('alt-1')
                     }
-            mockCmHandleIdMapper.addMapping(cmHandleId, 'alt-1') >> isNewMapping
+            mockAlternateIdChecker.canApplyAlternateId(cmHandleId, '','alt-1') >> isNewMapping
         where: 'following updates are attempted'
             scenario                | isNewMapping || callsToDataService
             'new alternate id   '   | true         || 1
@@ -205,8 +203,8 @@ class NetworkCmProxyDataServicePropertyHandlerSpec extends Specification {
         given: 'an existing data node and an update request with an alternate id'
             def ncmpServiceCmHandle = new NcmpServiceCmHandle(cmHandleId: cmHandleId, alternateId: 'alt-1')
             DataNode existingCmHandleDataNode = new DataNode(xpath: cmHandleXpath, leaves: ['alternate-id': null])
-        and: 'a new mapping is added'
-            mockCmHandleIdMapper.addMapping(cmHandleId, 'alt-1') >> true
+        and: 'an applicable alternate id for the cm handle'
+            mockAlternateIdChecker.canApplyAlternateId(cmHandleId, '','alt-1') >> true
         and: 'but an exception occurs while saving'
             def originalException = new NullPointerException('some exception')
             mockCpsDataService.updateNodeLeaves(*_) >> { throw originalException }
@@ -215,8 +213,6 @@ class NetworkCmProxyDataServicePropertyHandlerSpec extends Specification {
         then: 'the original exception is thrown up'
             def thrownException = thrown(NullPointerException)
             assert thrownException == originalException
-        and: 'the mapping is removed from the cache'
-            1 * mockCmHandleIdMapper.removeMapping(cmHandleId)
     }
 
     def convertToProperties(expectedPropertiesAfterUpdateAsMap) {
index a3b923f..83acb22 100644 (file)
 
 package org.onap.cps.ncmp.api.impl.inventory
 
-import org.onap.cps.api.CpsAnchorService
-
-import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME
-import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DATASPACE_NAME
-import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DMI_REGISTRY_ANCHOR
-import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DMI_REGISTRY_PARENT
-import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NO_TIMESTAMP
-import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
-
 import com.fasterxml.jackson.databind.ObjectMapper
+import org.onap.cps.api.CpsAnchorService
 import org.onap.cps.api.CpsDataService
 import org.onap.cps.api.CpsModuleService
 import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle
 import org.onap.cps.spi.CascadeDeleteAllowed
 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.ModuleDefinition
 import org.onap.cps.spi.model.ModuleReference
-import org.onap.cps.utils.JsonObjectMapper
 import org.onap.cps.spi.utils.CpsValidator
+import org.onap.cps.utils.JsonObjectMapper
 import spock.lang.Shared
 import spock.lang.Specification
+
 import java.time.OffsetDateTime
 import java.time.ZoneOffset
 import java.time.format.DateTimeFormatter
 
+import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DATASPACE_NAME
+import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DMI_REGISTRY_ANCHOR
+import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DMI_REGISTRY_PARENT
+import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME
+import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NO_TIMESTAMP
+import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
+import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
+
 class InventoryPersistenceImplSpec extends Specification {
 
     def spiedJsonObjectMapper = Spy(new JsonObjectMapper(new ObjectMapper()))
@@ -60,8 +62,10 @@ class InventoryPersistenceImplSpec extends Specification {
 
     def mockCpsValidator = Mock(CpsValidator)
 
+    def mockCmHandleQueries = Mock(CmHandleQueries)
+
     def objectUnderTest = new InventoryPersistenceImpl(spiedJsonObjectMapper, mockCpsDataService, mockCpsModuleService,
-            mockCpsValidator, mockCpsAnchorService)
+            mockCpsValidator, mockCpsAnchorService, mockCmHandleQueries)
 
     def formattedDateAndTime = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
             .format(OffsetDateTime.of(2022, 12, 31, 20, 30, 40, 1, ZoneOffset.UTC))
@@ -283,13 +287,32 @@ class InventoryPersistenceImplSpec extends Specification {
 
     def 'Get cmHandle data node'() {
         given: 'expected xPath to get cmHandle data node'
-            def expectedXPath = '/dmi-registry/cm-handles[@id=\'sample cmHandleId\']';
+            def expectedXPath = '/dmi-registry/cm-handles[@id=\'sample cmHandleId\']'
         when: 'the method to get data nodes is called'
-            objectUnderTest.getCmHandleDataNode('sample cmHandleId')
+            objectUnderTest.getCmHandleDataNodeByCmHandleId('sample cmHandleId')
         then: 'the data persistence service method to get cmHandle data node is invoked once with expected xPath'
             1 * mockCpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, expectedXPath, INCLUDE_ALL_DESCENDANTS)
     }
 
+    def 'Get cm handle data node'() {
+        given: 'expected xPath to get cmHandle data node'
+            def expectedXPath = '/dmi-registry/cm-handles[@alternate-id=\'alternate id\']'
+        and: 'query service is invoked with expected xpath'
+            mockCmHandleQueries.queryNcmpRegistryByCpsPath(expectedXPath, OMIT_DESCENDANTS) >> [new DataNode()]
+        expect: 'getting the cm handle data node'
+            assert objectUnderTest.getCmHandleDataNodeByAlternateId('alternate id') == new DataNode()
+    }
+
+    def 'Attempt to get non existing cm handle data node by alternate id'() {
+        given: 'query service is invoked and returns empty collection of data nodes'
+            mockCmHandleQueries.queryNcmpRegistryByCpsPath(*_) >> []
+        when: 'getting the cm handle data node'
+            objectUnderTest.getCmHandleDataNodeByAlternateId('alternate id')
+        then: 'no data found exception thrown'
+            def thrownException = thrown(DataNodeNotFoundException)
+            assert thrownException.getMessage().contains('DataNode not found')
+    }
+
     def 'Get CM handles that has given module names'() {
         when: 'the method to get cm handles is called'
             objectUnderTest.getCmHandleIdsWithGivenModules(['sample-module-name'])
diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/AlternateIdCheckerSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/AlternateIdCheckerSpec.groovy
new file mode 100644 (file)
index 0000000..f41fd6c
--- /dev/null
@@ -0,0 +1,97 @@
+/*
+ * ============LICENSE_START========================================================
+ * Copyright (c) 2024 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.utils
+
+
+import org.onap.cps.ncmp.api.impl.inventory.InventoryPersistence
+import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle
+import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle
+import org.onap.cps.spi.exceptions.DataNodeNotFoundException
+import org.onap.cps.spi.model.DataNode
+import org.onap.cps.spi.model.DataNodeBuilder
+import spock.lang.Specification
+
+class AlternateIdCheckerSpec extends Specification {
+
+    def mockInventoryPersistenceService = Mock(InventoryPersistence)
+    def someDataNode = new DataNodeBuilder().build()
+    def dataNodeFoundException = new DataNodeNotFoundException('', '')
+
+    def objectUnderTest = new AlternateIdChecker(mockInventoryPersistenceService)
+
+    def 'Check new cm handle with new alternate id.'() {
+        given: 'inventory persistence can not find cm handle id'
+            mockInventoryPersistenceService.getYangModelCmHandle('ch 1') >> {throw dataNodeFoundException}
+        and: 'inventory persistence can not find alternate id'
+            mockInventoryPersistenceService.getCmHandleDataNodeByAlternateId('alternate id') >> {throw dataNodeFoundException}
+        expect: 'mapping can be added'
+             assert objectUnderTest.canApplyAlternateId('ch 1', 'alternate id')
+    }
+
+    def 'Check new cm handle with used alternate id.'() {
+        given: 'inventory persistence can not find cm handle id'
+            mockInventoryPersistenceService.getYangModelCmHandle('ch 1') >> {throw dataNodeFoundException}
+        and: 'inventory persistence can find alternate id'
+            mockInventoryPersistenceService.getCmHandleDataNodeByAlternateId('alternate id') >> { someDataNode }
+        expect: 'mapping can not be added'
+            assert objectUnderTest.canApplyAlternateId('ch 1', 'alternate id') == false
+    }
+
+    def 'Check for existing cm handle with #currentAlternateId.'() {
+        given: 'a cm handle with the #currentAlternateId'
+            def yangModelCmHandle = new YangModelCmHandle(alternateId: currentAlternateId)
+        and: 'inventory service finds the cm handle'
+            mockInventoryPersistenceService.getYangModelCmHandle('my cm handle') >> yangModelCmHandle
+        expect: 'add mapping returns expected result'
+            assert canAdd == objectUnderTest.canApplyAlternateId('my cm handle', 'same alternate id')
+        where: 'following alternate ids is used'
+            currentAlternateId   || canAdd
+            'same alternate id'  || true
+            'other alternate id' || false
+    }
+
+    def 'Check a batch of NEW cm handles with #scenario.'() {
+        given: 'a batch of 2 new cm handles alternate id ids #alt1 and #alt2'
+            def batch = [new NcmpServiceCmHandle(cmHandleId: 'ch-1', alternateId: alt1),
+                         new NcmpServiceCmHandle(cmHandleId: 'ch-2', alternateId: alt2)]
+        and: 'the database already contains cm handle(s) with these alternate ids: #alreadyinDb'
+            mockInventoryPersistenceService.getCmHandleDataNodeByAlternateId(_) >>
+                {  args -> altAlreadyInDb.contains(args[0]) ? new DataNode() : throwDataNodeNotFoundException() }
+        when: 'the batch of new cm handles is checked'
+            def result = objectUnderTest.getIdsOfCmHandlesWithAcceptableAlternateId(batch)
+        then: 'the result only contains the ids of the acceptable cm handles'
+            assert result.contains('ch-1') == acceptCh1
+            assert result.contains('ch-2') == acceptCh2
+        where: 'the following alternate ids are used'
+            scenario                          | alt1   | alt2   | altAlreadyInDb  || acceptCh1 | acceptCh2
+            'no alternate ids'                | ''     | ''     | ['dont matter'] || true      | true
+            'new alternate ids'               | 'fdn1' | 'fdn2' | ['other fdn']   || true      | true
+            'one already used alternate id'   | 'fdn1' | 'fdn2' | ['fdn1']        || false     | true
+            'two already used alternate ids'  | 'fdn1' | 'fdn2' | ['fdn1','fdn2'] || false     | false
+            'duplicate alternate id in batch' | 'fdn1' | 'fdn1' | ['dont matter'] || true      | false
+    }
+
+    def throwDataNodeNotFoundException() {
+        // cannot 'return' an exception in conditional stub behavior, so hence a method call that will always throw this exception
+        throw dataNodeFoundException
+    }
+
+}
diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/CmHandleIdMapperSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/CmHandleIdMapperSpec.groovy
deleted file mode 100644 (file)
index 55ccdf3..0000000
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- * ============LICENSE_START========================================================
- * Copyright (c) 2024 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.utils
-
-import ch.qos.logback.classic.Level
-import ch.qos.logback.classic.Logger
-import ch.qos.logback.classic.spi.ILoggingEvent
-import ch.qos.logback.core.read.ListAppender
-import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle
-import org.slf4j.LoggerFactory
-import org.onap.cps.ncmp.api.NetworkCmProxyCmHandleQueryService
-import spock.lang.Specification
-
-class CmHandleIdMapperSpec extends Specification {
-
-    def alternateIdPerCmHandle = new HashMap<String, String>()
-    def cmHandlePerAlternateId = new HashMap<String, String>()
-    def mockCpsCmHandlerQueryService = Mock(NetworkCmProxyCmHandleQueryService)
-
-    def objectUnderTest = new CmHandleIdMapper(alternateIdPerCmHandle, cmHandlePerAlternateId, mockCpsCmHandlerQueryService)
-
-    def logger = Spy(ListAppender<ILoggingEvent>)
-
-    def setup() {
-        ((Logger) LoggerFactory.getLogger(CmHandleIdMapper.class)).addAppender(logger)
-        logger.start()
-        mockCpsCmHandlerQueryService.getAllCmHandles() >> []
-        assert objectUnderTest.addMapping('my cmhandle id', 'my alternate id')
-    }
-
-    void cleanup() {
-        ((Logger) LoggerFactory.getLogger(CmHandleIdMapper.class)).detachAndStopAllAppenders()
-    }
-
-    def 'Checking entries in the cache.'() {
-        expect: 'the alternate id can be converted to cmhandle id'
-            assert objectUnderTest.alternateIdToCmHandleId('my alternate id') == 'my cmhandle id'
-        and: 'the cmhandle id can be converted to alternate id'
-            assert objectUnderTest.cmHandleIdToAlternateId('my cmhandle id') == 'my alternate id'
-    }
-
-    def 'Attempt adding #scenario alternate id.'() {
-        expect: 'cmhandle id - alternate id mapping fails'
-            assert objectUnderTest.addMapping('ch-1', alternateId) == false
-        and: 'alternate id looked up by cmhandle id unsuccessfully'
-            assert objectUnderTest.cmHandleIdToAlternateId('ch-1') == null
-        where: 'alternate id has an invalid value'
-            scenario | alternateId
-            'empty'  | ''
-            'blank'  | '  '
-            'null'   | null
-    }
-
-    def 'Remove an entry from the cache.'() {
-        when: 'removing an entry'
-            objectUnderTest.removeMapping('my cmhandle id')
-        then: 'converting alternate id returns null'
-            assert objectUnderTest.alternateIdToCmHandleId('my alternate id') == null
-        and: 'converting cmhandle id returns null'
-            assert objectUnderTest.cmHandleIdToAlternateId('my cmhandle id') == null
-    }
-
-    def 'Attempt to remove a non-existing entry from the cache.'() {
-        when: 'removing an entry that is not cached'
-            objectUnderTest.removeMapping('non-cached cmhandle id')
-        then: 'deleting from the cmhandle cache returns null'
-            assert alternateIdPerCmHandle.remove('non-cached cmhandle id') == null
-        and: 'removal from the alternate id cache is skipped'
-            0 * cmHandlePerAlternateId.remove(_)
-    }
-
-    def 'Cannot update existing alternate id.'() {
-        given: 'attempt to update an existing alternate id'
-            objectUnderTest.addMapping('my cmhandle id', 'other id')
-        expect: 'still returns the original alternate id'
-            assert objectUnderTest.cmHandleIdToAlternateId('my cmhandle id') == 'my alternate id'
-        and: 'converting other alternate id returns null'
-            assert objectUnderTest.alternateIdToCmHandleId('other id') == null
-        and: 'a warning is logged with the original alternate id'
-            def lastLoggingEvent = logger.list[1]
-            assert lastLoggingEvent.level == Level.WARN
-            assert lastLoggingEvent.formattedMessage.contains('my alternate id')
-    }
-
-    def 'Update existing alternate id with the same value.'() {
-        expect: 'update an existing alternate id with the same value returns false (no update)'
-            assert objectUnderTest.addMapping('my cmhandle id', 'my alternate id') == false
-        and: 'conversion still returns the original alternate id'
-            assert objectUnderTest.cmHandleIdToAlternateId('my cmhandle id') == 'my alternate id'
-    }
-
-    def 'Initializing cache #scenario.'() {
-        when: 'the cache is (re-)initialized'
-            objectUnderTest.cacheIsInitialized = false
-            objectUnderTest.initializeCache()
-        then: 'the alternate id can be converted to cmhandle id'
-            assert objectUnderTest.alternateIdToCmHandleId('alt-1') == convertedCmHandleId
-        and: 'the cm handle id can be converted to alternate id'
-            assert objectUnderTest.cmHandleIdToAlternateId('ch-1') == convertedAlternatId
-        and: 'the query service is called to get the initial data'
-            1 * mockCpsCmHandlerQueryService.getAllCmHandles() >> persistedCmHandles
-        where: 'the initial data has a cm handle #scenario'
-            scenario                  | persistedCmHandles                                                  || convertedAlternatId | convertedCmHandleId
-            'with alternate id'       | [new NcmpServiceCmHandle(cmHandleId: 'ch-1', alternateId: 'alt-1')] || 'alt-1'             | 'ch-1'
-            'without alternate id'    | [new NcmpServiceCmHandle(cmHandleId: 'ch-1')]                       || null                | null
-            'with blank alternate id' | [new NcmpServiceCmHandle(cmHandleId: 'ch-1', alternateId: ' ')]     || null                | null
-    }
-}
\ No newline at end of file