diff options
author | Joseph Keenan <joseph.keenan@est.tech> | 2022-08-02 12:26:46 +0000 |
---|---|---|
committer | Gerrit Code Review <gerrit@onap.org> | 2022-08-02 12:26:46 +0000 |
commit | 7a75eea4e388a04d8b67610ef53ed16db75489e6 (patch) | |
tree | 75664b32b00cb5df106ec58a15e4c3e0f337b8a3 | |
parent | b3c2b74c5a90750978a051cc9c8a6933f77e7948 (diff) | |
parent | 82a550f6b080cb50912d93f7b13ba0fc97a95470 (diff) |
Merge "Query CmHandles using CPS path"
19 files changed, 699 insertions, 327 deletions
diff --git a/cps-bom/pom.xml b/cps-bom/pom.xml index 9b864b07fe..bf8fbe8c07 100644 --- a/cps-bom/pom.xml +++ b/cps-bom/pom.xml @@ -114,6 +114,11 @@ <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> diff --git a/cps-ncmp-rest/docs/openapi/components.yaml b/cps-ncmp-rest/docs/openapi/components.yaml index 2cb9d894c0..14fd4d24d3 100644 --- a/cps-ncmp-rest/docs/openapi/components.yaml +++ b/cps-ncmp-rest/docs/openapi/components.yaml @@ -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 diff --git a/cps-ncmp-rest/docs/openapi/ncmp.yml b/cps-ncmp-rest/docs/openapi/ncmp.yml index aaf0d6a1ab..d7b383705f 100755 --- a/cps-ncmp-rest/docs/openapi/ncmp.yml +++ b/cps-ncmp-rest/docs/openapi/ncmp.yml @@ -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 diff --git a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/mapper/CmHandleStateMapper.java b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/mapper/CmHandleStateMapper.java index 55b64ec760..097dd0af49 100644 --- a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/mapper/CmHandleStateMapper.java +++ b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/mapper/CmHandleStateMapper.java @@ -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); diff --git a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyControllerSpec.groovy b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyControllerSpec.groovy index 23263c9aa8..06a7759be9 100644 --- a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyControllerSpec.groovy +++ b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyControllerSpec.groovy @@ -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' ] diff --git a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/mapper/CmHandleStateMapperTest.groovy b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/mapper/CmHandleStateMapperTest.groovy index 677cf66127..663b9d02a6 100644 --- a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/mapper/CmHandleStateMapperTest.groovy +++ b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/mapper/CmHandleStateMapperTest.groovy @@ -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'() { diff --git a/cps-ncmp-service/pom.xml b/cps-ncmp-service/pom.xml index 93c265a7bf..d94c6d1dec 100644 --- a/cps-ncmp-service/pom.xml +++ b/cps-ncmp-service/pom.xml @@ -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. @@ -46,6 +46,10 @@ <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> </dependency> diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandlerQueryServiceImpl.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandlerQueryServiceImpl.java index a62a009ce7..d784bcdeac 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandlerQueryServiceImpl.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandlerQueryServiceImpl.java @@ -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()); diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImpl.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImpl.java index 8d32c1ade5..28cbf2cc24 100755 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImpl.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImpl.java @@ -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 index 0000000000..92387bab32 --- /dev/null +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/CmHandleQueries.java @@ -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())); + } +} + + diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/InventoryPersistence.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/InventoryPersistence.java index be26a58d58..14fc6d698a 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/InventoryPersistence.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/InventoryPersistence.java @@ -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; @@ -100,57 +97,6 @@ public class InventoryPersistence { } /** - * 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 * @return yang model cm handle @@ -219,17 +165,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. * * @param xpath xpath diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/sync/SyncUtils.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/sync/SyncUtils.java index 467fd8f608..2b7d3c99cb 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/sync/SyncUtils.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/sync/SyncUtils.java @@ -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()); } } diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandlerQueryServiceSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandlerQueryServiceSpec.groovy index 7cf572ddb1..40ec12da87 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandlerQueryServiceSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandlerQueryServiceSpec.groovy @@ -20,8 +20,14 @@ 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 index 0000000000..10a5d62461 --- /dev/null +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/CmHandleQueriesSpec.groovy @@ -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]) + } +} diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/InventoryPersistenceSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/InventoryPersistenceSpec.groovy index 7ac231c169..f9ca676f3b 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/InventoryPersistenceSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/InventoryPersistenceSpec.groovy @@ -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') diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/sync/SyncUtilsSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/sync/SyncUtilsSpec.groovy index 6c2d8f15b3..82e9d33ae7 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/sync/SyncUtilsSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/sync/SyncUtilsSpec.groovy @@ -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' diff --git a/cps-service/src/main/java/org/onap/cps/utils/CmHandleQueryRestParametersValidator.java b/cps-service/src/main/java/org/onap/cps/utils/CmHandleQueryRestParametersValidator.java index c3811eb485..7fe47be2da 100644 --- a/cps-service/src/main/java/org/onap/cps/utils/CmHandleQueryRestParametersValidator.java +++ b/cps-service/src/main/java/org/onap/cps/utils/CmHandleQueryRestParametersValidator.java @@ -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 index 0000000000..1d7ccb91df --- /dev/null +++ b/cps-service/src/main/java/org/onap/cps/utils/ValidQueryProperties.java @@ -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; + } +} diff --git a/cps-service/src/test/groovy/org/onap/cps/utils/CmHandleQueryRestParametersValidatorSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/utils/CmHandleQueryRestParametersValidatorSpec.groovy index a9b04c1ced..d5dcb7fc5c 100644 --- a/cps-service/src/test/groovy/org/onap/cps/utils/CmHandleQueryRestParametersValidatorSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/utils/CmHandleQueryRestParametersValidatorSpec.groovy @@ -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 + } } |