Merge "Query CmHandles using CPS path"
authorJoseph Keenan <joseph.keenan@est.tech>
Tue, 2 Aug 2022 12:26:46 +0000 (12:26 +0000)
committerGerrit Code Review <gerrit@onap.org>
Tue, 2 Aug 2022 12:26:46 +0000 (12:26 +0000)
19 files changed:
cps-bom/pom.xml
cps-ncmp-rest/docs/openapi/components.yaml
cps-ncmp-rest/docs/openapi/ncmp.yml
cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/mapper/CmHandleStateMapper.java
cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyControllerSpec.groovy
cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/mapper/CmHandleStateMapperTest.groovy
cps-ncmp-service/pom.xml
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandlerQueryServiceImpl.java
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/inventory/CmHandleQueries.java [new file with mode: 0644]
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/InventoryPersistence.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/sync/SyncUtils.java
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandlerQueryServiceSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/CmHandleQueriesSpec.groovy [new file with mode: 0644]
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/InventoryPersistenceSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/sync/SyncUtilsSpec.groovy
cps-service/src/main/java/org/onap/cps/utils/CmHandleQueryRestParametersValidator.java
cps-service/src/main/java/org/onap/cps/utils/ValidQueryProperties.java [new file with mode: 0644]
cps-service/src/test/groovy/org/onap/cps/utils/CmHandleQueryRestParametersValidatorSpec.groovy

index 9b864b0..bf8fbe8 100644 (file)
                 <artifactId>spotbugs</artifactId>
                 <version>${project.version}</version>
             </dependency>
+            <dependency>
+                <groupId>${project.groupId}</groupId>
+                <artifactId>cps-path-parser</artifactId>
+                <version>${project.version}</version>
+            </dependency>
         </dependencies>
     </dependencyManagement>
 </project>
index 2cb9d89..14fd4d2 100644 (file)
@@ -185,18 +185,7 @@ components:
             type: object
             $ref: '#/components/schemas/OldConditionProperties'
           description: not necessary, it is just for backward compatibility
-      example:
-        cmHandleQueryParameters:
-          - conditionName: hasAllModules
-            conditionParameters:
-              - { "moduleName": "my-module-1" }
-              - { "moduleName": "my-module-2" }
-              - { "moduleName": "my-module-3" }
-          - conditionName: hasAllProperties
-            conditionParameters:
-              - { "Color": "yellow" }
-              - { "Shape": "circle" }
-              - { "Size": "small" }
+
     ConditionProperties:
       properties:
         conditionName:
@@ -279,7 +268,7 @@ components:
     sync-state:
       type: object
       properties:
-        state:
+        syncState:
           type: string
           example: NONE_REQUESTED
         lastSyncTime:
@@ -380,6 +369,51 @@ components:
                       - Philip Pullman
                 name: kids
 
+    allCmHandleQueryParameters:
+      value:
+        cmHandleQueryParameters:
+          - conditionName: hasAllModules
+            conditionParameters:
+              - { "moduleName": "my-module-1" }
+              - { "moduleName": "my-module-2" }
+              - { "moduleName": "my-module-3" }
+          - conditionName: hasAllProperties
+            conditionParameters:
+              - { "Color": "yellow" }
+              - { "Shape": "circle" }
+              - { "Size": "small" }
+          - conditionName: cmHandleWithCpsPath
+            conditionParameters:
+              - { "cpsPath": "//state[@cm-handle-state='ADVISED']" }
+    pubPropCmHandleQueryParameters:
+      value:
+        cmHandleQueryParameters:
+          - conditionName: hasAllProperties
+            conditionParameters:
+              - { "Color": "yellow" }
+              - { "Shape": "circle" }
+              - { "Size": "small" }
+    modulesCmHandleQueryParameters:
+      value:
+        cmHandleQueryParameters:
+          - conditionName: hasAllModules
+            conditionParameters:
+              - { "moduleName": "my-module-1" }
+              - { "moduleName": "my-module-2" }
+              - { "moduleName": "my-module-3" }
+    cpsPathCmHandleStateQueryParameters:
+      value:
+        cmHandleQueryParameters:
+          - conditionName: cmHandleWithCpsPath
+            conditionParameters:
+              - { "cpsPath": "//state[@cm-handle-state='LOCKED']" }
+    cpsPathCmHandleDataSyncQueryParameters:
+      value:
+        cmHandleQueryParameters:
+          - conditionName: cmHandleWithCpsPath
+            conditionParameters:
+              - { "cpsPath": "//state[@data-sync-enabled='true']" }
+
   parameters:
     cmHandleInPath:
       name: cm-handle
index aaf0d6a..d7b3837 100755 (executable)
@@ -273,7 +273,7 @@ fetchModuleDefinitionsByCmHandle:
 
 searchCmHandles:
   post:
-    description: Execute cm handle query search, to be included in the result a cm-handle must fulfill ALL the conditions listed here, if one of the given module names does not exists, return with an empty collection.
+    description: Execute cm handle query search and return a list of cm handle details. Any number of conditions can be applied. To be included in the result a cm-handle must fulfill ALL the conditions. An empty collection will be returned in the case that the cm handle does not match a condition. For more on cm handle query search please refer to <a href="RTD page to be created in separate task">cm handle query search Read the Docs</a>.<br/>By supplying a CPS Path it is possible to query on any data related to the cm handle. For more on CPS Path please refer to <a href="https://docs.onap.org/projects/onap-cps/en/latest/cps-path.html">CPS Path Read the Docs</a>. The cm handle ancestor is automatically returned for this query.
     tags:
       - network-cm-proxy
     summary: Execute cm handle search using the available conditions
@@ -284,6 +284,17 @@ searchCmHandles:
         application/json:
           schema:
             $ref: 'components.yaml#/components/schemas/CmHandleQueryParameters'
+          examples:
+            Cm handle properties query:
+              $ref: 'components.yaml#/components/examples/pubPropCmHandleQueryParameters'
+            Cm handle modules query:
+              $ref: 'components.yaml#/components/examples/modulesCmHandleQueryParameters'
+            All cm handle query parameters:
+              $ref: 'components.yaml#/components/examples/allCmHandleQueryParameters'
+            Cm handle with CPS path state query:
+              $ref: 'components.yaml#/components/examples/cpsPathCmHandleStateQueryParameters'
+            Cm handle with data sync flag query:
+              $ref: 'components.yaml#/components/examples/cpsPathCmHandleDataSyncQueryParameters'
     responses:
       200:
         description: OK
@@ -379,7 +390,7 @@ getCmHandleStateById:
 
 searchCmHandleIds:
   post:
-    description: Execute cm handle query search, to be included in the result a cm-handle must fulfill ALL the conditions listed here, if one of the given module names does not exists, return with an empty collection.
+    description: Execute cm handle query search and return a list of cm handle ids. Any number of conditions can be applied. To be included in the result a cm-handle must fulfill ALL the conditions. An empty collection will be returned in the case that the cm handle does not match a condition. For more on cm handle query search please refer to <a href="RTD page to be created in separate task">cm handle query search Read the Docs</a>.<br/>By supplying a CPS Path it is possible to query on any data related to the cm handle. For more on CPS Path please refer to <a href="https://docs.onap.org/projects/onap-cps/en/latest/cps-path.html">CPS Path Read the Docs</a>. The cm handle ancestor is automatically returned for this query.
     tags:
       - network-cm-proxy
     summary: Execute cm handle query upon a given set of query parameters
@@ -390,6 +401,17 @@ searchCmHandleIds:
         application/json:
           schema:
             $ref: 'components.yaml#/components/schemas/CmHandleQueryParameters'
+          examples:
+            Cm handle properties query:
+              $ref: 'components.yaml#/components/examples/pubPropCmHandleQueryParameters'
+            Cm handle modules query:
+              $ref: 'components.yaml#/components/examples/modulesCmHandleQueryParameters'
+            All cm handle query parameters:
+              $ref: 'components.yaml#/components/examples/allCmHandleQueryParameters'
+            Cm handle with CPS path state query:
+              $ref: 'components.yaml#/components/examples/cpsPathCmHandleStateQueryParameters'
+            Cm handle with data sync flag query:
+              $ref: 'components.yaml#/components/examples/cpsPathCmHandleDataSyncQueryParameters'
     responses:
       200:
         description: OK
