diff options
33 files changed, 458 insertions, 95 deletions
diff --git a/cps-application/src/main/resources/application.yml b/cps-application/src/main/resources/application.yml index 83494d6545..f61a09ba17 100644 --- a/cps-application/src/main/resources/application.yml +++ b/cps-application/src/main/resources/application.yml @@ -215,7 +215,7 @@ ncmp: advised-modules-sync: sleep-time-ms: 5000 locked-modules-sync: - sleep-time-ms: 300000 + sleep-time-ms: 60000 cm-handle-data-sync: sleep-time-ms: 30000 subscription-forwarding: diff --git a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyController.java b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyController.java index a482cf5a3c..af5f226a4f 100755 --- a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyController.java +++ b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyController.java @@ -86,7 +86,7 @@ public class NetworkCmProxyController implements NetworkCmProxyApi { * Get resource data from datastore. * * @param datastoreName name of the datastore - * @param cmHandle cm handle identifier + * @param cmHandleReference cm handle or alternate id identifier * @param resourceIdentifier resource identifier * @param optionsParamInQuery options query parameter * @param topicParamInQuery topic query parameter @@ -97,15 +97,16 @@ public class NetworkCmProxyController implements NetworkCmProxyApi { @Override @Timed(value = "cps.ncmp.controller.get", description = "Time taken to get resource data from datastore") public ResponseEntity<Object> getResourceDataForCmHandle(final String datastoreName, - final String cmHandle, + final String cmHandleReference, final String resourceIdentifier, final String optionsParamInQuery, final String topicParamInQuery, final Boolean includeDescendants, final String authorization) { - final CmResourceAddress cmResourceAddress = new CmResourceAddress(datastoreName, cmHandle, resourceIdentifier); + final CmResourceAddress cmResourceAddress = new CmResourceAddress(datastoreName, cmHandleReference, + resourceIdentifier); final Object result = networkCmProxyFacade.getResourceDataForCmHandle(cmResourceAddress, optionsParamInQuery, - topicParamInQuery, includeDescendants, authorization); + topicParamInQuery, includeDescendants, authorization); return ResponseEntity.ok(result); } 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 f7d80ad747..9f5331dbc3 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 @@ -41,6 +41,7 @@ import org.onap.cps.ncmp.impl.data.NetworkCmProxyFacade import org.onap.cps.ncmp.impl.inventory.DataStoreSyncState import org.onap.cps.ncmp.impl.inventory.models.CmHandleState import org.onap.cps.ncmp.impl.inventory.models.LockReasonCategory +import org.onap.cps.ncmp.impl.utils.AlternateIdMatcher import org.onap.cps.ncmp.rest.model.DataOperationDefinition import org.onap.cps.ncmp.rest.model.DataOperationRequest import org.onap.cps.ncmp.rest.util.CmHandleStateMapper @@ -94,6 +95,9 @@ class NetworkCmProxyControllerSpec extends Specification { NetworkCmProxyInventoryFacade mockNetworkCmProxyInventoryFacade = Mock() @SpringBean + AlternateIdMatcher mockalternateIdMatcher = Mock() + + @SpringBean ObjectMapper objectMapper = new ObjectMapper() @SpringBean @@ -136,12 +140,10 @@ class NetworkCmProxyControllerSpec extends Specification { def 'Get Resource Data from pass-through operational.'() { given: 'resource data url' def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-operational?resourceIdentifier=parent/child&options=(a=1,b=2)" - and: 'the expected cm resource address' - def expectedCmResourceAddress = new CmResourceAddress(PASSTHROUGH_OPERATIONAL.datastoreName, 'testCmHandle', 'parent/child') when: 'get data resource request is performed' def response = mvc.perform(get(getUrl).contentType(MediaType.APPLICATION_JSON)).andReturn().response then: 'the NCMP data service is called with correct parameters' - 1 * mockNetworkCmProxyFacade.getResourceDataForCmHandle(expectedCmResourceAddress, '(a=1,b=2)', NO_TOPIC, false, NO_AUTH_HEADER) >> Mono.just(new ResponseEntity<Object>(HttpStatus.OK)) + 1 * mockNetworkCmProxyFacade.getResourceDataForCmHandle(_, '(a=1,b=2)', NO_TOPIC, false, NO_AUTH_HEADER) >> Mono.just(new ResponseEntity<Object>(HttpStatus.OK)) and: 'response status is Ok' assert response.status == HttpStatus.OK.value() } @@ -150,11 +152,10 @@ class NetworkCmProxyControllerSpec extends Specification { given: 'resource data url' def getUrl = "$ncmpBasePathV1/ch/h123/data/ds/ncmp-datastore:operational?resourceIdentifier=parent/child${additionalUrlParam}" and: 'the expected cm resource address' - def expectedCmResourceAddress = new CmResourceAddress('ncmp-datastore:operational', 'h123', 'parent/child') when: 'get data resource request is performed' def response = mvc.perform(get(getUrl).contentType(MediaType.APPLICATION_JSON)).andReturn().response then: 'the NCMP data service is called with correct parameters' - 1 * mockNetworkCmProxyFacade.getResourceDataForCmHandle(expectedCmResourceAddress, NO_OPTIONS, NO_TOPIC, expectedIncludeDescendants, NO_AUTH_HEADER) + 1 * mockNetworkCmProxyFacade.getResourceDataForCmHandle(_, NO_OPTIONS, NO_TOPIC, expectedIncludeDescendants, NO_AUTH_HEADER) and: 'response status is OK' assert response.status == HttpStatus.OK.value() where: 'the following parameters are used' @@ -206,8 +207,7 @@ class NetworkCmProxyControllerSpec extends Specification { given: 'resource data url' def getUrl = "$ncmpBasePathV1/ch/ch-1/data/ds/ncmp-datastore:passthrough-running?resourceIdentifier=$resourceIdentifier&options=(a=1)" and: 'ncmp service returns json object' - def expectedCmResourceAddress = new CmResourceAddress(PASSTHROUGH_RUNNING.datastoreName, 'ch-1', resourceIdentifier) - 1 * mockNetworkCmProxyFacade.getResourceDataForCmHandle(expectedCmResourceAddress, '(a=1)', NO_TOPIC, false, NO_AUTH_HEADER) + 1 * mockNetworkCmProxyFacade.getResourceDataForCmHandle(_, '(a=1)', NO_TOPIC, false, NO_AUTH_HEADER) >> new ResponseEntity<Object>('{valid-json}', HttpStatus.OK) when: 'get data resource request is performed' def response = mvc.perform(get(getUrl).contentType(MediaType.APPLICATION_JSON)).andReturn().response diff --git a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyRestExceptionHandlerSpec.groovy b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyRestExceptionHandlerSpec.groovy index 97f3e03c6d..e6288ffbec 100644 --- a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyRestExceptionHandlerSpec.groovy +++ b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyRestExceptionHandlerSpec.groovy @@ -34,6 +34,7 @@ import org.onap.cps.ncmp.api.inventory.NetworkCmProxyInventoryFacade import org.onap.cps.ncmp.impl.data.NcmpCachedResourceRequestHandler import org.onap.cps.ncmp.impl.data.NcmpPassthroughResourceRequestHandler import org.onap.cps.ncmp.impl.data.NetworkCmProxyFacade +import org.onap.cps.ncmp.impl.inventory.InventoryPersistence import org.onap.cps.ncmp.rest.util.CmHandleStateMapper import org.onap.cps.ncmp.rest.util.DataOperationRequestMapper import org.onap.cps.ncmp.rest.util.DeprecationHelper @@ -77,6 +78,9 @@ class NetworkCmProxyRestExceptionHandlerSpec extends Specification { NetworkCmProxyInventoryFacade mockNetworkCmProxyInventoryFacade = Mock() @SpringBean + InventoryPersistence mockInventoryPersistence = Mock() + + @SpringBean JsonObjectMapper stubbedJsonObjectMapper = Stub() @SpringBean diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/data/models/CmResourceAddress.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/data/models/CmResourceAddress.java index e93aa4c603..98a343b92e 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/data/models/CmResourceAddress.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/data/models/CmResourceAddress.java @@ -20,6 +20,22 @@ package org.onap.cps.ncmp.api.data.models; -public record CmResourceAddress(String datastoreName, String cmHandleId, String resourceIdentifier) { +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.onap.cps.ncmp.config.CpsApplicationContext; +import org.onap.cps.ncmp.impl.utils.AlternateIdMatcher; +@Getter +@RequiredArgsConstructor +public class CmResourceAddress { + + private final String datastoreName; + @Getter(AccessLevel.NONE) + private final String cmHandleReference; + private final String resourceIdentifier; + + public String getResolvedCmHandleId() { + return CpsApplicationContext.getCpsBean(AlternateIdMatcher.class).getCmHandleId(cmHandleReference); + } } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/DmiDataOperations.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/DmiDataOperations.java index 4cbf9d4b3b..90783a829a 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/DmiDataOperations.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/DmiDataOperations.java @@ -92,12 +92,12 @@ public class DmiDataOperations { final String topic, final String requestId, final String authorization) { - final YangModelCmHandle yangModelCmHandle = getYangModelCmHandle(cmResourceAddress.cmHandleId()); + final YangModelCmHandle yangModelCmHandle = getYangModelCmHandle(cmResourceAddress.getResolvedCmHandleId()); final CmHandleState cmHandleState = yangModelCmHandle.getCompositeState().getCmHandleState(); validateIfCmHandleStateReady(yangModelCmHandle, cmHandleState); final String jsonRequestBody = getDmiRequestBody(READ, requestId, null, null, yangModelCmHandle); final UrlTemplateParameters urlTemplateParameters = getUrlTemplateParameters(cmResourceAddress - .datastoreName(), yangModelCmHandle, cmResourceAddress.resourceIdentifier(), options, topic); + .getDatastoreName(), yangModelCmHandle, cmResourceAddress.getResourceIdentifier(), options, topic); return dmiRestClient.asynchronousPostOperationWithJsonData(DATA, urlTemplateParameters, jsonRequestBody, READ, authorization); } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/NcmpCachedResourceRequestHandler.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/NcmpCachedResourceRequestHandler.java index bff2f6390c..01022cc03e 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/NcmpCachedResourceRequestHandler.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/NcmpCachedResourceRequestHandler.java @@ -61,9 +61,9 @@ public class NcmpCachedResourceRequestHandler extends NcmpDatastoreRequestHandle final String authorization) { final FetchDescendantsOption fetchDescendantsOption = getFetchDescendantsOption(includeDescendants); - final DataNode dataNode = cpsDataService.getDataNodes(cmResourceAddress.datastoreName(), - cmResourceAddress.cmHandleId(), - cmResourceAddress.resourceIdentifier(), + final DataNode dataNode = cpsDataService.getDataNodes(cmResourceAddress.getDatastoreName(), + cmResourceAddress.getResolvedCmHandleId(), + cmResourceAddress.getResourceIdentifier(), fetchDescendantsOption).iterator().next(); return Mono.justOrEmpty(dataNode); } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/NetworkCmProxyFacade.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/NetworkCmProxyFacade.java index 503915716e..b97088a5e0 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/NetworkCmProxyFacade.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/NetworkCmProxyFacade.java @@ -33,6 +33,7 @@ import org.onap.cps.ncmp.api.data.models.CmResourceAddress; import org.onap.cps.ncmp.api.data.models.DataOperationRequest; import org.onap.cps.ncmp.api.data.models.DatastoreType; import org.onap.cps.ncmp.api.data.models.OperationType; +import org.onap.cps.ncmp.impl.utils.AlternateIdMatcher; import org.onap.cps.spi.model.DataNode; import org.springframework.stereotype.Service; @@ -44,34 +45,35 @@ public class NetworkCmProxyFacade { private final NcmpCachedResourceRequestHandler ncmpCachedResourceRequestHandler; private final NcmpPassthroughResourceRequestHandler ncmpPassthroughResourceRequestHandler; private final DmiDataOperations dmiDataOperations; + private final AlternateIdMatcher alternateIdMatcher; /** * Fetches resource data for a given data store using DMI (Data Management Interface). * This method retrieves data based on the provided CmResourceAddress and additional query parameters. * It supports asynchronous processing and handles authorization if required. * - * @param cmResourceAddress The target data store, including the CM handle and resource identifier. - * This parameter must not be null. - * @param options Additional query parameters that may influence the data retrieval process, - * such as filters or limits. This parameter can be null. - * @param topic The topic name for triggering asynchronous responses. If specified, - * the response will be sent to this topic. This parameter can be null. - * @param includeDescendants include (all) descendants or not - * @param authorization The contents of the Authorization header. This parameter can be null - * if authorization is not required. + * @param cmResourceAddress The target data store, including the CM handle and resource identifier. + * This parameter must not be null. + * @param optionsParamInQuery Additional query parameters that may influence the data retrieval process, + * such as filters or limits. This parameter can be null. + * @param topicParamInQuery The topic name for triggering asynchronous responses. If specified, + * the response will be sent to this topic. This parameter can be null. + * @param includeDescendants include (all) descendants or not + * @param authorization The contents of the Authorization header. This parameter can be null + * if authorization is not required. * @return the result object, depends on use op topic. With topic a map object with request id is returned * otherwise the result of the request. */ public Object getResourceDataForCmHandle(final CmResourceAddress cmResourceAddress, - final String options, - final String topic, + final String optionsParamInQuery, + final String topicParamInQuery, final Boolean includeDescendants, final String authorization) { - final NcmpDatastoreRequestHandler ncmpDatastoreRequestHandler - = getNcmpDatastoreRequestHandler(cmResourceAddress.datastoreName()); - return ncmpDatastoreRequestHandler.executeRequest(cmResourceAddress, options, topic, includeDescendants, - authorization); + final NcmpDatastoreRequestHandler ncmpDatastoreRequestHandler + = getNcmpDatastoreRequestHandler(cmResourceAddress.getDatastoreName()); + return ncmpDatastoreRequestHandler.executeRequest(cmResourceAddress, optionsParamInQuery, + topicParamInQuery, includeDescendants, authorization); } /** @@ -117,7 +119,6 @@ public class NetworkCmProxyFacade { operationType, requestData, dataType, authorization); } - private NcmpDatastoreRequestHandler getNcmpDatastoreRequestHandler(final String datastoreName) { if (OPERATIONAL.equals(DatastoreType.fromDatastoreName(datastoreName))) { return ncmpCachedResourceRequestHandler; diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/InventoryPersistence.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/InventoryPersistence.java index cb4b04e4a0..beef752ef1 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/InventoryPersistence.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/InventoryPersistence.java @@ -144,4 +144,12 @@ public interface InventoryPersistence extends NcmpPersistence { * @return Collection of CM handle Ids */ Collection<String> getCmHandleIdsWithGivenModules(Collection<String> moduleNamesForQuery); + + /** + * Check database if cm handle id exists if not return false. + * + * @param cmHandleId cmHandle Id + * @return Boolean + */ + boolean isExistingCmHandleId(String cmHandleId); } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/InventoryPersistenceImpl.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/InventoryPersistenceImpl.java index 0ca2cd3407..083b25db3d 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/InventoryPersistenceImpl.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/InventoryPersistenceImpl.java @@ -195,6 +195,15 @@ public class InventoryPersistenceImpl extends NcmpPersistenceImpl implements Inv return cpsAnchorService.queryAnchorNames(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, moduleNamesForQuery); } + @Override + public boolean isExistingCmHandleId(final String cmHandleId) { + try { + return getCmHandleDataNodeByCmHandleId(cmHandleId).size() > 0; + } catch (final DataNodeNotFoundException exception) { + return false; + } + } + private static String getXPathForCmHandleById(final String cmHandleId) { return NCMP_DMI_REGISTRY_PARENT + "/cm-handles[@id='" + cmHandleId + "']"; } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/sync/ModuleSyncService.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/sync/ModuleSyncService.java index d2bc3ada86..ca0f1c6a6d 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/sync/ModuleSyncService.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/sync/ModuleSyncService.java @@ -29,23 +29,17 @@ import static org.onap.cps.ncmp.impl.inventory.NcmpPersistence.NFP_OPERATIONAL_D import java.time.OffsetDateTime; import java.util.Collection; import java.util.Collections; -import java.util.List; import java.util.Map; import lombok.AllArgsConstructor; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; import org.onap.cps.api.CpsAnchorService; import org.onap.cps.api.CpsDataService; import org.onap.cps.api.CpsModuleService; -import org.onap.cps.ncmp.impl.inventory.CmHandleQueryService; import org.onap.cps.ncmp.impl.inventory.models.CmHandleState; import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle; -import org.onap.cps.ncmp.impl.utils.YangDataConverter; import org.onap.cps.spi.CascadeDeleteAllowed; -import org.onap.cps.spi.FetchDescendantsOption; import org.onap.cps.spi.exceptions.SchemaSetNotFoundException; -import org.onap.cps.spi.model.DataNode; import org.onap.cps.spi.model.ModuleReference; import org.onap.cps.utils.ContentType; import org.onap.cps.utils.JsonObjectMapper; @@ -58,7 +52,6 @@ public class ModuleSyncService { private final DmiModelOperations dmiModelOperations; private final CpsModuleService cpsModuleService; - private final CmHandleQueryService cmHandleQueryService; private final CpsDataService cpsDataService; private final CpsAnchorService cpsAnchorService; private final JsonObjectMapper jsonObjectMapper; @@ -113,34 +106,25 @@ public class ModuleSyncService { } private ModuleDelta getModuleDelta(final YangModelCmHandle yangModelCmHandle, final String targetModuleSetTag) { - final Collection<ModuleReference> allModuleReferences; final Map<String, String> newYangResources; - - final YangModelCmHandle cmHandleWithSameModuleSetTag = getAnyReadyCmHandleByModuleSetTag(targetModuleSetTag); - if (cmHandleWithSameModuleSetTag == null) { + Collection<ModuleReference> allModuleReferences = getModuleReferencesByModuleSetTag(targetModuleSetTag); + if (allModuleReferences.isEmpty()) { allModuleReferences = dmiModelOperations.getModuleReferences(yangModelCmHandle); newYangResources = dmiModelOperations.getNewYangResourcesFromDmi(yangModelCmHandle, cpsModuleService.identifyNewModuleReferences(allModuleReferences)); } else { log.info("Found other cm handle having same module set tag: {}", targetModuleSetTag); - allModuleReferences = cpsModuleService.getYangResourcesModuleReferences( - NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, cmHandleWithSameModuleSetTag.getId()); newYangResources = NO_NEW_MODULES; } return new ModuleDelta(allModuleReferences, newYangResources); } - private YangModelCmHandle getAnyReadyCmHandleByModuleSetTag(final String moduleSetTag) { - if (StringUtils.isBlank(moduleSetTag)) { - return null; + private Collection<ModuleReference> getModuleReferencesByModuleSetTag(final String moduleSetTag) { + if (moduleSetTag == null || moduleSetTag.trim().isEmpty()) { + return Collections.emptyList(); } - final String escapedModuleSetTag = moduleSetTag.replace("'", "''"); - final List<DataNode> dataNodes = cmHandleQueryService.queryNcmpRegistryByCpsPath( - NCMP_DMI_REGISTRY_PARENT + "/cm-handles[@module-set-tag='" + escapedModuleSetTag + "']", - FetchDescendantsOption.DIRECT_CHILDREN_ONLY); - return dataNodes.stream().map(YangDataConverter::toYangModelCmHandle) - .filter(cmHandle -> cmHandle.getCompositeState().getCmHandleState() == CmHandleState.READY) - .findFirst().orElse(null); + return cpsModuleService.getModuleReferencesByAttribute(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, + Map.of("module-set-tag", moduleSetTag), Map.of("cm-handle-state", CmHandleState.READY.name())); } private void setCmHandleModuleSetTag(final YangModelCmHandle yangModelCmHandle, final String newModuleSetTag) { @@ -149,4 +133,5 @@ public class ModuleSyncService { cpsDataService.updateNodeLeaves(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, NCMP_DMI_REGISTRY_PARENT, jsonForUpdate, OffsetDateTime.now(), ContentType.JSON); } + } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/utils/AlternateIdMatcher.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/utils/AlternateIdMatcher.java index 832e576d55..c408ff9b13 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/utils/AlternateIdMatcher.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/utils/AlternateIdMatcher.java @@ -56,6 +56,21 @@ public class AlternateIdMatcher { throw new NoAlternateIdMatchFoundException(alternateId); } + /** + * Get cm handle Id from given cmHandleReference. + * + * @param cmHandleReference alternate ID + * @return cm handle id string + */ + public String getCmHandleId(final String cmHandleReference) { + if (inventoryPersistence.isExistingCmHandleId(cmHandleReference)) { + return cmHandleReference; + } else { + return inventoryPersistence.getCmHandleDataNodeByAlternateId(cmHandleReference) + .getLeaves().get("id").toString(); + } + } + private String getParentPath(final String path, final String separator) { final int lastSeparatorIndex = path.lastIndexOf(separator); return lastSeparatorIndex < 0 ? "" : path.substring(0, lastSeparatorIndex); diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/DmiDataOperationsSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/DmiDataOperationsSpec.groovy index 970444f643..8b369bf549 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/DmiDataOperationsSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/DmiDataOperationsSpec.groovy @@ -32,6 +32,7 @@ import org.onap.cps.ncmp.impl.dmi.DmiOperationsBaseSpec import org.onap.cps.ncmp.impl.dmi.DmiProperties import org.onap.cps.ncmp.impl.dmi.UrlTemplateParameters import org.onap.cps.ncmp.impl.inventory.models.CmHandleState +import org.onap.cps.ncmp.impl.utils.AlternateIdMatcher import org.onap.cps.ncmp.utils.TestUtils import org.onap.cps.utils.JsonObjectMapper import org.spockframework.spring.SpringBean @@ -76,6 +77,9 @@ class DmiDataOperationsSpec extends DmiOperationsBaseSpec { @SpringBean PolicyExecutor policyExecutor = Mock() + @SpringBean + AlternateIdMatcher alternateIdMatcher = Mock() + def 'call get resource data for #expectedDataStore from DMI without topic #scenario.'() { given: 'a cm handle for #cmHandleId' mockYangModelCmHandleRetrieval(dmiProperties) @@ -86,6 +90,7 @@ class DmiDataOperationsSpec extends DmiOperationsBaseSpec { mockDmiRestClient.asynchronousPostOperationWithJsonData(DATA, expectedUrlTemplateWithVariables, expectedJson, READ, NO_AUTH_HEADER) >> responseFromDmi when: 'get resource data is invoked' def cmResourceAddress = new CmResourceAddress(expectedDataStore.datastoreName, cmHandleId, resourceIdentifier) + alternateIdMatcher.getCmHandleId(cmHandleId) >> cmHandleId def result = objectUnderTest.getResourceDataFromDmi(cmResourceAddress, expectedOptions, NO_TOPIC, NO_REQUEST_ID, NO_AUTH_HEADER).block() then: 'the result is the response from the DMI service' assert result.body == '{some-key:some-value}' diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/NcmpCachedResourceRequestHandlerSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/NcmpCachedResourceRequestHandlerSpec.groovy index 9c696dcc7a..314b76183e 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/NcmpCachedResourceRequestHandlerSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/NcmpCachedResourceRequestHandlerSpec.groovy @@ -21,19 +21,35 @@ package org.onap.cps.ncmp.impl.data import org.onap.cps.api.CpsDataService +import org.onap.cps.events.EventsPublisher import org.onap.cps.ncmp.api.data.models.CmResourceAddress +import org.onap.cps.ncmp.config.CpsApplicationContext +import org.onap.cps.ncmp.impl.dmi.DmiProperties +import org.onap.cps.ncmp.impl.utils.AlternateIdMatcher import org.onap.cps.spi.model.DataNode +import org.spockframework.spring.SpringBean +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.context.ApplicationContext +import org.springframework.test.context.ContextConfiguration import reactor.core.publisher.Mono import spock.lang.Specification import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS +@SpringBootTest +@ContextConfiguration(classes = [CpsApplicationContext]) class NcmpCachedResourceRequestHandlerSpec extends Specification { def cpsDataService = Mock(CpsDataService) def networkCmProxyQueryService= Mock(NetworkCmProxyQueryService) + @SpringBean + AlternateIdMatcher alternateIdMatcher = Mock() + + @SpringBean + ApplicationContext applicationContext = Mock() + def objectUnderTest = new NcmpCachedResourceRequestHandler(cpsDataService, networkCmProxyQueryService) def 'Execute a request with include descendants = #includeDescendants.'() { @@ -54,6 +70,7 @@ class NcmpCachedResourceRequestHandlerSpec extends Specification { def dataNode2 = new DataNode(xpath:'p2') cpsDataService.getDataNodes('datastore','ch-1','resource',OMIT_DESCENDANTS) >> [dataNode1, dataNode2] when: 'getting the resource data' + alternateIdMatcher.getCmHandleId('ch-1') >> 'ch-1' def result = objectUnderTest.getResourceDataForCmHandle(cmResourceAddress, 'options', 'topic', 'request id', false, 'authorization') then: 'the result is a "Mono" holding just the first data node' assert result instanceof Mono diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/NetworkCmProxyFacadeSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/NetworkCmProxyFacadeSpec.groovy index f4e449904b..5f83ad5f83 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/NetworkCmProxyFacadeSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/NetworkCmProxyFacadeSpec.groovy @@ -26,6 +26,7 @@ package org.onap.cps.ncmp.impl.data import org.onap.cps.ncmp.api.data.models.CmResourceAddress import org.onap.cps.ncmp.api.data.models.DataOperationRequest +import org.onap.cps.ncmp.impl.utils.AlternateIdMatcher import org.onap.cps.spi.model.DataNode import reactor.core.publisher.Mono import spock.lang.Specification @@ -41,8 +42,9 @@ class NetworkCmProxyFacadeSpec extends Specification { def mockDmiDataOperations = Mock(DmiDataOperations) def mockNcmpCachedResourceRequestHandler = Mock(NcmpCachedResourceRequestHandler) def mockNcmpPassthroughResourceRequestHandler = Mock(NcmpPassthroughResourceRequestHandler) + def mockAlternateIdMatcher = Mock(AlternateIdMatcher) - def objectUnderTest = new NetworkCmProxyFacade(mockNcmpCachedResourceRequestHandler, mockNcmpPassthroughResourceRequestHandler, mockDmiDataOperations) + def objectUnderTest = new NetworkCmProxyFacade(mockNcmpCachedResourceRequestHandler, mockNcmpPassthroughResourceRequestHandler, mockDmiDataOperations, mockAlternateIdMatcher) def NO_TOPIC = null @@ -87,6 +89,7 @@ class NetworkCmProxyFacadeSpec extends Specification { given: 'a cm resource address for datastore operational' def cmResourceAddress = new CmResourceAddress('ncmp-datastore:operational', 'some CM Handle', 'some resource Id') and: 'get resource data from DMI is called' + mockAlternateIdMatcher.getCmHandleId('some CM Handle') >> 'some CM Handle' mockNcmpCachedResourceRequestHandler.executeRequest(cmResourceAddress, 'options', NO_TOPIC, false, 'authorization') >> Mono.just('dmi response') when: 'get resource data operational for the given cm resource address is called' @@ -103,6 +106,4 @@ class NetworkCmProxyFacadeSpec extends Specification { then: 'DMI called with correct data' 1 * mockDmiDataOperations.writeResourceDataPassThroughRunningFromDmi('testCmHandle', 'testResourceId', UPDATE, '{some-json}', 'application/json', 'authorization') } - - } diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/sync/ModuleSyncServiceSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/sync/ModuleSyncServiceSpec.groovy index c3a01a7393..6030e5debf 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/sync/ModuleSyncServiceSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/sync/ModuleSyncServiceSpec.groovy @@ -30,8 +30,6 @@ import org.onap.cps.ncmp.impl.inventory.models.CmHandleState import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle import org.onap.cps.spi.CascadeDeleteAllowed import org.onap.cps.spi.exceptions.SchemaSetNotFoundException -import org.onap.cps.spi.model.DataNode -import org.onap.cps.spi.model.DataNodeBuilder import org.onap.cps.spi.model.ModuleReference import org.onap.cps.utils.JsonObjectMapper import spock.lang.Specification @@ -49,13 +47,9 @@ class ModuleSyncServiceSpec extends Specification { def mockJsonObjectMapper = Mock(JsonObjectMapper) def objectUnderTest = new ModuleSyncService(mockDmiModelOperations, mockCpsModuleService, - mockCmHandleQueries, mockCpsDataService, mockCpsAnchorService, mockJsonObjectMapper) + mockCpsDataService, mockCpsAnchorService, mockJsonObjectMapper) def expectedDataspaceName = NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME - def static cmHandleWithModuleSetTag = new DataNodeBuilder() - .withXpath("/dmi-registry/cm-handles[@id='otherId']") - .withLeaves(['id': 'otherId', 'module-set-tag': 'tag-1']) - .withAnchor('otherId').build() def 'Sync model for a NEW cm handle using module set tags: #scenario.'() { given: 'a cm handle state to be synced' @@ -70,8 +64,8 @@ class ModuleSyncServiceSpec extends Specification { mockDmiModelOperations.getNewYangResourcesFromDmi(yangModelCmHandle, identifiedNewModuleReferences) >> newModuleNameContentToMap and: 'the module service identifies #identifiedNewModuleReferences.size() new modules' mockCpsModuleService.identifyNewModuleReferences(moduleReferences) >> identifiedNewModuleReferences - and: 'system contains other cm handle with "same tag" (that is READY)' - mockCmHandleQueries.queryNcmpRegistryByCpsPath(*_) >> existingCmHandlesWithSameTag + and: 'the service returns a list of module references when queried with the specified attributes' + mockCpsModuleService.getModuleReferencesByAttribute(*_) >> existingModuleReferences when: 'module sync is triggered' objectUnderTest.syncAndCreateSchemaSetAndAnchor(yangModelCmHandle) then: 'create schema set from module is invoked with correct parameters' @@ -79,10 +73,10 @@ class ModuleSyncServiceSpec extends Specification { and: 'anchor is created with the correct parameters' 1 * mockCpsAnchorService.createAnchor(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, 'ch-1', 'ch-1') where: 'the following parameters are used' - scenario | existingModuleResourcesInCps | identifiedNewModuleReferences | newModuleNameContentToMap | moduleSetTag | existingCmHandlesWithSameTag - 'one new module, new tag' | [['module2': '2'], ['module3': '3']] | [new ModuleReference('module1', '1')] | [module1: 'some yang source'] | '' | [] - 'no new module, new tag' | [['module1': '1'], ['module2': '2']] | [] | [:] | 'new-tag-1' | [] - 'same tag' | [['module1': '1'], ['module2': '2']] | [] | [:] | 'same-tag' | [cmHandleWithModuleSetTag] + scenario | identifiedNewModuleReferences | newModuleNameContentToMap | moduleSetTag | existingModuleReferences + 'one new module, new tag' | [new ModuleReference('module1', '1')] | [module1: 'some yang source'] | '' | [] + 'no new module, new tag' | [] | [:] | 'new-tag-1' | [] + 'same tag' | [] | [:] | 'same-tag' | [new ModuleReference('module1', '1'), new ModuleReference('module2', '2')] } def 'Upgrade model for an existing cm handle with Module Set Tag where the modules are #scenario'() { @@ -101,8 +95,8 @@ class ModuleSyncServiceSpec extends Specification { mockCpsModuleService.identifyNewModuleReferences(_) >> [] and: 'CPS-Core returns list of existing module resources for TBD' mockCpsModuleService.getYangResourcesModuleReferences(*_) >> [ new ModuleReference('module1','1') ] - and: 'system contains #existingCmHandlesWithSameTag.size() cm handles with same tag' - mockCmHandleQueries.queryNcmpRegistryByCpsPath(*_) >> existingCmHandlesWithSameTag + and: 'the service returns a list of module references when queried with the specified attributes' + mockCpsModuleService.getModuleReferencesByAttribute(*_) >> existingModuleReferences and: 'the other cm handle is a state ready' mockCmHandleQueries.cmHandleHasState('otherId', CmHandleState.READY) >> true when: 'module sync is triggered' @@ -114,9 +108,9 @@ class ModuleSyncServiceSpec extends Specification { and: 'No anchor is created for the upgraded cm handle' 0 * mockCpsAnchorService.createAnchor(*_) where: 'the following parameters are used' - scenario | existingCmHandlesWithSameTag + scenario | existingModuleReferences 'new' | [] - 'in database' | [cmHandleWithModuleSetTag] + 'in database' | [new ModuleReference('module1', '1')] } def 'upgrade model for a existing cm handle'() { @@ -130,9 +124,8 @@ class ModuleSyncServiceSpec extends Specification { and: 'the module service returns some module references' def moduleReferences = [new ModuleReference('module1', '1'), new ModuleReference('module2', '2')] mockCpsModuleService.getYangResourcesModuleReferences(*_)>> moduleReferences - and: 'a cm handle with the same moduleSetTag can be found in the registry' - mockCmHandleQueries.queryNcmpRegistryByCpsPath(*_) >> [new DataNode(xpath: '/dmi-registry/cm-handles[@id=\'cmHandleId-1\']', leaves: ['id': 'cmHandleId-1'], - childDataNodes: [new DataNode(xpath: '/dmi-registry/cm-handles[@id=\'cmHandleId-1\']/state', leaves: ['cm-handle-state': 'READY'])])] + and: 'the service returns a list of module references when queried with the specified attributes' + mockCpsModuleService.getModuleReferencesByAttribute(*_) >> moduleReferences when: 'module upgrade is triggered' objectUnderTest.syncAndUpgradeSchemaSet(yangModelCmHandle) then: 'the upgrade is delegated to the module service (with the correct parameters)' diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/utils/AlternateIdMatcherSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/utils/AlternateIdMatcherSpec.groovy index ad84495825..a497b4554a 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/utils/AlternateIdMatcherSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/utils/AlternateIdMatcherSpec.groovy @@ -63,4 +63,18 @@ class AlternateIdMatcherSpec extends Specification { 'no match for other child' | '/a/c' 'no match at all' | '/x/y' } + + def 'Get cmHandle id from passed cmHandleReference (cmHandleId scenario)' () { + when: 'a cmHandleCmReference is passed in' + def result = objectUnderTest.getCmHandleId(cmHandleReference) + then: 'the inventory persistence service returns a cm handle (or not)' + mockInventoryPersistence.isExistingCmHandleId(cmHandleReference) >> existingCmHandleIdResponse + mockInventoryPersistence.getCmHandleDataNodeByAlternateId(cmHandleReference) >> alternateIdGetResponse + and: 'correct result is returned' + assert result == cmHandleReference + where: + cmHandleReference | existingCmHandleIdResponse | alternateIdGetResponse + 'ch-1' | true | '' + 'alt-1' | false | new DataNode(leaves: [id:'alt-1']) + } } diff --git a/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsModulePersistenceServiceImpl.java b/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsModulePersistenceServiceImpl.java index 17f13b81ad..2c4cc7486b 100755 --- a/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsModulePersistenceServiceImpl.java +++ b/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsModulePersistenceServiceImpl.java @@ -241,6 +241,15 @@ public class CpsModulePersistenceServiceImpl implements CpsModulePersistenceServ return moduleReferenceRepository.identifyNewModuleReferences(moduleReferencesToCheck); } + @Override + public Collection<ModuleReference> getModuleReferencesByAttribute(final String dataspaceName, + final String anchorName, + final Map<String, String> parentAttributes, + final Map<String, String> childAttributes) { + return moduleReferenceRepository.findModuleReferences(dataspaceName, anchorName, parentAttributes, + childAttributes); + } + private Set<YangResourceEntity> synchronizeYangResources( final Map<String, String> moduleReferenceNameToContentMap) { final Map<String, YangResourceEntity> checksumToEntityMap = moduleReferenceNameToContentMap.entrySet().stream() diff --git a/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryCpsPathQueryImpl.java b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryCpsPathQueryImpl.java index 78e0f08c44..9c98f7f7d9 100644 --- a/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryCpsPathQueryImpl.java +++ b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryCpsPathQueryImpl.java @@ -1,6 +1,6 @@ /*- * ============LICENSE_START======================================================= - * Copyright (C) 2021-2023 Nordix Foundation. + * Copyright (C) 2021-2024 Nordix Foundation. * Modifications Copyright (C) 2023 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,8 +21,6 @@ package org.onap.cps.spi.repository; -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; import jakarta.persistence.Query; import jakarta.transaction.Transactional; import java.util.List; @@ -38,9 +36,6 @@ import org.onap.cps.spi.entities.FragmentEntity; @Slf4j public class FragmentRepositoryCpsPathQueryImpl implements FragmentRepositoryCpsPathQuery { - @PersistenceContext - private EntityManager entityManager; - private final FragmentQueryBuilder fragmentQueryBuilder; @Override diff --git a/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceQuery.java b/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceQuery.java index 00e53aa00f..4082307384 100644 --- a/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceQuery.java +++ b/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceQuery.java @@ -1,6 +1,6 @@ /*- * ============LICENSE_START======================================================= - * Copyright (C) 2022 Nordix Foundation. + * Copyright (C) 2022-2024 Nordix Foundation. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ package org.onap.cps.spi.repository; import java.util.Collection; +import java.util.Map; import org.onap.cps.spi.model.ModuleReference; /** @@ -29,4 +30,8 @@ import org.onap.cps.spi.model.ModuleReference; public interface ModuleReferenceQuery { Collection<ModuleReference> identifyNewModuleReferences(final Collection<ModuleReference> moduleReferencesToCheck); + + Collection<ModuleReference> findModuleReferences(final String dataspaceName, final String anchorName, + final Map<String, String> parentAttributes, + final Map<String, String> childAttributes); } diff --git a/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceRepositoryImpl.java b/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceRepositoryImpl.java index 454848b98f..6cc8234c90 100644 --- a/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceRepositoryImpl.java +++ b/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceRepositoryImpl.java @@ -22,12 +22,15 @@ package org.onap.cps.spi.repository; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; +import jakarta.persistence.Query; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; -import lombok.AllArgsConstructor; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.onap.cps.spi.model.ModuleReference; @@ -35,13 +38,13 @@ import org.springframework.transaction.annotation.Transactional; @Slf4j @Transactional -@AllArgsConstructor +@RequiredArgsConstructor public class ModuleReferenceRepositoryImpl implements ModuleReferenceQuery { @PersistenceContext private EntityManager entityManager; - private TempTableCreator tempTableCreator; + private final TempTableCreator tempTableCreator; @Override @SneakyThrows @@ -66,6 +69,96 @@ public class ModuleReferenceRepositoryImpl implements ModuleReferenceQuery { return identifyNewModuleReferencesForCmHandle(tempTableName); } + /** + * Finds module references based on specified dataspace, anchor, and attribute filters. + * This method constructs and executes a SQL query to retrieve module references. The query applies filters to + * parent and child fragments using the provided attribute maps. The `parentAttributes` are used to filter + * parent fragments, while `childAttributes` filter child fragments. + * + * @param dataspaceName the name of the dataspace to filter on. + * @param anchorName the name of the anchor to filter on. + * @param parentAttributes a map of attributes for filtering parent fragments. + * @param childAttributes a map of attributes for filtering child fragments. + * @return a collection of {@link ModuleReference} objects that match the specified filters. + */ + @Transactional + @SuppressWarnings("unchecked") + @Override + public Collection<ModuleReference> findModuleReferences(final String dataspaceName, final String anchorName, + final Map<String, String> parentAttributes, + final Map<String, String> childAttributes) { + + final String parentFragmentWhereClause = buildWhereClause(childAttributes, "parentFragment"); + final String childFragmentWhereClause = buildWhereClause(parentAttributes, "childFragment"); + + final String moduleReferencesSqlQuery = buildModuleReferencesSqlQuery(parentFragmentWhereClause, + childFragmentWhereClause); + + final Query query = entityManager.createNativeQuery(moduleReferencesSqlQuery); + setQueryParameters(query, parentAttributes, childAttributes, anchorName, dataspaceName); + return processQueryResults(query.getResultList()); + } + + private String buildWhereClause(final Map<String, String> attributes, final String alias) { + return attributes.keySet().stream() + .map(attributeName -> String.format("%s.attributes->>'%s' = ?", alias, attributeName)) + .collect(Collectors.joining(" AND ")); + } + + private void setQueryParameters(final Query query, final Map<String, String> parentAttributes, + final Map<String, String> childAttributes, final String anchorName, + final String dataspaceName) { + final String childAttributeValue = childAttributes.entrySet().iterator().next().getValue(); + query.setParameter(1, childAttributeValue); + + final String parentAttributeValue = parentAttributes.entrySet().iterator().next().getValue(); + query.setParameter(2, parentAttributeValue); + + query.setParameter(3, anchorName); + query.setParameter(4, dataspaceName); + } + + private String buildModuleReferencesSqlQuery(final String parentFragmentClause, final String childFragmentClause) { + return """ + WITH Fragment AS ( + SELECT childFragment.attributes->>'id' AS schema_set_name + FROM fragment parentFragment + JOIN fragment childFragment ON parentFragment.parent_id = childFragment.id + JOIN anchor anchorInfo ON parentFragment.anchor_id = anchorInfo.id + JOIN dataspace dataspaceInfo ON anchorInfo.dataspace_id = dataspaceInfo.id + WHERE %s + AND %s + AND anchorInfo.name = ? + AND dataspaceInfo.name = ? + LIMIT 1 + ), + SchemaSet AS ( + SELECT id + FROM schema_set + WHERE name = (SELECT schema_set_name FROM Fragment) + ) + SELECT yangResource.module_name, yangResource.revision + FROM yang_resource yangResource + JOIN schema_set_yang_resources schemaSetYangResources + ON yangResource.id = schemaSetYangResources.yang_resource_id + WHERE schemaSetYangResources.schema_set_id = (SELECT id FROM SchemaSet); + """.formatted(parentFragmentClause, childFragmentClause); + } + + private Collection<ModuleReference> processQueryResults(final List<Object[]> queryResults) { + if (queryResults.isEmpty()) { + log.info("No module references found for the provided attributes."); + return Collections.emptyList(); + } + return queryResults.stream() + .map(queryResult -> { + final String name = (String) queryResult[0]; + final String revision = (String) queryResult[1]; + return new ModuleReference(name, revision); + }) + .collect(Collectors.toList()); + } + private Collection<ModuleReference> identifyNewModuleReferencesForCmHandle(final String tempTableName) { final String sql = String.format( "SELECT %1$s.module_name, %1$s.revision" @@ -81,7 +174,6 @@ public class ModuleReferenceRepositoryImpl implements ModuleReferenceQuery { for (final Object[] row : resultsAsObjects) { resultsAsModuleReferences.add(new ModuleReference((String) row[0], (String) row[1])); } - return resultsAsModuleReferences; } } diff --git a/cps-service/src/main/java/org/onap/cps/api/CpsModuleService.java b/cps-service/src/main/java/org/onap/cps/api/CpsModuleService.java index bdd361458e..931209c998 100644 --- a/cps-service/src/main/java/org/onap/cps/api/CpsModuleService.java +++ b/cps-service/src/main/java/org/onap/cps/api/CpsModuleService.java @@ -155,4 +155,37 @@ public interface CpsModuleService { Collection<ModuleReference> identifyNewModuleReferences( Collection<ModuleReference> moduleReferencesToCheck); + /** + * Retrieves module references based on the provided dataspace name, anchor name and attribute filters + * for both parent and child fragments. + + * This method constructs and executes a SQL query to find module references from a database, using + * the specified `dataspaceName`, `anchorName` and two sets of attribute filters: one for parent fragments + * and one for child fragments. The method applies these filters to identify the appropriate fragments + * and schema sets, and then retrieves the corresponding module references. + + * The SQL query is dynamically built based on the provided attribute filters: + * - The `parentAttributes` map is used to filter the parent fragments. The entries in this map are + * converted into a WHERE clause for the parent fragments. + * - The `childAttributes` map is used to filter the child fragments. This is applied to the child fragments + * after filtering the parent fragments. + * + * @param dataspaceName the name of the dataspace to filter on. It is used to locate the relevant dataspace + * in the database. + * @param anchorName the name of the anchor to filter on. It is used to locate the relevant anchor within + * the dataspace. + * @param parentAttributes a map of attributes to filter parent fragments. Each entry in this map represents + * an attribute key-value pair used in the WHERE clause for parent fragments. + * @param childAttributes a map of attributes to filter child fragments. Each entry in this map represents + * an attribute key-value pair used in the WHERE clause for child fragments. + * @return a collection of {@link ModuleReference} objects that match the given criteria. Each + * {@code ModuleReference} contains information about a module's name and revision. + * @implNote The method assumes that both `parentAttributes` and `childAttributes` maps contain at least + * one entry. The first entry from `parentAttributes` is used to filter parent fragments, + * and the first entry from `childAttributes` is used to filter child fragments. + */ + Collection<ModuleReference> getModuleReferencesByAttribute(final String dataspaceName, final String anchorName, + final Map<String, String> parentAttributes, + final Map<String, String> childAttributes); + } diff --git a/cps-service/src/main/java/org/onap/cps/api/impl/CpsModuleServiceImpl.java b/cps-service/src/main/java/org/onap/cps/api/impl/CpsModuleServiceImpl.java index e6ad9a8bb8..34610f3455 100644 --- a/cps-service/src/main/java/org/onap/cps/api/impl/CpsModuleServiceImpl.java +++ b/cps-service/src/main/java/org/onap/cps/api/impl/CpsModuleServiceImpl.java @@ -171,6 +171,17 @@ public class CpsModuleServiceImpl implements CpsModuleService { return cpsModulePersistenceService.identifyNewModuleReferences(moduleReferencesToCheck); } + @Timed(value = "cps.module.service.module.reference.query", + description = "Time taken to query list of module references") + @Override + public Collection<ModuleReference> getModuleReferencesByAttribute(final String dataspaceName, + final String anchorName, + final Map<String, String> parentAttributes, + final Map<String, String> childAttributes) { + return cpsModulePersistenceService.getModuleReferencesByAttribute(dataspaceName, anchorName, parentAttributes, + childAttributes); + } + private boolean isCascadeDeleteProhibited(final CascadeDeleteAllowed cascadeDeleteAllowed) { return CascadeDeleteAllowed.CASCADE_DELETE_PROHIBITED == cascadeDeleteAllowed; } diff --git a/cps-service/src/main/java/org/onap/cps/spi/CpsModulePersistenceService.java b/cps-service/src/main/java/org/onap/cps/spi/CpsModulePersistenceService.java index eeaaa47991..793f38e4bc 100755 --- a/cps-service/src/main/java/org/onap/cps/spi/CpsModulePersistenceService.java +++ b/cps-service/src/main/java/org/onap/cps/spi/CpsModulePersistenceService.java @@ -153,4 +153,20 @@ public interface CpsModulePersistenceService { Collection<ModuleReference> identifyNewModuleReferences( Collection<ModuleReference> moduleReferencesToCheck); + /** + * Retrieves module references based on the specified dataspace, anchor, and attribute filters. + + * Constructs and executes a SQL query to find module references by applying filters for parent and child fragments. + * Uses `parentAttributes` for filtering parent fragments and `childAttributes` for filtering child fragments. + * + * @param dataspaceName the name of the dataspace to filter on. + * @param anchorName the name of the anchor to filter on. + * @param parentAttributes a map of attributes for filtering parent fragments. + * @param childAttributes a map of attributes for filtering child fragments. + * @return a collection of {@link ModuleReference} objects matching the criteria. + */ + Collection<ModuleReference> getModuleReferencesByAttribute(final String dataspaceName, final String anchorName, + final Map<String, String> parentAttributes, + final Map<String, String> childAttributes); + } diff --git a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsModuleServiceImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsModuleServiceImplSpec.groovy index ad8c54bf27..62eba0c397 100644 --- a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsModuleServiceImplSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsModuleServiceImplSpec.groovy @@ -238,6 +238,23 @@ class CpsModuleServiceImplSpec extends Specification { 1 * mockCpsModulePersistenceService.identifyNewModuleReferences(moduleReferencesToCheck) } + def 'Get module references when queried by attributes'() { + given: 'a valid dataspace name and anchor name' + def dataspaceName = 'someDataspace' + def anchorName = 'someAnchor' + and: 'a set of parent attributes and child attributes used for filtering' + def parentAttributes = ['some-property-key1': 'some-property-val1'] + def childAttributes = ['some-property-key2': 'some-property-val2'] + and: 'a list of expected module references returned by the persistence service' + def expectedModuleReferences = [new ModuleReference(moduleName: 'some-name', revision: 'some-revision')] + mockCpsModulePersistenceService.getModuleReferencesByAttribute(dataspaceName, anchorName, parentAttributes, childAttributes) >> expectedModuleReferences + when: 'the method is invoked to retrieve module references by attributes' + def actualModuleReferences = objectUnderTest.getModuleReferencesByAttribute(dataspaceName, anchorName, parentAttributes, childAttributes) + then: 'the retrieved module references should match the expected module references' + assert actualModuleReferences == expectedModuleReferences + } + + def 'Getting module definitions with module name'() { given: 'module persistence service returns module definitions for module name' def moduleDefinitionsFromPersistenceService = [ new ModuleDefinition('name', 'revision', 'content' ) ] diff --git a/docs/_static/logo_onap_2024.png b/docs/_static/logo_onap_2024.png Binary files differnew file mode 100644 index 0000000000..55d307fc34 --- /dev/null +++ b/docs/_static/logo_onap_2024.png diff --git a/docs/conf.py b/docs/conf.py index e8bb6630f3..5d7a79941d 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -31,7 +31,7 @@ html_theme = "sphinx_rtd_theme" html_theme_options = { "style_nav_header_background": "white", "sticky_navigation": "False" } -html_logo = "_static/logo_onap_2017.png" +html_logo = "_static/logo_onap_2024.png" html_favicon = "_static/favicon.ico" html_static_path = ["_static"] html_show_sphinx = False diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/functional/ncmp/AlternateIdSpec.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/functional/ncmp/AlternateIdSpec.groovy new file mode 100644 index 0000000000..222b3c0f6f --- /dev/null +++ b/integration-test/src/test/groovy/org/onap/cps/integration/functional/ncmp/AlternateIdSpec.groovy @@ -0,0 +1,54 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the 'License'); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps.integration.functional.ncmp + +import org.onap.cps.integration.base.CpsIntegrationSpecBase +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get + +class AlternateIdSpec extends CpsIntegrationSpecBase { + + def setup() { + dmiDispatcher1.moduleNamesPerCmHandleId['ch-1'] = ['M1', 'M2'] + registerCmHandle(DMI1_URL, 'ch-1', NO_MODULE_SET_TAG, 'alternateId') + } + + def cleanup() { + deregisterCmHandle(DMI1_URL, 'ch-1') + } + + def 'AlternateId in pass-through data operations should return OK status.'() { + given: 'the URL for the pass-through data request' + def url = '/ncmp/v1/ch/alternateId/data/ds/ncmp-datastore:passthrough-running' + when: 'a pass-through data request is sent to NCMP' + def response = mvc.perform(get(url) + .queryParam('resourceIdentifier', 'my-resource-id') + .contentType(MediaType.APPLICATION_JSON)) + .andReturn().response + then: 'response status is Ok' + assert response.status == HttpStatus.OK.value() + } + + + +} diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/functional/ncmp/CmHandleCreateSpec.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/functional/ncmp/CmHandleCreateSpec.groovy index 3d526c6a42..d27badccb2 100644 --- a/integration-test/src/test/groovy/org/onap/cps/integration/functional/ncmp/CmHandleCreateSpec.groovy +++ b/integration-test/src/test/groovy/org/onap/cps/integration/functional/ncmp/CmHandleCreateSpec.groovy @@ -24,6 +24,7 @@ import org.apache.kafka.common.TopicPartition import org.apache.kafka.common.serialization.StringDeserializer import org.onap.cps.integration.KafkaTestContainer import org.onap.cps.integration.base.CpsIntegrationSpecBase +import org.onap.cps.ncmp.api.NcmpResponseStatus import org.onap.cps.ncmp.api.inventory.NetworkCmProxyInventoryFacade import org.onap.cps.ncmp.api.inventory.models.CmHandleRegistrationResponse import org.onap.cps.ncmp.api.inventory.models.DmiPluginRegistration @@ -133,6 +134,38 @@ class CmHandleCreateSpec extends CpsIntegrationSpecBase { deregisterCmHandles(DMI1_URL, ['ch-1', 'ch-2', 'ch-3']) } + def 'Create CM-handles with alternate IDs.'() { + given: 'DMI will return modules for all CM-handles when requested' + dmiDispatcher1.moduleNamesPerCmHandleId = (1..7).collectEntries{ ['ch-'+it, ['M1']] } + and: 'an existing CM-handle with an alternate ID' + registerCmHandle(DMI1_URL, 'ch-1', NO_MODULE_SET_TAG, 'existing-alt-id') + and: 'an existing CM-handle with no alternate ID' + registerCmHandle(DMI1_URL, 'ch-2', NO_MODULE_SET_TAG, NO_ALTERNATE_ID) + + when: 'a batch of CM-handles is registered for creation with various alternate IDs' + def cmHandlesToCreate = [ + new NcmpServiceCmHandle(cmHandleId: 'ch-3', alternateId: NO_ALTERNATE_ID), + new NcmpServiceCmHandle(cmHandleId: 'ch-4', alternateId: 'unique-alt-id'), + new NcmpServiceCmHandle(cmHandleId: 'ch-5', alternateId: 'existing-alt-id'), + new NcmpServiceCmHandle(cmHandleId: 'ch-6', alternateId: 'duplicate-alt-id'), + new NcmpServiceCmHandle(cmHandleId: 'ch-7', alternateId: 'duplicate-alt-id'), + ] + def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: DMI1_URL, createdCmHandles: cmHandlesToCreate) + def dmiPluginRegistrationResponse = objectUnderTest.updateDmiRegistrationAndSyncModule(dmiPluginRegistration) + + then: 'registration gives expected responses' + assert dmiPluginRegistrationResponse.createdCmHandles.sort { it.cmHandle } == [ + CmHandleRegistrationResponse.createSuccessResponse('ch-3'), + CmHandleRegistrationResponse.createSuccessResponse('ch-4'), + CmHandleRegistrationResponse.createFailureResponse('ch-5', NcmpResponseStatus.ALTERNATE_ID_ALREADY_ASSOCIATED), + CmHandleRegistrationResponse.createSuccessResponse('ch-6'), + CmHandleRegistrationResponse.createFailureResponse('ch-7', NcmpResponseStatus.ALTERNATE_ID_ALREADY_ASSOCIATED), + ] + + cleanup: 'deregister CM handles' + deregisterCmHandles(DMI1_URL, (1..7).collect{ 'ch-'+it }) + } + def 'CM Handle retry after failed module sync.'() { given: 'DMI is not initially available to handle requests' dmiDispatcher1.isAvailable = false diff --git a/k6-tests/ncmp/common/cmhandle-crud.js b/k6-tests/ncmp/common/cmhandle-crud.js index 6d5aff7fca..88ecdb45b8 100644 --- a/k6-tests/ncmp/common/cmhandle-crud.js +++ b/k6-tests/ncmp/common/cmhandle-crud.js @@ -20,7 +20,7 @@ import http from 'k6/http'; import { check, sleep } from 'k6'; -import { NCMP_BASE_URL, DMI_PLUGIN_URL, TOTAL_CM_HANDLES, REGISTRATION_BATCH_SIZE, CONTENT_TYPE_JSON_PARAM, makeBatchOfCmHandleIds } from './utils.js'; +import { NCMP_BASE_URL, DMI_PLUGIN_URL, TOTAL_CM_HANDLES, MODULE_SET_TAGS, REGISTRATION_BATCH_SIZE, CONTENT_TYPE_JSON_PARAM, makeBatchOfCmHandleIds } from './utils.js'; import { executeCmHandleIdSearch } from './search-base.js'; export function registerAllCmHandles() { @@ -44,8 +44,10 @@ function createCmHandles(cmHandleIds) { const url = `${NCMP_BASE_URL}/ncmpInventory/v1/ch`; const payload = { "dmiPlugin": DMI_PLUGIN_URL, - "createdCmHandles": cmHandleIds.map(cmHandleId => ({ + "createdCmHandles": cmHandleIds.map((cmHandleId, index) => ({ "cmHandle": cmHandleId, + "alternateId": `alt-${cmHandleId}`, + "moduleSetTag": MODULE_SET_TAGS[index % MODULE_SET_TAGS.length], "cmHandleProperties": {"neType": "RadioNode"}, "publicCmHandleProperties": { "Color": "yellow", diff --git a/k6-tests/ncmp/common/passthrough-crud.js b/k6-tests/ncmp/common/passthrough-crud.js index 76bda4e1bd..5617f9d093 100644 --- a/k6-tests/ncmp/common/passthrough-crud.js +++ b/k6-tests/ncmp/common/passthrough-crud.js @@ -36,6 +36,16 @@ export function passthroughRead() { return response; } +export function passthroughReadWithAltId() { + const cmHandleId = getRandomCmHandleId(); + const resourceIdentifier = 'my-resource-identifier'; + const includeDescendants = true; + const datastoreName = 'ncmp-datastore:passthrough-operational'; + const url = `${NCMP_BASE_URL}/ncmp/v1/ch/alt-${cmHandleId}/data/ds/${datastoreName}?resourceIdentifier=${resourceIdentifier}&include-descendants=${includeDescendants}` + const response = http.get(url); + return response; +} + export function passthroughWrite() { const cmHandleId = getRandomCmHandleId(); const resourceIdentifier = 'my-resource-identifier'; diff --git a/k6-tests/ncmp/common/utils.js b/k6-tests/ncmp/common/utils.js index f24edc50d6..58c958dce0 100644 --- a/k6-tests/ncmp/common/utils.js +++ b/k6-tests/ncmp/common/utils.js @@ -28,6 +28,7 @@ export const CONTENT_TYPE_JSON_PARAM = { headers: {'Content-Type': 'application/ export const DATA_OPERATION_READ_BATCH_SIZE = 200; export const TOPIC_DATA_OPERATIONS_BATCH_READ = 'topic-data-operations-batch-read'; export const KAFKA_BOOTSTRAP_SERVERS = ['localhost:9092']; +export const MODULE_SET_TAGS = ['tagA','tagB','tagC',' tagD'] export function recordTimeInSeconds(functionToExecute) { const startTimeInMillis = Date.now(); @@ -66,6 +67,7 @@ export function makeCustomSummaryReport(data, options) { makeSummaryCsvLine('4', 'CM-handle search with Module filter', 'milliseconds', 'http_req_duration{scenario:cm_search_module}', data, options), makeSummaryCsvLine('5a', 'Synchronous single CM-handle pass-through read', 'requests/second', 'http_reqs{scenario:passthrough_read}', data, options), makeSummaryCsvLine('5b', 'NCMP overhead for Synchronous single CM-handle pass-through read', 'milliseconds', 'ncmp_overhead_passthrough_read', data, options), + makeSummaryCsvLine('5c', 'NCMP overhead for Synchronous single CM-handle pass-through read with alternate id', 'milliseconds', 'ncmp_overhead_passthrough_read_alt_id', data, options), makeSummaryCsvLine('6a', 'Synchronous single CM-handle pass-through write', 'requests/second', 'http_reqs{scenario:passthrough_write}', data, options), makeSummaryCsvLine('6b', 'NCMP overhead for Synchronous single CM-handle pass-through write', 'milliseconds', 'ncmp_overhead_passthrough_write', data, options), makeSummaryCsvLine('7', 'Data operations batch read', 'events/second', 'data_operations_batch_read_cmhandles_per_second', data, options), diff --git a/k6-tests/ncmp/ncmp-kpi.js b/k6-tests/ncmp/ncmp-kpi.js index 8ff9ec50b4..d7e4405393 100644 --- a/k6-tests/ncmp/ncmp-kpi.js +++ b/k6-tests/ncmp/ncmp-kpi.js @@ -27,7 +27,7 @@ import { } from './common/utils.js'; import { registerAllCmHandles, deregisterAllCmHandles } from './common/cmhandle-crud.js'; import { executeCmHandleSearch, executeCmHandleIdSearch } from './common/search-base.js'; -import { passthroughRead, passthroughWrite, batchRead } from './common/passthrough-crud.js'; +import { passthroughRead, passthroughReadWithAltId, passthroughWrite, batchRead } from './common/passthrough-crud.js'; import { Reader, } from 'k6/x/kafka'; @@ -35,6 +35,7 @@ import { let cmHandlesCreatedPerSecondGauge = new Gauge('cmhandles_created_per_second'); let cmHandlesDeletedPerSecondGauge = new Gauge('cmhandles_deleted_per_second'); let passthroughReadNcmpOverheadTrend = new Trend('ncmp_overhead_passthrough_read'); +let passthroughReadNcmpOverheadTrendWithAlternateId = new Trend('ncmp_overhead_passthrough_read_alt_id'); let passthroughWriteNcmpOverheadTrend = new Trend('ncmp_overhead_passthrough_write'); let dataOperationsBatchReadCmHandlePerSecondTrend = new Trend('data_operations_batch_read_cmhandles_per_second'); @@ -55,6 +56,12 @@ export const options = { vus: 10, duration: DURATION, }, + passthrough_read_alt_id: { + executor: 'constant-vus', + exec: 'passthrough_read_alt_id', + vus: 1, + duration: DURATION, + }, passthrough_write: { executor: 'constant-vus', exec: 'passthrough_write', @@ -96,6 +103,7 @@ export const options = { 'http_reqs{scenario:passthrough_write}': ['rate >= 13'], 'http_reqs{scenario:passthrough_read}': ['rate >= 25'], 'ncmp_overhead_passthrough_read': ['avg <= 100'], + 'ncmp_overhead_passthrough_read_alt_id': ['avg <= 100'], 'ncmp_overhead_passthrough_write': ['avg <= 100'], 'http_req_duration{scenario:id_search_module}': ['avg <= 625'], 'http_req_duration{scenario:cm_search_module}': ['avg <= 13000'], @@ -126,6 +134,13 @@ export function passthrough_read() { passthroughReadNcmpOverheadTrend.add(overhead); } +export function passthrough_read_alt_id() { + const response = passthroughReadWithAltId(); + check(response, { 'passthrough read with alternate Id status equals 200': (r) => r.status === 200 }); + const overhead = response.timings.duration - READ_DATA_FOR_CM_HANDLE_DELAY_MS; + passthroughReadNcmpOverheadTrendWithAlternateId.add(overhead); +} + export function passthrough_write() { const response = passthroughWrite(); check(response, { 'passthrough write status equals 201': (r) => r.status === 201 }); |