Query CmHandles using CPS path 53/129953/9
authorlukegleeson <luke.gleeson@est.tech>
Mon, 11 Jul 2022 09:55:53 +0000 (10:55 +0100)
committerlukegleeson <luke.gleeson@est.tech>
Fri, 29 Jul 2022 09:42:04 +0000 (10:42 +0100)
Added withCpsPath condition parameter
Validated to prevent misuse and blocking of querying using private properties
Updated OpenAPI with examples and links to documentation
Moved methods related to cmHandle querying using cps path from InventoryPersistence to CmHandleQueries
Renamed private method deleteSchemaSetAndListElementByCmHandleId to deleteCmHandleByCmHandleId

Issue-ID: CPS-977
Change-Id: I83827215b7e58de74f8f62cd0140516d217d93f1
Signed-off-by: lukegleeson <luke.gleeson@est.tech>
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
+    }
 }