index 55b64ec..097dd0a 100644 (file)
@@ -56,7 +56,7 @@ public interface CmHandleStateMapper {
 
         if (compositeStateDataStore.getOperationalDataStore() != null) {
             final SyncState operationalSyncState = new SyncState();
-            operationalSyncState.setState(compositeStateDataStore.getOperationalDataStore()
+            operationalSyncState.setSyncState(compositeStateDataStore.getOperationalDataStore()
                     .getDataStoreSyncState().name());
             operationalSyncState.setLastSyncTime(compositeStateDataStore.getOperationalDataStore().getLastSyncTime());
             dataStores.setOperational(operationalSyncState);
index 23263c9..06a7759 100644 (file)
@@ -453,7 +453,7 @@ class NetworkCmProxyControllerSpec extends Specification {
             '"dataSyncEnabled":false',
             '"dataSyncState":',
             '"operational":',
-            '"state":"NONE_REQUESTED"',
+            '"syncState":"NONE_REQUESTED"',
             '"lastSyncTime":"2022-12-31T20:30:40.000+0000"',
             '"running":null'
         ]
index 677cf66..663b9d0 100644 (file)
@@ -56,7 +56,7 @@ class CmHandleStateMapperTest extends Specification {
             assert result.lockReason.reason == 'LOCKED_MISBEHAVING'
             assert result.lockReason.details == 'locked details'
             assert result.cmHandleState == 'ADVISED'
-            assert result.dataSyncState.operational.getState() != null
+            assert result.dataSyncState.operational.getSyncState() != null
     }
 
     def 'Internal to External Lock Reason Mapping of #scenario'() {
index 93c265a..d94c6d1 100644 (file)
@@ -3,7 +3,7 @@
   ============LICENSE_START=======================================================
   Copyright (C) 2021-2022 Nordix Foundation
   Modifications Copyright (C) 2021 Pantheon.tech
- Modifications Copyright (C) 2022 Bell Canada
 Modifications Copyright (C) 2022 Bell Canada
   ================================================================================
   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
             <groupId>org.onap.cps</groupId>
             <artifactId>cps-ncmp-events</artifactId>
         </dependency>
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>cps-path-parser</artifactId>
+        </dependency>
         <dependency>
             <groupId>org.springframework</groupId>
             <artifactId>spring-web</artifactId>
index a62a009..d784bcd 100644 (file)
@@ -21,6 +21,8 @@
 package org.onap.cps.ncmp.api.impl;
 
 import static org.onap.cps.ncmp.api.impl.utils.YangDataConverter.convertYangModelCmHandleToNcmpServiceCmHandle;
+import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS;
+import static org.onap.cps.utils.CmHandleQueryRestParametersValidator.validateCpsPathConditionProperties;
 import static org.onap.cps.utils.CmHandleQueryRestParametersValidator.validateModuleNameConditionProperties;
 
 import java.util.ArrayList;
@@ -31,17 +33,22 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.onap.cps.cpspath.parser.PathParsingException;
 import org.onap.cps.ncmp.api.NetworkCmProxyCmHandlerQueryService;
 import org.onap.cps.ncmp.api.impl.utils.YangDataConverter;
+import org.onap.cps.ncmp.api.inventory.CmHandleQueries;
 import org.onap.cps.ncmp.api.inventory.InventoryPersistence;
 import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle;
+import org.onap.cps.spi.exceptions.DataValidationException;
 import org.onap.cps.spi.model.Anchor;
 import org.onap.cps.spi.model.CmHandleQueryServiceParameters;
 import org.onap.cps.spi.model.ConditionProperties;
 import org.onap.cps.spi.model.DataNode;
+import org.onap.cps.utils.ValidQueryProperties;
 import org.springframework.stereotype.Service;
 
 @Service
@@ -49,9 +56,8 @@ import org.springframework.stereotype.Service;
 @RequiredArgsConstructor
 public class NetworkCmProxyCmHandlerQueryServiceImpl implements NetworkCmProxyCmHandlerQueryService {
 
-    private static final String PROPERTY_QUERY_NAME = "hasAllProperties";
-    private static final String MODULE_QUERY_NAME = "hasAllModules";
-    private static final Map<String, NcmpServiceCmHandle> NO_QUERY_EXECUTED = null;
+    private static final Map<String, NcmpServiceCmHandle> NO_QUERY_TO_EXECUTE = null;
+    private final CmHandleQueries cmHandleQueries;
     private final InventoryPersistence inventoryPersistence;
 
     /**
@@ -68,14 +74,10 @@ public class NetworkCmProxyCmHandlerQueryServiceImpl implements NetworkCmProxyCm
             return getAllCmHandles();
         }
 
-        final Map<String, NcmpServiceCmHandle> publicPropertyQueryResult
-            = executePublicPropertyQueries(cmHandleQueryServiceParameters);
+        final Map<String, NcmpServiceCmHandle> combinedQueryResult = executeInventoryQueries(
+            cmHandleQueryServiceParameters);
 
-        final Map<String, NcmpServiceCmHandle> combinedQueryResult =
-            combineWithModuleNameQuery(cmHandleQueryServiceParameters, publicPropertyQueryResult);
-
-        return combinedQueryResult == NO_QUERY_EXECUTED
-            ? Collections.emptySet() : new HashSet<>(combinedQueryResult.values());
+        return new HashSet<>(combineWithModuleNameQuery(cmHandleQueryServiceParameters, combinedQueryResult).values());
     }
 
     /**
@@ -92,52 +94,24 @@ public class NetworkCmProxyCmHandlerQueryServiceImpl implements NetworkCmProxyCm
             return getAllCmHandleIds();
         }
 
-        final Map<String, NcmpServiceCmHandle> publicPropertyQueryResult
-            = executePublicPropertyQueries(cmHandleQueryServiceParameters);
+        final Map<String, NcmpServiceCmHandle> combinedQueryResult = executeInventoryQueries(
+            cmHandleQueryServiceParameters);
 
         final Collection<String> moduleNamesForQuery =
             getModuleNamesForQuery(cmHandleQueryServiceParameters.getCmHandleQueryParameters());
         if (moduleNamesForQuery.isEmpty()) {
-            return publicPropertyQueryResult == NO_QUERY_EXECUTED
-                ? Collections.emptySet() : publicPropertyQueryResult.keySet();
+            return combinedQueryResult.keySet();
         }
         final Set<String> moduleNameQueryResult = getNamesOfAnchorsWithGivenModules(moduleNamesForQuery);
 
-        if (publicPropertyQueryResult == NO_QUERY_EXECUTED) {
+        if (combinedQueryResult == NO_QUERY_TO_EXECUTE) {
             return moduleNameQueryResult;
         }
 
-        moduleNameQueryResult.retainAll(publicPropertyQueryResult.keySet());
+        moduleNameQueryResult.retainAll(combinedQueryResult.keySet());
         return moduleNameQueryResult;
     }
 
-    private Map<String, NcmpServiceCmHandle> executePublicPropertyQueries(
-        final CmHandleQueryServiceParameters cmHandleQueryServiceParameters) {
-        final Map<String, String> publicPropertyQueryPairs =
-            getPublicPropertyPairs(cmHandleQueryServiceParameters.getCmHandleQueryParameters());
-        if (publicPropertyQueryPairs.isEmpty()) {
-            return NO_QUERY_EXECUTED;
-        }
-        Map<String, NcmpServiceCmHandle> cmHandleIdToNcmpServiceCmHandles = null;
-        for (final Map.Entry<String, String> entry : publicPropertyQueryPairs.entrySet()) {
-            final String cpsPath = "//public-properties[@name='" + entry.getKey() + "' and @value='"
-                + entry.getValue() + "']/ancestor::cm-handles";
-
-            final Collection<DataNode> dataNodes = inventoryPersistence.queryDataNodes(cpsPath);
-            if (cmHandleIdToNcmpServiceCmHandles == NO_QUERY_EXECUTED) {
-                cmHandleIdToNcmpServiceCmHandles = collectDataNodesToNcmpServiceCmHandles(dataNodes);
-            } else {
-                final Collection<String> cmHandleIdsToRetain = dataNodes.parallelStream()
-                    .map(dataNode -> dataNode.getLeaves().get("id").toString()).collect(Collectors.toSet());
-                cmHandleIdToNcmpServiceCmHandles.keySet().retainAll(cmHandleIdsToRetain);
-            }
-            if (cmHandleIdToNcmpServiceCmHandles.isEmpty()) {
-                break;
-            }
-        }
-        return cmHandleIdToNcmpServiceCmHandles;
-    }
-
     private Map<String, NcmpServiceCmHandle> combineWithModuleNameQuery(
         final CmHandleQueryServiceParameters cmHandleQueryServiceParameters,
         final Map<String, NcmpServiceCmHandle> previousQueryResult) {
@@ -151,7 +125,7 @@ public class NetworkCmProxyCmHandlerQueryServiceImpl implements NetworkCmProxyCm
             return Collections.emptyMap();
         }
         final Map<String, NcmpServiceCmHandle> queryResult = new HashMap<>(cmHandleIdsByModuleName.size());
-        if (previousQueryResult == NO_QUERY_EXECUTED) {
+        if (previousQueryResult == NO_QUERY_TO_EXECUTE) {
             cmHandleIdsByModuleName.forEach(cmHandleId ->
                     queryResult.put(cmHandleId, createNcmpServiceCmHandle(
                             inventoryPersistence.getDataNode("/dmi-registry/cm-handles[@id='" + cmHandleId + "']")))
@@ -163,19 +137,68 @@ public class NetworkCmProxyCmHandlerQueryServiceImpl implements NetworkCmProxyCm
         return queryResult;
     }
 
+    private Map<String, NcmpServiceCmHandle> executeInventoryQueries(
+        final CmHandleQueryServiceParameters cmHandleQueryServiceParameters) {
+        final Map<String, String> cpsPath = getCpsPath(cmHandleQueryServiceParameters.getCmHandleQueryParameters());
+        if (!validateCpsPathConditionProperties(cpsPath)) {
+            return Collections.emptyMap();
+        }
+        final Map<String, NcmpServiceCmHandle> cpsPathQueryResult;
+        if (cpsPath.isEmpty()) {
+            cpsPathQueryResult = NO_QUERY_TO_EXECUTE;
+        } else {
+            try {
+                cpsPathQueryResult = cmHandleQueries.getCmHandleDataNodesByCpsPath(
+                    cpsPath.get("cpsPath"), INCLUDE_ALL_DESCENDANTS)
+                    .stream().map(this::createNcmpServiceCmHandle)
+                    .collect(Collectors.toMap(NcmpServiceCmHandle::getCmHandleId,
+                        Function.identity()));
+            } catch (final PathParsingException pathParsingException) {
+                throw new DataValidationException(pathParsingException.getMessage(), pathParsingException.getDetails(),
+                    pathParsingException);
+            }
+            if (cpsPathQueryResult.isEmpty()) {
+                return Collections.emptyMap();
+            }
+        }
+
+        final Map<String, String> publicPropertyQueryPairs =
+            getPublicPropertyPairs(cmHandleQueryServiceParameters.getCmHandleQueryParameters());
+        final Map<String, NcmpServiceCmHandle> propertiesQueryResult = publicPropertyQueryPairs.isEmpty()
+            ? NO_QUERY_TO_EXECUTE : cmHandleQueries.queryCmHandlePublicProperties(publicPropertyQueryPairs);
+
+        return cmHandleQueries.combineCmHandleQueries(cpsPathQueryResult, propertiesQueryResult);
+    }
+
     private Set<String> getNamesOfAnchorsWithGivenModules(final Collection<String> moduleNamesForQuery) {
         final Collection<Anchor> anchors = inventoryPersistence.queryAnchors(moduleNamesForQuery);
         return anchors.parallelStream().map(Anchor::getName).collect(Collectors.toSet());
     }
 
-    private Map<String, NcmpServiceCmHandle> collectDataNodesToNcmpServiceCmHandles(
-        final Collection<DataNode> dataNodes) {
-        final Map<String, NcmpServiceCmHandle> cmHandleIdToNcmpServiceCmHandle = new HashMap<>();
-        dataNodes.forEach(dataNode -> {
-            final NcmpServiceCmHandle ncmpServiceCmHandle = createNcmpServiceCmHandle(dataNode);
-            cmHandleIdToNcmpServiceCmHandle.put(ncmpServiceCmHandle.getCmHandleId(), ncmpServiceCmHandle);
-        });
-        return cmHandleIdToNcmpServiceCmHandle;
+    private Collection<String> getModuleNamesForQuery(final List<ConditionProperties> conditionProperties) {
+        final List<String> result = new ArrayList<>();
+        getConditions(conditionProperties, ValidQueryProperties.HAS_ALL_MODULES.getQueryProperty())
+            .parallelStream().forEach(
+                conditionProperty -> {
+                    validateModuleNameConditionProperties(conditionProperty);
+                    result.add(conditionProperty.get("moduleName"));
+                }
+            );
+        return result;
+    }
+
+    private Map<String, String> getCpsPath(final List<ConditionProperties> conditionProperties) {
+        final Map<String, String> result = new HashMap<>();
+        getConditions(conditionProperties, ValidQueryProperties.WITH_CPS_PATH.getQueryProperty()).forEach(
+            result::putAll);
+        return result;
+    }
+
+    private Map<String, String> getPublicPropertyPairs(final List<ConditionProperties> conditionProperties) {
+        final Map<String, String> result = new HashMap<>();
+        getConditions(conditionProperties,
+            ValidQueryProperties.HAS_ALL_PROPERTIES.getQueryProperty()).forEach(result::putAll);
+        return result;
     }
 
     private List<Map<String, String>> getConditions(final List<ConditionProperties> conditionProperties,
@@ -188,23 +211,6 @@ public class NetworkCmProxyCmHandlerQueryServiceImpl implements NetworkCmProxyCm
         return Collections.emptyList();
     }
 
-    private Collection<String> getModuleNamesForQuery(final List<ConditionProperties> conditionProperties) {
-        final List<String> result = new ArrayList<>();
-        getConditions(conditionProperties, MODULE_QUERY_NAME).parallelStream().forEach(
-            conditionProperty -> {
-                validateModuleNameConditionProperties(conditionProperty);
-                result.add(conditionProperty.get("moduleName"));
-            }
-        );
-        return result;
-    }
-
-    private Map<String, String> getPublicPropertyPairs(final List<ConditionProperties> conditionProperties) {
-        final Map<String, String> result = new HashMap<>();
-        getConditions(conditionProperties, PROPERTY_QUERY_NAME).forEach(result::putAll);
-        return result;
-    }
-
     private Set<NcmpServiceCmHandle> getAllCmHandles() {
         return inventoryPersistence.getDataNode("/dmi-registry")
             .getChildDataNodes().stream().map(this::createNcmpServiceCmHandle).collect(Collectors.toSet());
index 8d32c1a..28cbf2c 100755 (executable)
@@ -271,7 +271,7 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService
                 final YangModelCmHandle yangModelCmHandle = inventoryPersistence.getYangModelCmHandle(cmHandleId);
                 lcmEventsCmHandleStateHandler.updateCmHandleState(yangModelCmHandle,
                         CmHandleState.DELETING);
-                deleteSchemaSetAndListElementByCmHandleId(cmHandleId);
+                deleteCmHandleByCmHandleId(cmHandleId);
                 cmHandleRegistrationResponses.add(CmHandleRegistrationResponse.createSuccessResponse(cmHandleId));
                 lcmEventsCmHandleStateHandler.updateCmHandleState(yangModelCmHandle,
                         CmHandleState.DELETED);
@@ -295,7 +295,7 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService
         return cmHandleRegistrationResponses;
     }
 
-    private void deleteSchemaSetAndListElementByCmHandleId(final String cmHandleId) {
+    private void deleteCmHandleByCmHandleId(final String cmHandleId) {
         inventoryPersistence.deleteSchemaSetWithCascade(cmHandleId);
         inventoryPersistence.deleteListOrListElement("/dmi-registry/cm-handles[@id='" + cmHandleId + "']");
     }
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/CmHandleQueries.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/CmHandleQueries.java
new file mode 100644 (file)
index 0000000..92387ba
--- /dev/null
@@ -0,0 +1,165 @@
+/*
+ *  ============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.inventory;
+
+import static org.onap.cps.ncmp.api.impl.utils.YangDataConverter.convertYangModelCmHandleToNcmpServiceCmHandle;
+import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import lombok.RequiredArgsConstructor;
+import org.onap.cps.ncmp.api.impl.utils.YangDataConverter;
+import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle;
+import org.onap.cps.spi.CpsDataPersistenceService;
+import org.onap.cps.spi.FetchDescendantsOption;
+import org.onap.cps.spi.model.DataNode;
+import org.springframework.stereotype.Component;
+
+@RequiredArgsConstructor
+@Component
+public class CmHandleQueries {
+
+    private static final String NCMP_DATASPACE_NAME = "NCMP-Admin";
+    private static final String NCMP_DMI_REGISTRY_ANCHOR = "ncmp-dmi-registry";
+
+    private final CpsDataPersistenceService cpsDataPersistenceService;
+    private static final Map<String, NcmpServiceCmHandle> NO_QUERY_TO_EXECUTE = null;
+    private static final String ANCESTOR_CM_HANDLES = "/ancestor::cm-handles";
+
+
+    /**
+     * Query CmHandles based on PublicProperties.
+     *
+     * @param publicPropertyQueryPairs public properties for query
+     * @return CmHandles which have these public properties
+     */
+    public Map<String, NcmpServiceCmHandle> queryCmHandlePublicProperties(
+        final Map<String, String> publicPropertyQueryPairs) {
+        if (publicPropertyQueryPairs.isEmpty()) {
+            return Collections.emptyMap();
+        }
+        Map<String, NcmpServiceCmHandle> cmHandleIdToNcmpServiceCmHandles = null;
+        for (final Map.Entry<String, String> publicPropertyQueryPair : publicPropertyQueryPairs.entrySet()) {
+            final String cpsPath = "//public-properties[@name=\"" + publicPropertyQueryPair.getKey()
+                + "\" and @value=\"" + publicPropertyQueryPair.getValue() + "\"]";
+
+            final Collection<DataNode> dataNodes = getCmHandleDataNodesByCpsPath(cpsPath, INCLUDE_ALL_DESCENDANTS);
+            if (cmHandleIdToNcmpServiceCmHandles == null) {
+                cmHandleIdToNcmpServiceCmHandles = collectDataNodesToNcmpServiceCmHandles(dataNodes);
+            } else {
+                final Collection<String> cmHandleIdsToRetain = dataNodes.parallelStream()
+                    .map(dataNode -> dataNode.getLeaves().get("id").toString()).collect(Collectors.toSet());
+                cmHandleIdToNcmpServiceCmHandles.keySet().retainAll(cmHandleIdsToRetain);
+            }
+            if (cmHandleIdToNcmpServiceCmHandles.isEmpty()) {
+                break;
+            }
+        }
+        return cmHandleIdToNcmpServiceCmHandles;
+    }
+
+    /**
+     * Combine Maps of CmHandles.
+     *
+     * @param firstQuery first CmHandles Map
+     * @param secondQuery second CmHandles Map
+     * @return combined Map of CmHandles
+     */
+    public Map<String, NcmpServiceCmHandle> combineCmHandleQueries(
+        final Map<String, NcmpServiceCmHandle> firstQuery,
+        final Map<String, NcmpServiceCmHandle> secondQuery) {
+        if (firstQuery == NO_QUERY_TO_EXECUTE && secondQuery == NO_QUERY_TO_EXECUTE) {
+            return Collections.emptyMap();
+        } else if (firstQuery == NO_QUERY_TO_EXECUTE) {
+            return secondQuery;
+        } else if (secondQuery == NO_QUERY_TO_EXECUTE) {
+            return firstQuery;
+        } else {
+            firstQuery.keySet().retainAll(secondQuery.keySet());
+            return firstQuery;
+        }
+    }
+
+    /**
+     * Method which returns cm handles by the cm handles state.
+     *
+     * @param cmHandleState cm handle state
+     * @return a list of cm handles
+     */
+    public List<DataNode> getCmHandlesByState(final CmHandleState cmHandleState) {
+        return getCmHandleDataNodesByCpsPath("//state[@cm-handle-state=\"" + cmHandleState + "\"]",
+            INCLUDE_ALL_DESCENDANTS);
+    }
+
+    /**
+     * Method to return data nodes representing the cm handles.
+     *
+     * @param cpsPath cps path for which the cmHandle is requested
+     * @return a list of data nodes representing the cm handles.
+     */
+    public List<DataNode> getCmHandleDataNodesByCpsPath(final String cpsPath,
+                                                        final FetchDescendantsOption fetchDescendantsOption) {
+        return cpsDataPersistenceService.queryDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,
+            cpsPath + ANCESTOR_CM_HANDLES, fetchDescendantsOption);
+    }
+
+    /**
+     * Method which returns cm handles by the cm handle id and state.
+     * @param cmHandleId cm handle id
+     * @param cmHandleState cm handle state
+     * @return a list of cm handles
+     */
+    public List<DataNode> getCmHandlesByIdAndState(final String cmHandleId, final CmHandleState cmHandleState) {
+        return getCmHandleDataNodesByCpsPath("//cm-handles[@id='" + cmHandleId + "']/state[@cm-handle-state=\""
+                + cmHandleState + "\"]", FetchDescendantsOption.OMIT_DESCENDANTS);
+    }
+
+    /**
+     * Method which returns cm handles by the operational sync state of cm handle.
+     * @param dataStoreSyncState sync state
+     * @return a list of cm handles
+     */
+    public List<DataNode> getCmHandlesByOperationalSyncState(final DataStoreSyncState dataStoreSyncState) {
+        return getCmHandleDataNodesByCpsPath("//state/datastores" + "/operational[@sync-state=\""
+                + dataStoreSyncState + "\"]", FetchDescendantsOption.OMIT_DESCENDANTS);
+    }
+
+    private Map<String, NcmpServiceCmHandle> collectDataNodesToNcmpServiceCmHandles(
+        final Collection<DataNode> dataNodes) {
+        final Map<String, NcmpServiceCmHandle> cmHandleIdToNcmpServiceCmHandle = new HashMap<>();
+        dataNodes.forEach(dataNode -> {
+            final NcmpServiceCmHandle ncmpServiceCmHandle = createNcmpServiceCmHandle(dataNode);
+            cmHandleIdToNcmpServiceCmHandle.put(ncmpServiceCmHandle.getCmHandleId(), ncmpServiceCmHandle);
+        });
+        return cmHandleIdToNcmpServiceCmHandle;
+    }
+
+    private NcmpServiceCmHandle createNcmpServiceCmHandle(final DataNode dataNode) {
+        return convertYangModelCmHandleToNcmpServiceCmHandle(YangDataConverter
+            .convertCmHandleToYangModel(dataNode, dataNode.getLeaves().get("id").toString()));
+    }
+}
+
+
index be26a58..14fc6d6 100644 (file)
@@ -28,7 +28,6 @@ import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS;
 
 import java.time.OffsetDateTime;
 import java.util.Collection;
-import java.util.List;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.onap.cps.api.CpsDataService;
@@ -60,8 +59,6 @@ public class InventoryPersistence {
 
     private static final String CM_HANDLE_XPATH_TEMPLATE = "/dmi-registry/cm-handles[@id='" + "%s" + "']";
 
-    private static final String ANCESTOR_CM_HANDLES = "\"]/ancestor::cm-handles";
-
     private final JsonObjectMapper jsonObjectMapper;
 
     private final CpsDataService cpsDataService;
@@ -99,57 +96,6 @@ public class InventoryPersistence {
             cmHandleJsonData, OffsetDateTime.now());
     }
 
-    /**
-     * Method which returns cm handles by the cm handles state.
-     *
-     * @param cmHandleState cm handle state
-     * @return a list of cm handles
-     */
-    public List<DataNode> getCmHandlesByState(final CmHandleState cmHandleState) {
-        return cpsDataPersistenceService.queryDataNodes(NCMP_DATASPACE_NAME,
-            NCMP_DMI_REGISTRY_ANCHOR, "//state[@cm-handle-state=\""
-                + cmHandleState + ANCESTOR_CM_HANDLES,
-            FetchDescendantsOption.OMIT_DESCENDANTS);
-    }
-
-    /**
-     * Method to return data nodes representing the cm handles.
-     *
-     * @param cpsPath cps path for which the cmHandle is requested
-     * @param fetchDescendantsOption defines the scope of data to fetch: either single node or all the descendant nodes
-     * @return a list of data nodes representing the cm handles.
-     */
-    public List<DataNode> getCmHandleDataNodesByCpsPath(final String cpsPath,
-                                                        final FetchDescendantsOption fetchDescendantsOption) {
-        return cpsDataPersistenceService.queryDataNodes(
-            NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, cpsPath, fetchDescendantsOption);
-    }
-
-    /**
-     * Method which returns cm handles by the cm handle id and state.
-     * @param cmHandleId cm handle id
-     * @param cmHandleState cm handle state
-     * @return a list of cm handles
-     */
-    public List<DataNode> getCmHandlesByIdAndState(final String cmHandleId, final CmHandleState cmHandleState) {
-        return cpsDataPersistenceService.queryDataNodes(NCMP_DATASPACE_NAME,
-            NCMP_DMI_REGISTRY_ANCHOR, "//cm-handles[@id='" + cmHandleId + "']/state[@cm-handle-state=\""
-                + cmHandleState + ANCESTOR_CM_HANDLES,
-            FetchDescendantsOption.OMIT_DESCENDANTS);
-    }
-
-    /**
-     * Method which returns cm handles by the operational sync state of cm handle.
-     * @param dataStoreSyncState sync state
-     * @return a list of cm handles
-     */
-    public List<DataNode> getCmHandlesByOperationalSyncState(final DataStoreSyncState dataStoreSyncState) {
-        return cpsDataPersistenceService.queryDataNodes(NCMP_DATASPACE_NAME,
-            NCMP_DMI_REGISTRY_ANCHOR, "//state/datastores"
-                + "/operational[@sync-state=\"" + dataStoreSyncState + ANCESTOR_CM_HANDLES,
-            FetchDescendantsOption.OMIT_DESCENDANTS);
-    }
-
     /**
      * This method retrieves DMI service name, DMI properties and the state for a given cm handle.
      * @param cmHandleId the id of the cm handle
@@ -218,17 +164,6 @@ public class InventoryPersistence {
         }
     }
 
-    /**
-     * Query data nodes via cps path.
-     *
-     * @param cpsPath cps path
-     * @return List of data nodes
-     */
-    public List<DataNode> queryDataNodes(final String cpsPath) {
-        return cpsDataPersistenceService.queryDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,
-                cpsPath, INCLUDE_ALL_DESCENDANTS);
-    }
-
     /**
      * Get data node via xpath.
      *
index 467fd8f..2b7d3c9 100644 (file)
@@ -40,6 +40,7 @@ import org.onap.cps.ncmp.api.impl.operations.DmiDataOperations;
 import org.onap.cps.ncmp.api.impl.operations.DmiOperations;
 import org.onap.cps.ncmp.api.impl.utils.YangDataConverter;
 import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle;
+import org.onap.cps.ncmp.api.inventory.CmHandleQueries;
 import org.onap.cps.ncmp.api.inventory.CmHandleState;
 import org.onap.cps.ncmp.api.inventory.CompositeState;
 import org.onap.cps.ncmp.api.inventory.DataStoreSyncState;
@@ -57,6 +58,8 @@ import org.springframework.stereotype.Service;
 public class SyncUtils {
     private final InventoryPersistence inventoryPersistence;
 
+    private final CmHandleQueries cmHandleQueries;
+
     private final DmiDataOperations dmiDataOperations;
 
     private final JsonObjectMapper jsonObjectMapper;
@@ -70,7 +73,7 @@ public class SyncUtils {
      */
     public List<YangModelCmHandle> getAdvisedCmHandles() {
         final List<DataNode> advisedCmHandlesAsDataNodeList = new ArrayList<>(
-                inventoryPersistence.getCmHandlesByState(CmHandleState.ADVISED));
+            cmHandleQueries.getCmHandlesByState(CmHandleState.ADVISED));
         log.info("Total number of fetched advised cm handle(s) is (are) {}", advisedCmHandlesAsDataNodeList.size());
         if (advisedCmHandlesAsDataNodeList.isEmpty()) {
             return Collections.emptyList();
@@ -87,16 +90,16 @@ public class SyncUtils {
      *         return null if not found
      */
     public YangModelCmHandle getAnUnSynchronizedReadyCmHandle() {
-        final List<DataNode> unSynchronizedCmHandles = inventoryPersistence
-                .getCmHandlesByOperationalSyncState(DataStoreSyncState.UNSYNCHRONIZED);
+        final List<DataNode> unSynchronizedCmHandles = cmHandleQueries
+            .getCmHandlesByOperationalSyncState(DataStoreSyncState.UNSYNCHRONIZED);
         if (unSynchronizedCmHandles.isEmpty()) {
             return null;
         }
         Collections.shuffle(unSynchronizedCmHandles);
         for (final DataNode cmHandle : unSynchronizedCmHandles) {
             final String cmHandleId = cmHandle.getLeaves().get("id").toString();
-            final List<DataNode> readyCmHandles = inventoryPersistence
-                    .getCmHandlesByIdAndState(cmHandleId, CmHandleState.READY);
+            final List<DataNode> readyCmHandles = cmHandleQueries
+                .getCmHandlesByIdAndState(cmHandleId, CmHandleState.READY);
             if (!readyCmHandles.isEmpty()) {
                 return inventoryPersistence.getYangModelCmHandle(cmHandleId);
             }
@@ -110,8 +113,8 @@ public class SyncUtils {
      * @return a random LOCKED yang model cm handle, return null if not found
      */
     public List<YangModelCmHandle> getModuleSyncFailedCmHandles() {
-        final List<DataNode> lockedCmHandlesAsDataNodeList = inventoryPersistence.getCmHandleDataNodesByCpsPath(
-            "//lock-reason[@reason=\"LOCKED_MODULE_SYNC_FAILED\"]/ancestor::cm-handles",
+        final List<DataNode> lockedCmHandlesAsDataNodeList = cmHandleQueries.getCmHandleDataNodesByCpsPath(
+            "//lock-reason[@reason=\"LOCKED_MODULE_SYNC_FAILED\"]",
             FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS);
         return convertCmHandlesDataNodesToYangModelCmHandles(lockedCmHandlesAsDataNodeList);
     }
@@ -171,8 +174,8 @@ public class SyncUtils {
      */
     public String getResourceData(final String cmHandleId) {
         final ResponseEntity<Object> resourceDataResponseEntity = dmiDataOperations.getResourceDataFromDmi(
-                cmHandleId, DmiOperations.DataStoreEnum.PASSTHROUGH_OPERATIONAL,
-                UUID.randomUUID().toString());
+            cmHandleId, DmiOperations.DataStoreEnum.PASSTHROUGH_OPERATIONAL,
+            UUID.randomUUID().toString());
         if (resourceDataResponseEntity.getStatusCode().is2xxSuccessful()) {
             return getFirstResource(resourceDataResponseEntity.getBody());
         }
@@ -188,8 +191,8 @@ public class SyncUtils {
     }
 
     private List<YangModelCmHandle> convertCmHandlesDataNodesToYangModelCmHandles(
-            final List<DataNode> cmHandlesAsDataNodeList) {
+        final List<DataNode> cmHandlesAsDataNodeList) {
         return cmHandlesAsDataNodeList.stream().map(dataNode -> YangDataConverter.convertCmHandleToYangModel(dataNode,
-                dataNode.getLeaves().get("id").toString())).collect(Collectors.toList());
+            dataNode.getLeaves().get("id").toString())).collect(Collectors.toList());
     }
 }
index 7cf572d..40ec12d 100644 (file)
 
 package org.onap.cps.ncmp.api.impl
 
+import org.onap.cps.cpspath.parser.PathParsingException
 import org.onap.cps.ncmp.api.NetworkCmProxyCmHandlerQueryService
 import org.onap.cps.ncmp.api.inventory.InventoryPersistence
+import org.onap.cps.ncmp.api.inventory.CmHandleQueries
+import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle
+import org.onap.cps.spi.FetchDescendantsOption
+import org.onap.cps.spi.exceptions.DataInUseException
+import org.onap.cps.spi.exceptions.DataValidationException
 import org.onap.cps.spi.model.Anchor
 import org.onap.cps.spi.model.CmHandleQueryServiceParameters
 import org.onap.cps.spi.model.ConditionProperties
@@ -32,87 +38,127 @@ import java.util.stream.Collectors
 
 class NetworkCmProxyCmHandlerQueryServiceSpec extends Specification {
 
+    def cmHandleQueries = Mock(CmHandleQueries)
     def inventoryPersistence = Mock(InventoryPersistence)
 
-    NetworkCmProxyCmHandlerQueryService objectUnderTest = new NetworkCmProxyCmHandlerQueryServiceImpl(inventoryPersistence)
+    def static someCmHandleDataNode = new DataNode(xpath: '/dmi-registry/cm-handles[@id=\'some-cmhandle-id\']', leaves: ['id':'some-cmhandle-id'])
+    def dmiRegistry = new DataNode(xpath: '/dmi-registry', childDataNodes: createDataNodeList(['PNFDemo1', 'PNFDemo2', 'PNFDemo3', 'PNFDemo4']))
 
-    def 'Retrieve cm handles with public properties when #scenario.'() {
-        given: 'a condition property'
+    def objectUnderTest = new NetworkCmProxyCmHandlerQueryServiceImpl(cmHandleQueries, inventoryPersistence)
+
+    def 'Retrieve cm handles with cpsPath when combined with no Module Query.'() {
+        given: 'a cmHandleWithCpsPath condition property'
             def cmHandleQueryParameters = new CmHandleQueryServiceParameters()
-            def conditionProperties = new ConditionProperties()
-            conditionProperties.conditionName = 'hasAllProperties'
-            conditionProperties.conditionParameters = publicProperties
+            def conditionProperties = createConditionProperties('cmHandleWithCpsPath', [['cpsPath' : '/some/cps/path']])
             cmHandleQueryParameters.setCmHandleQueryParameters([conditionProperties])
-        and: 'mock services'
-            mockResponses()
-        when: 'a query is execute (with and without Data)'
+        and: 'cmHandleQueries returns a non null query result'
+            cmHandleQueries.getCmHandleDataNodesByCpsPath('/some/cps/path', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> [new DataNode(leaves: ['id':'some-cmhandle-id'])]
+        and: 'CmHandleQueries returns cmHandles with the relevant query result'
+            cmHandleQueries.combineCmHandleQueries(*_) >> ['PNFDemo1': new NcmpServiceCmHandle(cmHandleId: 'PNFDemo1'), 'PNFDemo3': new NcmpServiceCmHandle(cmHandleId: 'PNFDemo3')]
+        when: 'the query is executed for both cm handle ids and details'
             def returnedCmHandlesJustIds = objectUnderTest.queryCmHandleIds(cmHandleQueryParameters)
             def returnedCmHandlesWithData = objectUnderTest.queryCmHandles(cmHandleQueryParameters)
         then: 'the correct expected cm handles ids are returned'
-            returnedCmHandlesJustIds == expectedCmHandleIds as Set
-        and: 'the correct cm handle data objects are returned'
-            returnedCmHandlesWithData.stream().map(dataNode -> dataNode.cmHandleId).collect(Collectors.toSet()) == expectedCmHandleIds as Set
+            returnedCmHandlesJustIds == ['PNFDemo1', 'PNFDemo3'] as Set
+        and: 'the correct ncmp service cm handles are returned'
+            returnedCmHandlesWithData.stream().map(CmHandle -> CmHandle.cmHandleId).collect(Collectors.toSet()) == ['PNFDemo1', 'PNFDemo3'] as Set
+    }
+
+    def 'Retrieve cm handles with cpsPath where #scenario.'() {
+        given: 'a cmHandleWithCpsPath condition property'
+            def cmHandleQueryParameters = new CmHandleQueryServiceParameters()
+            def conditionProperties = createConditionProperties('cmHandleWithCpsPath', [['cpsPath' : '/some/cps/path']])
+            cmHandleQueryParameters.setCmHandleQueryParameters([conditionProperties])
+        and: 'cmHandleQueries throws a path parsing exception'
+            cmHandleQueries.getCmHandleDataNodesByCpsPath('/some/cps/path', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> { throw thrownException }
+        when: 'the query is executed for both cm handle ids and details'
+            objectUnderTest.queryCmHandleIds(cmHandleQueryParameters)
+            objectUnderTest.queryCmHandles(cmHandleQueryParameters)
+        then: 'a data validation exception is thrown'
+            thrown(expectedException)
         where: 'the following data is used'
-            scenario                         | publicProperties                                                                                  || expectedCmHandleIds
-            'single property matches'        | [['Contact' : 'newemailforstore@bookstore.com']]                                                  || ['PNFDemo1', 'PNFDemo2', 'PNFDemo4']
-            'public property does not match' | [['wont_match' : 'wont_match']]                                                                   || []
-            '2 properties, only one match'   | [['Contact' : 'newemailforstore@bookstore.com'], ['Contact2': 'newemailforstore2@bookstore.com']] || ['PNFDemo4']
-            '2 properties, no matches'       | [['Contact' : 'newemailforstore@bookstore.com'], ['Contact2': '']]                                || []
+            scenario                           | thrownException                                          || expectedException
+            'a PathParsingException is thrown' | new PathParsingException('some message', 'some details') || DataValidationException
+            'any other Exception is thrown'    | new DataInUseException('some message', 'some details')   || DataInUseException
     }
 
-    def 'Retrieve cm handles with module names when #scenario.'() {
-        given: 'a condition property'
+    def 'Query cm handles with public properties when combined with empty modules query result.'() {
+        given: 'a public properties condition property'
             def cmHandleQueryParameters = new CmHandleQueryServiceParameters()
-            def conditionProperties = new ConditionProperties()
-            conditionProperties.conditionName = 'hasAllModules'
-            conditionProperties.conditionParameters = moduleNames
+            def conditionProperties = createConditionProperties('hasAllProperties', [['some-property-key': 'some-property-value']])
             cmHandleQueryParameters.setCmHandleQueryParameters([conditionProperties])
-        and: 'mock services'
-            mockResponses()
-        when: 'the service is invoked'
+        and: 'CmHandleQueries returns cmHandles with the relevant query result'
+            cmHandleQueries.combineCmHandleQueries(*_) >> ['PNFDemo1': new NcmpServiceCmHandle(cmHandleId: 'PNFDemo1'), 'PNFDemo3': new NcmpServiceCmHandle(cmHandleId: 'PNFDemo3')]
+        when: 'the query is executed for both cm handle ids and details'
             def returnedCmHandlesJustIds = objectUnderTest.queryCmHandleIds(cmHandleQueryParameters)
             def returnedCmHandlesWithData = objectUnderTest.queryCmHandles(cmHandleQueryParameters)
-        then: 'the correct expected cm handles are returned'
+        then: 'the correct expected cm handles ids are returned'
+            returnedCmHandlesJustIds == ['PNFDemo1', 'PNFDemo3'] as Set
+        and: 'the correct cm handle data objects are returned'
+            returnedCmHandlesWithData.stream().map(dataNode -> dataNode.cmHandleId).collect(Collectors.toSet()) == ['PNFDemo1', 'PNFDemo3'] as Set
+    }
+
+    def 'Retrieve cm handles with module names when #scenario from query.'() {
+        given: 'a modules condition property'
+            def cmHandleQueryParameters = new CmHandleQueryServiceParameters()
+            def conditionProperties = createConditionProperties('hasAllModules', [['moduleName': 'some-module-name']])
+            cmHandleQueryParameters.setCmHandleQueryParameters([conditionProperties])
+        and: 'null is returned from the state and public property queries'
+            cmHandleQueries.combineCmHandleQueries(*_) >> null
+        and: '#scenario from the modules query'
+            inventoryPersistence.queryAnchors(*_) >> returnedAnchors
+        and: 'the same cmHandles are returned from the persistence service layer'
+            returnedAnchors.size() * inventoryPersistence.getDataNode(*_) >> returnedCmHandles
+        when: 'the query is executed for both cm handle ids and details'
+            def returnedCmHandlesJustIds = objectUnderTest.queryCmHandleIds(cmHandleQueryParameters)
+            def returnedCmHandlesWithData = objectUnderTest.queryCmHandles(cmHandleQueryParameters)
+        then: 'the correct expected cm handles ids are returned'
             returnedCmHandlesJustIds == expectedCmHandleIds as Set
+        and: 'the correct cm handle data objects are returned'
             returnedCmHandlesWithData.stream().map(dataNode -> dataNode.cmHandleId).collect(Collectors.toSet()) == expectedCmHandleIds as Set
         where: 'the following data is used'
-            scenario                         | moduleNames                                                             || expectedCmHandleIds
-            'single matching module name'    | [['moduleName' : 'MODULE-NAME-001']]                                    || ['PNFDemo3', 'PNFDemo1', 'PNFDemo2']
-            'module name dont match'         | [['moduleName' : 'MODULE-NAME-004']]                                    || []
-            '2 module names, only one match' | [['moduleName' : 'MODULE-NAME-002'], ['moduleName': 'MODULE-NAME-003']] || ['PNFDemo4']
-            '2 module names, no matches'     | [['moduleName' : 'MODULE-NAME-002'], ['moduleName': 'MODULE-NAME-004']] || []
+            scenario                  | returnedAnchors                        | returnedCmHandles    || expectedCmHandleIds
+            'One anchor returned'     | [new Anchor(name: 'some-cmhandle-id')] | someCmHandleDataNode || ['some-cmhandle-id']
+            'No anchors are returned' | []                                     | null                 || []
     }
 
     def 'Retrieve cm handles with combined queries when #scenario.'() {
-        given: 'condition properties'
+        given: 'all condition properties used'
             def cmHandleQueryParameters = new CmHandleQueryServiceParameters()
-            def conditionProperties1 = new ConditionProperties()
-            conditionProperties1.conditionName = 'hasAllProperties'
-            conditionProperties1.conditionParameters = publicProperties
-            def conditionProperties2 = new ConditionProperties()
-            conditionProperties2.conditionName = 'hasAllModules'
-            conditionProperties2.conditionParameters = moduleNames
-            cmHandleQueryParameters.setCmHandleQueryParameters([conditionProperties1,conditionProperties2])
-        and: 'mock services'
-            mockResponses()
-        when: 'the service is invoked'
+            def conditionPubProps = createConditionProperties('hasAllProperties', [['some-property-key': 'some-property-value']])
+            def conditionModules = createConditionProperties('hasAllModules', [['moduleName': 'some-module-name']])
+            def conditionState = createConditionProperties('cmHandleWithCpsPath', [['cpsPath' : '/some/cps/path']])
+            cmHandleQueryParameters.setCmHandleQueryParameters([conditionPubProps, conditionModules, conditionState])
+        and: 'cmHandles are returned from the state and public property combined queries'
+            cmHandleQueries.combineCmHandleQueries(*_) >> combinedQueryMap
+        and: 'cmHandles are returned from the module names query'
+            inventoryPersistence.queryAnchors(['some-module-name']) >> anchorsForModuleQuery
+        and: 'cmHandleQueries returns a datanode result'
+            2 * cmHandleQueries.getCmHandleDataNodesByCpsPath(*_) >> [someCmHandleDataNode]
+        when: 'the query is executed for both cm handle ids and details'
             def returnedCmHandlesJustIds = objectUnderTest.queryCmHandleIds(cmHandleQueryParameters)
             def returnedCmHandlesWithData = objectUnderTest.queryCmHandles(cmHandleQueryParameters)
-        then: 'the correct expected cm handles are returned'
+        then: 'the correct expected cm handles ids are returned'
             returnedCmHandlesJustIds == expectedCmHandleIds as Set
-            returnedCmHandlesWithData.stream().map(d -> d.cmHandleId).collect(Collectors.toSet()) == expectedCmHandleIds as Set
+        and: 'the correct cm handle data objects are returned'
+            returnedCmHandlesWithData.stream().map(dataNode -> dataNode.cmHandleId).collect(Collectors.toSet()) == expectedCmHandleIds as Set
         where: 'the following data is used'
-            scenario                 | moduleNames                          | publicProperties                                   || expectedCmHandleIds
-            'particularly intersect' | [['moduleName' : 'MODULE-NAME-001']] | [['Contact' : 'newemailforstore@bookstore.com']]   || ['PNFDemo1', 'PNFDemo2']
-            'empty intersect'        | [['moduleName' : 'MODULE-NAME-004']] | [['Contact' : 'newemailforstore@bookstore.com']]   || []
-            'total intersect'        | [['moduleName' : 'MODULE-NAME-002']] | [['Contact2' : 'newemailforstore2@bookstore.com']] || ['PNFDemo4']
+            scenario                                 | combinedQueryMap                                                                                                           | anchorsForModuleQuery                                        || expectedCmHandleIds
+            'combined and modules queries intersect' | ['PNFDemo1' : new NcmpServiceCmHandle(cmHandleId:'PNFDemo1')]                                                              | [new Anchor(name: 'PNFDemo1'), new Anchor(name: 'PNFDemo2')] || ['PNFDemo1']
+            'only module query results exist'        | [:]                                                                                                                        | [new Anchor(name: 'PNFDemo1'), new Anchor(name: 'PNFDemo2')] || []
+            'only combined query results exist'      | ['PNFDemo1' : new NcmpServiceCmHandle(cmHandleId:'PNFDemo1'), 'PNFDemo2' : new NcmpServiceCmHandle(cmHandleId:'PNFDemo2')] | []                                                           || []
+            'neither queries return results'         | [:]                                                                                                                        | []                                                           || []
+            'none intersect'                         | ['PNFDemo1' : new NcmpServiceCmHandle(cmHandleId:'PNFDemo1')]                                                              | [new Anchor(name: 'PNFDemo2')]                               || []
     }
 
     def 'Retrieve cm handles when the query is empty.'() {
-        given: 'mock services'
-            mockResponses()
-        when: 'the service is invoked'
+        given: 'We use an empty query'
             def cmHandleQueryParameters = new CmHandleQueryServiceParameters()
+        and: 'the inventory persistence returns the dmi registry datanode'
+            inventoryPersistence.getDataNode("/dmi-registry") >> dmiRegistry
+        and: 'the inventory persistence returns anchors for get anchors'
+            inventoryPersistence.getAnchors() >> [new Anchor(name: 'PNFDemo1'), new Anchor(name: 'PNFDemo2'), new Anchor(name: 'PNFDemo3'), new Anchor(name: 'PNFDemo4')]
+        when: 'the query is executed for both cm handle ids and details'
             def returnedCmHandlesJustIds = objectUnderTest.queryCmHandleIds(cmHandleQueryParameters)
             def returnedCmHandlesWithData = objectUnderTest.queryCmHandles(cmHandleQueryParameters)
         then: 'the correct expected cm handles are returned'
@@ -120,43 +166,13 @@ class NetworkCmProxyCmHandlerQueryServiceSpec extends Specification {
             returnedCmHandlesWithData.stream().map(d -> d.cmHandleId).collect(Collectors.toSet()) == ['PNFDemo1', 'PNFDemo2', 'PNFDemo3', 'PNFDemo4'] as Set
     }
 
-    void mockResponses() {
-        def pNFDemo1 = new DataNode(xpath: '/dmi-registry/cm-handles[@id=\'PNFDemo1\']', leaves: ['id':'PNFDemo1'])
-        def pNFDemo2 = new DataNode(xpath: '/dmi-registry/cm-handles[@id=\'PNFDemo2\']', leaves: ['id':'PNFDemo2'])
-        def pNFDemo3 = new DataNode(xpath: '/dmi-registry/cm-handles[@id=\'PNFDemo3\']', leaves: ['id':'PNFDemo3'])
-        def pNFDemo4 = new DataNode(xpath: '/dmi-registry/cm-handles[@id=\'PNFDemo4\']', leaves: ['id':'PNFDemo4'])
-        def dmiRegistry = new DataNode(xpath: '/dmi-registry', childDataNodes: [pNFDemo1, pNFDemo2, pNFDemo3, pNFDemo4])
-
-        inventoryPersistence.queryDataNodes('//public-properties[@name=\'Contact\' and @value=\'newemailforstore@bookstore.com\']/ancestor::cm-handles')
-                >> [pNFDemo1, pNFDemo2, pNFDemo4]
-        inventoryPersistence.queryDataNodes('//public-properties[@name=\'wont_match\' and @value=\'wont_match\']/ancestor::cm-handles')
-                >> []
-        inventoryPersistence.queryDataNodes('//public-properties[@name=\'Contact2\' and @value=\'newemailforstore2@bookstore.com\']/ancestor::cm-handles')
-                >> [pNFDemo4]
-        inventoryPersistence.queryDataNodes('//public-properties[@name=\'Contact2\' and @value=\'\']/ancestor::cm-handles')
-                >> []
-        inventoryPersistence.queryDataNodes('//public-properties/ancestor::cm-handles')
-                >> [pNFDemo1, pNFDemo2, pNFDemo3, pNFDemo4]
-
-        inventoryPersistence.queryDataNodes('//cm-handles[@id=\'PNFDemo\']') >> [pNFDemo1]
-        inventoryPersistence.queryDataNodes('//cm-handles[@id=\'PNFDemo2\']') >> [pNFDemo2]
-        inventoryPersistence.queryDataNodes('//cm-handles[@id=\'PNFDemo3\']') >> [pNFDemo3]
-        inventoryPersistence.queryDataNodes('//cm-handles[@id=\'PNFDemo4\']') >> [pNFDemo4]
-
-        inventoryPersistence.getDataNode('/dmi-registry') >> dmiRegistry
-
-        inventoryPersistence.getDataNode('/dmi-registry/cm-handles[@id=\'PNFDemo1\']') >> pNFDemo1
-        inventoryPersistence.getDataNode('/dmi-registry/cm-handles[@id=\'PNFDemo2\']') >> pNFDemo2
-        inventoryPersistence.getDataNode('/dmi-registry/cm-handles[@id=\'PNFDemo3\']') >> pNFDemo3
-        inventoryPersistence.getDataNode('/dmi-registry/cm-handles[@id=\'PNFDemo4\']') >> pNFDemo4
+    def createConditionProperties(String conditionName, List<Map<String, String>> conditionParameters) {
+        return new ConditionProperties(conditionName : conditionName, conditionParameters : conditionParameters)
+    }
 
-        inventoryPersistence.queryAnchors(['MODULE-NAME-001']) >> [new Anchor(name: 'PNFDemo1'), new Anchor(name: 'PNFDemo2'), new Anchor(name: 'PNFDemo3')]
-        inventoryPersistence.queryAnchors(['MODULE-NAME-004']) >> []
-        inventoryPersistence.queryAnchors(['MODULE-NAME-003', 'MODULE-NAME-002']) >> [new Anchor(name: 'PNFDemo4')]
-        inventoryPersistence.queryAnchors(['MODULE-NAME-002', 'MODULE-NAME-003']) >> [new Anchor(name: 'PNFDemo4')]
-        inventoryPersistence.queryAnchors(['MODULE-NAME-004', 'MODULE-NAME-002']) >> []
-        inventoryPersistence.queryAnchors(['MODULE-NAME-002', 'MODULE-NAME-004']) >> []
-        inventoryPersistence.queryAnchors(['MODULE-NAME-002']) >> [new Anchor(name: 'PNFDemo2'), new Anchor(name: 'PNFDemo4')]
-        inventoryPersistence.getAnchors() >> [new Anchor(name: 'PNFDemo1'), new Anchor(name: 'PNFDemo2'), new Anchor(name: 'PNFDemo3'), new Anchor(name: 'PNFDemo4')]
+    def static createDataNodeList(dataNodeIds) {
+        def dataNodes =[]
+        dataNodeIds.forEach(id -> {dataNodes.add(new DataNode(xpath: '/dmi-registry/cm-handles[@id=\'' + id + '\']', leaves: ['id':id]))})
+        return dataNodes
     }
 }
diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/CmHandleQueriesSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/CmHandleQueriesSpec.groovy
new file mode 100644 (file)
index 0000000..10a5d62
--- /dev/null
@@ -0,0 +1,150 @@
+/*
+ *  ============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.inventory
+
+import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle
+import org.onap.cps.spi.CpsDataPersistenceService
+import org.onap.cps.spi.model.DataNode
+import spock.lang.Shared
+import spock.lang.Specification
+
+import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
+import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
+
+class CmHandleQueriesSpec extends Specification {
+    def cpsDataPersistenceService = Mock(CpsDataPersistenceService)
+
+    def objectUnderTest = new CmHandleQueries(cpsDataPersistenceService)
+
+    @Shared
+    def static sampleDataNodes = [new DataNode()]
+
+    def static pnfDemo = createDataNode('PNFDemo')
+    def static pnfDemo2 = createDataNode('PNFDemo2')
+    def static pnfDemo3 = createDataNode('PNFDemo3')
+    def static pnfDemo4 = createDataNode('PNFDemo4')
+
+    def static pnfDemoCmHandle = new NcmpServiceCmHandle(cmHandleId: 'PNFDemo')
+    def static pnfDemo2CmHandle = new NcmpServiceCmHandle(cmHandleId: 'PNFDemo2')
+    def static pnfDemo3CmHandle = new NcmpServiceCmHandle(cmHandleId: 'PNFDemo3')
+
+    def 'Query CmHandles with public properties query pair.'() {
+        given: 'the DataNodes queried for a given cpsPath are returned from the persistence service.'
+            mockResponses()
+        when: 'a query on cmhandle public properties is performed with a public property pair'
+            def returnedCmHandlesWithData = objectUnderTest.queryCmHandlePublicProperties(publicPropertyPairs)
+        then: 'the correct cm handle data objects are returned'
+            returnedCmHandlesWithData.keySet().containsAll(expectedCmHandleIds)
+            returnedCmHandlesWithData.keySet().size() == expectedCmHandleIds.size()
+        where: 'the following data is used'
+            scenario                         | publicPropertyPairs                                                                           || expectedCmHandleIds
+            'single property matches'        | ['Contact' : 'newemailforstore@bookstore.com']                                                || ['PNFDemo', 'PNFDemo2', 'PNFDemo4']
+            'public property does not match' | ['wont_match' : 'wont_match']                                                                 || []
+            '2 properties, only one match'   | ['Contact' : 'newemailforstore@bookstore.com', 'Contact2': 'newemailforstore2@bookstore.com'] || ['PNFDemo4']
+            '2 properties, no matches'       | ['Contact' : 'newemailforstore@bookstore.com', 'Contact2': '']                                || []
+    }
+
+    def 'Query CmHandles using empty public properties query pair.'() {
+        when: 'a query on CmHandle public properties is executed using an empty map'
+            def returnedCmHandlesWithData = objectUnderTest.queryCmHandlePublicProperties([:])
+        then: 'no cm handles are returned'
+            returnedCmHandlesWithData.keySet().size() == 0
+    }
+
+    def 'Combine two query results where #scenario.'() {
+        when: 'two query results in the form of a map of NcmpServiceCmHandles are combined into a single query result'
+            def result = objectUnderTest.combineCmHandleQueries(firstQuery, secondQuery)
+        then: 'the returned result is the same as the expected result'
+            result == expectedResult
+        where:
+            scenario                                                     | firstQuery                                                 | secondQuery                                                || expectedResult
+            'two queries with unique and non unique entries exist'       | ['PNFDemo': pnfDemoCmHandle, 'PNFDemo2': pnfDemo2CmHandle] | ['PNFDemo': pnfDemoCmHandle, 'PNFDemo3': pnfDemo3CmHandle] || ['PNFDemo': pnfDemoCmHandle]
+            'the first query contains entries and second query is empty' | ['PNFDemo': pnfDemoCmHandle, 'PNFDemo2': pnfDemo2CmHandle] | [:]                                                        || [:]
+            'the second query contains entries and first query is empty' | [:]                                                        | ['PNFDemo': pnfDemoCmHandle, 'PNFDemo3': pnfDemo3CmHandle] || [:]
+            'the first query contains entries and second query is null'  | ['PNFDemo': pnfDemoCmHandle, 'PNFDemo2': pnfDemo2CmHandle] | null                                                       || ['PNFDemo': pnfDemoCmHandle, 'PNFDemo2': pnfDemo2CmHandle]
+            'the second query contains entries and first query is null'  | null                                                       | ['PNFDemo': pnfDemoCmHandle, 'PNFDemo3': pnfDemo3CmHandle] || ['PNFDemo': pnfDemoCmHandle, 'PNFDemo3': pnfDemo3CmHandle]
+            'both queries are empty'                                     | [:]                                                        | [:]                                                        || [:]
+            'both queries are null'                                      | null                                                       | null                                                       || [:]
+    }
+
+    def 'Get Cm Handles By State'() {
+        given: 'a cm handle state to query'
+            def cmHandleState = CmHandleState.ADVISED
+        and: 'the persistence service returns a list of data nodes'
+            cpsDataPersistenceService.queryDataNodes('NCMP-Admin', 'ncmp-dmi-registry',
+                '//state[@cm-handle-state="ADVISED"]/ancestor::cm-handles', INCLUDE_ALL_DESCENDANTS) >> sampleDataNodes
+        when: 'cm handles are fetched by state'
+            def result = objectUnderTest.getCmHandlesByState(cmHandleState)
+        then: 'the returned result matches the result from the persistence service'
+            assert result == sampleDataNodes
+    }
+
+    def 'Get Cm Handles By State and Cm-Handle Id'() {
+        given: 'a cm handle state to query'
+            def cmHandleState = CmHandleState.READY
+        and: 'cps data service returns a list of data nodes'
+            cpsDataPersistenceService.queryDataNodes('NCMP-Admin', 'ncmp-dmi-registry',
+                '//cm-handles[@id=\'some-cm-handle\']/state[@cm-handle-state="'+ 'READY'+'"]/ancestor::cm-handles', OMIT_DESCENDANTS) >> sampleDataNodes
+        when: 'cm handles are fetched by state and id'
+            def result = objectUnderTest.getCmHandlesByIdAndState('some-cm-handle', cmHandleState)
+        then: 'the returned result is a list of data nodes returned by cps data service'
+            assert result == sampleDataNodes
+    }
+
+    def 'Get Cm Handles By Operational Sync State : UNSYNCHRONIZED'() {
+        given: 'a cm handle state to query'
+            def cmHandleState = CmHandleState.READY
+        and: 'cps data service returns a list of data nodes'
+            cpsDataPersistenceService.queryDataNodes('NCMP-Admin', 'ncmp-dmi-registry',
+                '//state/datastores/operational[@sync-state="'+'UNSYNCHRONIZED'+'"]/ancestor::cm-handles', OMIT_DESCENDANTS) >> sampleDataNodes
+        when: 'cm handles are fetched by the UNSYNCHRONIZED operational sync state'
+            def result = objectUnderTest.getCmHandlesByOperationalSyncState(DataStoreSyncState.UNSYNCHRONIZED)
+        then: 'the returned result is a list of data nodes returned by cps data service'
+            assert result == sampleDataNodes
+    }
+
+    def 'Retrieve cm handle by cps path '() {
+        given: 'a cm handle state to query based on the cps path'
+            def cmHandleDataNode = new DataNode(xpath: 'xpath', leaves: ['cm-handle-state': 'LOCKED'])
+            def cpsPath = '//cps-path'
+        and: 'cps data service returns a valid data node'
+            cpsDataPersistenceService.queryDataNodes('NCMP-Admin', 'ncmp-dmi-registry',
+                cpsPath + '/ancestor::cm-handles', INCLUDE_ALL_DESCENDANTS)
+                >> Arrays.asList(cmHandleDataNode)
+        when: 'get cm handles by cps path is invoked'
+            def result = objectUnderTest.getCmHandleDataNodesByCpsPath(cpsPath, INCLUDE_ALL_DESCENDANTS)
+        then: 'the returned result is a list of data nodes returned by cps data service'
+            assert result.contains(cmHandleDataNode)
+    }
+
+    void mockResponses() {
+        cpsDataPersistenceService.queryDataNodes(_, _, '//public-properties[@name=\"Contact\" and @value=\"newemailforstore@bookstore.com\"]/ancestor::cm-handles', _) >> [pnfDemo, pnfDemo2, pnfDemo4]
+        cpsDataPersistenceService.queryDataNodes(_, _, '//public-properties[@name=\"wont_match\" and @value=\"wont_match\"]/ancestor::cm-handles', _) >> []
+        cpsDataPersistenceService.queryDataNodes(_, _, '//public-properties[@name=\"Contact2\" and @value=\"newemailforstore2@bookstore.com\"]/ancestor::cm-handles', _) >> [pnfDemo4]
+        cpsDataPersistenceService.queryDataNodes(_, _, '//public-properties[@name=\"Contact2\" and @value=\"\"]/ancestor::cm-handles', _) >> []
+        cpsDataPersistenceService.queryDataNodes(_, _, '//state[@cm-handle-state=\"READY\"]/ancestor::cm-handles', _) >> [pnfDemo, pnfDemo3]
+        cpsDataPersistenceService.queryDataNodes(_, _, '//state[@cm-handle-state=\"LOCKED\"]/ancestor::cm-handles', _) >> [pnfDemo2, pnfDemo4]
+    }
+
+    def static createDataNode(dataNodeId) {
+        return new DataNode(xpath: '/dmi-registry/cm-handles[@id=\'' + dataNodeId + '\']', leaves: ['id':dataNodeId])
+    }
+}
index 7ac231c..f9ca676 100644 (file)
@@ -80,9 +80,6 @@ class InventoryPersistenceSpec extends Specification {
     @Shared
     def childDataNodesForCmHandleWithState = [new DataNode(xpath: "/dmi-registry/cm-handles[@id='some-cm-handle']/state", leaves: ['cm-handle-state': 'ADVISED'])]
 
-    @Shared
-    def static sampleDataNodes = [new DataNode()]
-
     def "Retrieve CmHandle using datanode with #scenario."() {
         given: 'the cps data service returns a data node from the DMI registry'
             def dataNode = new DataNode(childDataNodes:childDataNodes, leaves: leaves)
@@ -157,56 +154,6 @@ class InventoryPersistenceSpec extends Specification {
             'DELETING'  | CmHandleState.DELETING || '{"state":{"cm-handle-state":"DELETING","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
     }
 
-    def 'Get Cm Handles By State'() {
-        given: 'a cm handle state to query'
-            def cmHandleState = CmHandleState.ADVISED
-        and: 'cps data service returns a list of data nodes'
-            mockCpsDataPersistenceService.queryDataNodes('NCMP-Admin', 'ncmp-dmi-registry',
-                    '//state[@cm-handle-state="ADVISED"]/ancestor::cm-handles', OMIT_DESCENDANTS) >> sampleDataNodes
-        when: 'get cm handles by state is invoked'
-            def result = objectUnderTest.getCmHandlesByState(cmHandleState)
-        then: 'the returned result is a list of data nodes returned by cps data service'
-            assert result == sampleDataNodes
-    }
-
-    def 'Get Cm Handles By State and Cm-Handle Id'() {
-        given: 'a cm handle state to query'
-            def cmHandleState = CmHandleState.READY
-        and: 'cps data service returns a list of data nodes'
-            mockCpsDataPersistenceService.queryDataNodes('NCMP-Admin', 'ncmp-dmi-registry',
-                    '//cm-handles[@id=\'some-cm-handle\']/state[@cm-handle-state="'+ 'READY'+'"]/ancestor::cm-handles', OMIT_DESCENDANTS) >> sampleDataNodes
-        when: 'get cm handles by state and id is invoked'
-            def result = objectUnderTest.getCmHandlesByIdAndState(cmHandleId, cmHandleState)
-        then: 'the returned result is a list of data nodes returned by cps data service'
-            assert result == sampleDataNodes
-    }
-
-    def 'Get Cm Handles By Operational Sync State : UNSYNCHRONIZED'() {
-        given: 'a cm handle state to query'
-            def cmHandleState = CmHandleState.READY
-        and: 'cps data service returns a list of data nodes'
-            mockCpsDataPersistenceService.queryDataNodes('NCMP-Admin', 'ncmp-dmi-registry',
-                    '//state/datastores/operational[@sync-state="'+'UNSYNCHRONIZED'+'"]/ancestor::cm-handles', OMIT_DESCENDANTS) >> sampleDataNodes
-        when: 'get cm handles by operational sync state as UNSYNCHRONIZED is invoked'
-            def result = objectUnderTest.getCmHandlesByOperationalSyncState(DataStoreSyncState.UNSYNCHRONIZED)
-        then: 'the returned result is a list of data nodes returned by cps data service'
-            assert result == sampleDataNodes
-    }
-
-    def 'Retrieve cm handle by cps path '() {
-        given: 'a cm handle state to query based on the cps path'
-            def cmHandleDataNode = new DataNode(xpath: 'xpath', leaves: ['cm-handle-state': 'LOCKED'])
-            def cpsPath = '//cps-path'
-        and: 'cps data service returns a valid data node'
-            mockCpsDataPersistenceService.queryDataNodes('NCMP-Admin', 'ncmp-dmi-registry',
-                    cpsPath, INCLUDE_ALL_DESCENDANTS)
-                    >> Arrays.asList(cmHandleDataNode)
-        when: 'get cm handles by cps path is invoked'
-            def result = objectUnderTest.getCmHandleDataNodesByCpsPath(cpsPath, INCLUDE_ALL_DESCENDANTS)
-        then: 'the returned result is a list of data nodes returned by cps data service'
-            assert result.contains(cmHandleDataNode)
-    }
-
     def 'Get module definitions'() {
         given: 'cps module service returns a collection of module definitions'
             def moduleDefinitions = [new ModuleDefinition('moduleName','revision','content')]
@@ -263,13 +210,6 @@ class InventoryPersistenceSpec extends Specification {
             0 * mockCpsModuleService.deleteSchemaSet('NFP-Operational', 'sampleSchemaSetName', CascadeDeleteAllowed.CASCADE_DELETE_ALLOWED)
     }
 
-    def 'Query data nodes via cpsPath'() {
-        when: 'the method to query data nodes is called'
-            objectUnderTest.queryDataNodes('sample cpsPath')
-        then: 'the data persistence service method to query data nodes is invoked once'
-            1 * mockCpsDataPersistenceService.queryDataNodes('NCMP-Admin','ncmp-dmi-registry','sample cpsPath', INCLUDE_ALL_DESCENDANTS)
-    }
-
     def 'Get data node via xPath'() {
         when: 'the method to get data nodes is called'
             objectUnderTest.getDataNode('sample xPath')
index 6c2d8f1..82e9d33 100644 (file)
@@ -25,6 +25,7 @@ import com.fasterxml.jackson.databind.JsonNode
 import com.fasterxml.jackson.databind.ObjectMapper
 import org.onap.cps.ncmp.api.impl.operations.DmiDataOperations
 import org.onap.cps.ncmp.api.impl.operations.DmiOperations
+import org.onap.cps.ncmp.api.inventory.CmHandleQueries
 import org.onap.cps.ncmp.api.inventory.CmHandleState
 import org.onap.cps.ncmp.api.inventory.CompositeState
 import org.onap.cps.ncmp.api.inventory.CompositeStateBuilder
@@ -47,11 +48,13 @@ class SyncUtilsSpec extends Specification{
 
     def mockInventoryPersistence = Mock(InventoryPersistence)
 
+    def mockCmHandleQueries = Mock(CmHandleQueries)
+
     def mockDmiDataOperations = Mock(DmiDataOperations)
 
     def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
 
-    def objectUnderTest = new SyncUtils(mockInventoryPersistence, mockDmiDataOperations, jsonObjectMapper)
+    def objectUnderTest = new SyncUtils(mockInventoryPersistence, mockCmHandleQueries, mockDmiDataOperations, jsonObjectMapper)
 
     @Shared
     def formattedDateAndTime = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ").format(OffsetDateTime.now())
@@ -61,14 +64,14 @@ class SyncUtilsSpec extends Specification{
 
     def 'Get an advised Cm-Handle where ADVISED cm handle #scenario'() {
         given: 'the inventory persistence service returns a collection of data nodes'
-            mockInventoryPersistence.getCmHandlesByState(CmHandleState.ADVISED) >> dataNodeCollection
+            mockCmHandleQueries.getCmHandlesByState(CmHandleState.ADVISED) >> dataNodeCollection
         when: 'get advised cm handles are fetched'
             def yangModelCmHandles = objectUnderTest.getAdvisedCmHandles()
         then: 'the returned data node collection is the correct size'
             yangModelCmHandles.size() == expectedDataNodeSize
         and: 'yang model collection contains the correct data'
             yangModelCmHandles.stream().map(yangModel -> yangModel.id).collect(Collectors.toSet()) ==
-                    dataNodeCollection.stream().map(dataNode -> dataNode.leaves.get("id")).collect(Collectors.toSet())
+                dataNodeCollection.stream().map(dataNode -> dataNode.leaves.get("id")).collect(Collectors.toSet())
         where: 'the following scenarios are used'
             scenario         | dataNodeCollection || expectedCallsToGetYangModelCmHandle | expectedDataNodeSize
             'exists'         | [dataNode]         || 1                                   | 1
@@ -77,7 +80,7 @@ class SyncUtilsSpec extends Specification{
 
     def 'Update Lock Reason, Details and Attempts where lock reason #scenario'() {
         given: 'A locked state'
-           def compositeState = new CompositeState(lockReason: lockReason)
+            def compositeState = new CompositeState(lockReason: lockReason)
         when: 'update cm handle details and attempts is called'
             objectUnderTest.updateLockReasonDetailsAndAttempts(compositeState, LockReasonCategory.LOCKED_MODULE_SYNC_FAILED, 'new error message')
         then: 'the composite state lock reason and details are updated'
@@ -91,9 +94,9 @@ class SyncUtilsSpec extends Specification{
 
     def 'Get all locked Cm-Handle where Lock Reason is LOCKED_MODULE_SYNC_FAILED cm handle #scenario'() {
         given: 'the cps (persistence service) returns a collection of data nodes'
-            mockInventoryPersistence.getCmHandleDataNodesByCpsPath(
-                    '//lock-reason[@reason="LOCKED_MODULE_SYNC_FAILED"]/ancestor::cm-handles',
-                FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> [dataNode ]
+            mockCmHandleQueries.getCmHandleDataNodesByCpsPath(
+                '//lock-reason[@reason="LOCKED_MODULE_SYNC_FAILED"]',
+                FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> [dataNode]
         when: 'get locked Misbehaving cm handle is called'
             def result = objectUnderTest.getModuleSyncFailedCmHandles()
         then: 'the returned cm handle collection is the correct size'
@@ -119,8 +122,8 @@ class SyncUtilsSpec extends Specification{
 
     def 'Get a Cm-Handle where Operational Sync state is UnSynchronized and Cm-handle state is READY and #scenario'() {
         given: 'the inventory persistence service returns a collection of data nodes'
-            mockInventoryPersistence.getCmHandlesByOperationalSyncState(DataStoreSyncState.UNSYNCHRONIZED) >> unSynchronizedDataNodes
-            mockInventoryPersistence.getCmHandlesByIdAndState("cm-handle-123", CmHandleState.READY) >> readyDataNodes
+            mockCmHandleQueries.getCmHandlesByOperationalSyncState(DataStoreSyncState.UNSYNCHRONIZED) >> unSynchronizedDataNodes
+            mockCmHandleQueries.getCmHandlesByIdAndState("cm-handle-123", CmHandleState.READY) >> readyDataNodes
         when: 'get advised cm handles are fetched'
             objectUnderTest.getAnUnSynchronizedReadyCmHandle()
         then: 'the returned data node collection is the correct size'
index c3811eb..7fe47be 100644 (file)
@@ -22,18 +22,17 @@ package org.onap.cps.utils;
 
 import com.google.common.base.Strings;
 import java.util.Arrays;
-import java.util.List;
 import java.util.Map;
 import lombok.AccessLevel;
 import lombok.NoArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
 import org.onap.cps.spi.exceptions.DataValidationException;
 import org.onap.cps.spi.model.CmHandleQueryServiceParameters;
 
+@Slf4j
 @NoArgsConstructor(access = AccessLevel.PRIVATE)
 public class CmHandleQueryRestParametersValidator {
 
-    private static final List<String> VALID_PROPERTY_NAMES = Arrays.asList("hasAllProperties", "hasAllModules");
-
     /**
      * Validate cm handle query parameters.
      * @param cmHandleQueryServiceParameters name of data to be validated
@@ -45,7 +44,8 @@ public class CmHandleQueryRestParametersValidator {
                     if (Strings.isNullOrEmpty(conditionApiProperty.getConditionName())) {
                         throwDataValidationException("Missing 'conditionName' - please supply a valid name.");
                     }
-                    if (!VALID_PROPERTY_NAMES.contains(conditionApiProperty.getConditionName())) {
+                    if (Arrays.stream(ValidQueryProperties.values()).noneMatch(validQueryProperty ->
+                        validQueryProperty.getQueryProperty().equals(conditionApiProperty.getConditionName()))) {
                         throwDataValidationException(
                                 String.format("Wrong 'conditionName': %s - please supply a valid name.",
                                 conditionApiProperty.getConditionName()));
@@ -89,6 +89,34 @@ public class CmHandleQueryRestParametersValidator {
         throwDataValidationException("Wrong module condition property. - please supply a valid condition property.");
     }
 
+    /**
+     * Validate CPS path condition properties.
+     * @param conditionProperty name of data to be validated
+     */
+    public static boolean validateCpsPathConditionProperties(final Map<String, String> conditionProperty) {
+        if (conditionProperty.isEmpty()) {
+            return true;
+        }
+        if (conditionProperty.size() > 1) {
+            throwDataValidationException("Only one condition property is allowed for the CPS path query.");
+        }
+        if (!conditionProperty.containsKey("cpsPath")) {
+            throwDataValidationException(
+                "Wrong CPS path condition property. - expecting \"cpsPath\" as the condition property.");
+        }
+        final String cpsPath = conditionProperty.get("cpsPath");
+        if (cpsPath.isBlank()) {
+            throwDataValidationException(
+                "Wrong CPS path. - please supply a valid CPS path.");
+        }
+        if (cpsPath.contains("/additional-properties")) {
+            log.debug("{} - Private metadata cannot be queried. Nothing to be returned",
+                cpsPath);
+            return false;
+        }
+        return true;
+    }
+
     private static void throwDataValidationException(final String details) {
         throw new DataValidationException("Invalid Query Parameter.", details);
     }
diff --git a/cps-service/src/main/java/org/onap/cps/utils/ValidQueryProperties.java b/cps-service/src/main/java/org/onap/cps/utils/ValidQueryProperties.java
new file mode 100644 (file)
index 0000000..1d7ccb9
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ *  ============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.utils;
+
+import lombok.Getter;
+
+@Getter
+public enum ValidQueryProperties {
+    HAS_ALL_PROPERTIES("hasAllProperties"),
+    HAS_ALL_MODULES("hasAllModules"),
+    WITH_CPS_PATH("cmHandleWithCpsPath");
+
+    private final String queryProperty;
+
+    ValidQueryProperties(final String queryProperty) {
+        this.queryProperty = queryProperty;
+    }
+}
index a9b04c1..d5dcb7f 100644 (file)
@@ -88,4 +88,29 @@ class CmHandleQueryRestParametersValidatorSpec extends Specification {
             'invalid value' | [moduleName: '']
             'invalid name'  | [wrongName: 'value']
     }
+
+    def 'Validate CmHandle where an exception is thrown due to #scenario.'() {
+        when: 'the validator is called on a cps path condition property'
+            CmHandleQueryRestParametersValidator.validateCpsPathConditionProperties(conditionProperty)
+        then: 'a data validation exception is thrown'
+            def e = thrown(DataValidationException)
+        and: 'exception message matches the expected message'
+            e.details.contains(exceptionMessage)
+        where:
+            scenario                              | conditionProperty                               || exceptionMessage
+            'more than one condition is supplied' | ['cpsPath':'some-path', 'cpsPath2':'some-path'] || 'Only one condition property is allowed for the CPS path query.'
+            'cpsPath key not supplied'            | ['wrong-key':'some-path']                       || 'Wrong CPS path condition property. - expecting "cpsPath" as the condition property.'
+            'cpsPath not supplied'                | ['cpsPath':'']                                  || 'Wrong CPS path. - please supply a valid CPS path.'
+    }
+
+    def 'Validate CmHandle where #scenario.'() {
+        when: 'the validator is called on a cps path condition property'
+            def result = CmHandleQueryRestParametersValidator.validateCpsPathConditionProperties(['cpsPath':cpsPath])
+        then: 'the expected boolean value is returned'
+            result == expectedBoolean
+        where:
+            scenario                                       | cpsPath                                                || expectedBoolean
+            'cpsPath is valid'                             | '/some/valid/path'                                     || true
+            'cpsPath attempts to query private properties' | "//additional-properties[@some-property='some-value']" || false
+    }
 }