diff options
Diffstat (limited to 'cps-ncmp-service')
84 files changed, 2065 insertions, 1180 deletions
diff --git a/cps-ncmp-service/pom.xml b/cps-ncmp-service/pom.xml index fc41da3ae..55abffc9b 100644 --- a/cps-ncmp-service/pom.xml +++ b/cps-ncmp-service/pom.xml @@ -38,6 +38,22 @@ </properties> <dependencies> <dependency> + <groupId>io.opentelemetry</groupId> + <artifactId>opentelemetry-exporter-otlp</artifactId> + </dependency> + <dependency> + <groupId>io.opentelemetry</groupId> + <artifactId>opentelemetry-sdk-extension-jaeger-remote-sampler</artifactId> + </dependency> + <dependency> + <groupId>io.opentelemetry</groupId> + <artifactId>opentelemetry-sdk-extension-autoconfigure</artifactId> + </dependency> + <dependency> + <groupId>io.opentelemetry.instrumentation</groupId> + <artifactId>opentelemetry-kafka-clients-2.6</artifactId> + </dependency> + <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency> @@ -70,8 +86,8 @@ <artifactId>mapstruct-processor</artifactId> </dependency> <dependency> - <groupId>org.springframework</groupId> - <artifactId>spring-web</artifactId> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <!-- T E S T - D E P E N D E N C I E S --> <dependency> @@ -104,5 +120,13 @@ <artifactId>spock</artifactId> <scope>test</scope> </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-actuator-autoconfigure</artifactId> + </dependency> + <dependency> + <groupId>jakarta.servlet</groupId> + <artifactId>jakarta.servlet-api</artifactId> + </dependency> </dependencies> </project> diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/NcmpResponseStatus.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/NcmpResponseStatus.java index bdc3dee77..8cfad7dbf 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/NcmpResponseStatus.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/NcmpResponseStatus.java @@ -26,14 +26,12 @@ import lombok.Getter; public enum NcmpResponseStatus { SUCCESS("0", "Successfully applied changes"), - SUCCESSFULLY_APPLIED_SUBSCRIPTION("1", "successfully applied subscription"), + CM_DATA_SUBSCRIPTION_ACCEPTED("1", "ACCEPTED"), CM_HANDLES_NOT_FOUND("100", "cm handle id(s) not found"), CM_HANDLES_NOT_READY("101", "cm handle(s) not ready"), DMI_SERVICE_NOT_RESPONDING("102", "dmi plugin service is not responding"), UNABLE_TO_READ_RESOURCE_DATA("103", "dmi plugin service is not able to read resource data"), - PARTIALLY_APPLIED_SUBSCRIPTION("104", "partially applied subscription"), - SUBSCRIPTION_NOT_APPLICABLE("105", "subscription not applicable for all cm handles"), - SUBSCRIPTION_PENDING("106", "subscription pending for all cm handles"), + CM_DATA_SUBSCRIPTION_REJECTED("104", "REJECTED"), UNKNOWN_ERROR("108", "Unknown error"), CM_HANDLE_ALREADY_EXIST("109", "cm-handle already exists"), CM_HANDLE_INVALID_ID("110", "cm-handle has an invalid character(s) in id"), diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/NetworkCmProxyDataService.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/NetworkCmProxyDataService.java index 20545d711..73c8d9609 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/NetworkCmProxyDataService.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/NetworkCmProxyDataService.java @@ -37,6 +37,7 @@ import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle; import org.onap.cps.spi.FetchDescendantsOption; import org.onap.cps.spi.model.ModuleDefinition; import org.onap.cps.spi.model.ModuleReference; +import reactor.core.publisher.Mono; /* * Datastore interface for handling CPS data. @@ -52,20 +53,29 @@ public interface NetworkCmProxyDataService { DmiPluginRegistrationResponse updateDmiRegistrationAndSyncModule(DmiPluginRegistration dmiPluginRegistration); /** - * Get resource data for given data store using dmi. - * - * @param cmResourceAddress target datastore, cm handle and resource identifier - * @param optionsParamInQuery options query - * @param topicParamInQuery topic name for (triggering) async responses - * @param requestId unique requestId for async request - * @param authorization contents of Authorization header, or null if not present - * @return {@code Object} resource data - */ - Object getResourceDataForCmHandle(CmResourceAddress cmResourceAddress, - String optionsParamInQuery, - String topicParamInQuery, - String requestId, - String authorization); + * 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 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 requestId A unique identifier for the request, used for tracking and correlating + * asynchronous operations. This parameter must not be null. + * @param authorization The contents of the Authorization header. This parameter can be null + * if authorization is not required. + * @return {@code Mono<Object>} A reactive Mono that emits the resource data on successful retrieval + * or an error signal if the operation fails. The Mono represents a single asynchronous + * computation result. + */ + Mono<Object> getResourceDataForCmHandle(CmResourceAddress cmResourceAddress, + String optionsParamInQuery, + String topicParamInQuery, + String requestId, + String authorization); /** * Get resource data for operational. diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/NetworkCmProxyQueryService.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/NetworkCmProxyQueryService.java index 340806b89..39d497217 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/NetworkCmProxyQueryService.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/NetworkCmProxyQueryService.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. @@ -20,7 +20,9 @@ package org.onap.cps.ncmp.api; +import java.util.Collection; import org.onap.cps.spi.FetchDescendantsOption; +import org.onap.cps.spi.model.DataNode; /* * Datastore interface for handling cached CPS data query requests. @@ -28,14 +30,21 @@ import org.onap.cps.spi.FetchDescendantsOption; public interface NetworkCmProxyQueryService { /** - * Get resource data for operational. + * Fetches operational resource data based on the provided CM handle identifier and CPS path. + * This method retrieves data nodes from the specified path within the context of a given CM handle. + * It supports options for fetching descendant nodes. * - * @param cmHandleId cm handle identifier - * @param cpsPath cps path - * @Link FetchDescendantsOption fetch descendants option - * @return {@code Object} resource data + * @param cmHandleId The CM handle identifier, which uniquely identifies the CM handle. + * This parameter must not be null. + * @param cpsPath The CPS (Control Plane Service) path specifying the location of the + * resource data within the CM handle. This parameter must not be null. + * @param fetchDescendantsOption The option specifying whether to fetch descendant nodes along with the specified + * resource data. + * @return {@code Collection<DataNode>} A collection of DataNode objects representing the resource data + * retrieved from the specified path. The collection may include descendant nodes based on the + * fetchDescendantsOption. */ - Object queryResourceDataOperational(String cmHandleId, - String cpsPath, - FetchDescendantsOption fetchDescendantsOption); + Collection<DataNode> queryResourceDataOperational(String cmHandleId, + String cpsPath, + FetchDescendantsOption fetchDescendantsOption); } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/DataJobService.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/datajobs/DataJobService.java index 6122afc80..f22124593 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/DataJobService.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/datajobs/DataJobService.java @@ -18,11 +18,11 @@ * ============LICENSE_END========================================================= */ -package org.onap.cps.ncmp.api; +package org.onap.cps.ncmp.api.datajobs; -import org.onap.cps.ncmp.api.models.datajob.DataJobMetadata; -import org.onap.cps.ncmp.api.models.datajob.DataJobReadRequest; -import org.onap.cps.ncmp.api.models.datajob.DataJobWriteRequest; +import org.onap.cps.ncmp.api.datajobs.models.DataJobMetadata; +import org.onap.cps.ncmp.api.datajobs.models.DataJobReadRequest; +import org.onap.cps.ncmp.api.datajobs.models.DataJobWriteRequest; public interface DataJobService { diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/datajob/DataJobMetadata.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/datajobs/models/DataJobMetadata.java index dc8037b86..564352d8d 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/datajob/DataJobMetadata.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/datajobs/models/DataJobMetadata.java @@ -18,7 +18,7 @@ * ============LICENSE_END========================================================= */ -package org.onap.cps.ncmp.api.models.datajob; +package org.onap.cps.ncmp.api.datajobs.models; /** * Metadata of read/write data job request. diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/datajob/DataJobReadRequest.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/datajobs/models/DataJobReadRequest.java index f861c3d49..19408b1da 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/datajob/DataJobReadRequest.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/datajobs/models/DataJobReadRequest.java @@ -18,7 +18,7 @@ * ============LICENSE_END========================================================= */ -package org.onap.cps.ncmp.api.models.datajob; +package org.onap.cps.ncmp.api.datajobs.models; import java.util.List; diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/datajob/DataJobWriteRequest.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/datajobs/models/DataJobWriteRequest.java index 254e198b8..d8961b17c 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/datajob/DataJobWriteRequest.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/datajobs/models/DataJobWriteRequest.java @@ -18,7 +18,7 @@ * ============LICENSE_END========================================================= */ -package org.onap.cps.ncmp.api.models.datajob; +package org.onap.cps.ncmp.api.datajobs.models; import java.util.List; diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/datajob/ReadOperation.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/datajobs/models/ReadOperation.java index d2b073896..2459e4cc2 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/datajob/ReadOperation.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/datajobs/models/ReadOperation.java @@ -18,7 +18,7 @@ * ============LICENSE_END========================================================= */ -package org.onap.cps.ncmp.api.models.datajob; +package org.onap.cps.ncmp.api.datajobs.models; import java.util.List; diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/datajob/WriteOperation.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/datajobs/models/WriteOperation.java index c2f6504ce..807e03f06 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/datajob/WriteOperation.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/datajobs/models/WriteOperation.java @@ -18,7 +18,7 @@ * ============LICENSE_END========================================================= */ -package org.onap.cps.ncmp.api.models.datajob; +package org.onap.cps.ncmp.api.datajobs.models; /** * Holds information of write data job operation. 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 6aa09767b..754050947 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 @@ -87,8 +87,8 @@ import org.onap.cps.spi.model.ModuleDefinition; import org.onap.cps.spi.model.ModuleReference; import org.onap.cps.utils.JsonObjectMapper; import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; +import reactor.core.publisher.Mono; @Slf4j @Service @@ -133,17 +133,14 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService } @Override - public Object getResourceDataForCmHandle(final CmResourceAddress cmResourceAddress, + public Mono<Object> getResourceDataForCmHandle(final CmResourceAddress cmResourceAddress, final String optionsParamInQuery, final String topicParamInQuery, final String requestId, final String authorization) { - final ResponseEntity<?> responseEntity = dmiDataOperations.getResourceDataFromDmi(cmResourceAddress, - optionsParamInQuery, - topicParamInQuery, - requestId, - authorization); - return responseEntity.getBody(); + return dmiDataOperations.getResourceDataFromDmi(cmResourceAddress, optionsParamInQuery, topicParamInQuery, + requestId, authorization) + .flatMap(responseEntity -> Mono.justOrEmpty(responseEntity.getBody())); } @Override @@ -160,8 +157,7 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService final DataOperationRequest dataOperationRequest, final String requestId, final String authorization) { - dmiDataOperations.requestResourceDataFromDmi(topicParamInQuery, dataOperationRequest, requestId, - authorization); + dmiDataOperations.requestResourceDataFromDmi(topicParamInQuery, dataOperationRequest, requestId, authorization); } @Override diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandler.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandler.java index f86191002..11c58235e 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandler.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandler.java @@ -2,7 +2,7 @@ * ============LICENSE_START======================================================= * Copyright (C) 2022-2024 Nordix Foundation * Modifications Copyright (C) 2022 Bell Canada - * Modifications Copyright (C) 2023 TechMahindra Ltd. + * Modifications Copyright (C) 2024 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -55,6 +55,7 @@ import org.onap.cps.spi.exceptions.DataNodeNotFoundException; import org.onap.cps.spi.exceptions.DataValidationException; import org.onap.cps.spi.model.DataNode; import org.onap.cps.spi.model.DataNodeBuilder; +import org.onap.cps.utils.ContentType; import org.onap.cps.utils.JsonObjectMapper; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; @@ -222,7 +223,7 @@ public class NetworkCmProxyDataServicePropertyHandler { cmHandleData.put(fieldName, newFieldValue); dmiRegistryData.put("cm-handles", cmHandleData); cpsDataService.updateNodeLeaves(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, NCMP_DMI_REGISTRY_PARENT, - jsonObjectMapper.asJsonString(dmiRegistryData), OffsetDateTime.now()); + jsonObjectMapper.asJsonString(dmiRegistryData), OffsetDateTime.now(), ContentType.JSON); log.debug("Updating {} for cmHandle {} with value : {})", fieldName, cmHandleIdToUpdate, newFieldValue); } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyQueryServiceImpl.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyQueryServiceImpl.java index d8353f302..8d3b6ed8f 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyQueryServiceImpl.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyQueryServiceImpl.java @@ -22,11 +22,13 @@ package org.onap.cps.ncmp.api.impl; import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME; +import java.util.Collection; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.onap.cps.api.CpsQueryService; import org.onap.cps.ncmp.api.NetworkCmProxyQueryService; import org.onap.cps.spi.FetchDescendantsOption; +import org.onap.cps.spi.model.DataNode; import org.springframework.stereotype.Service; @Slf4j @@ -37,9 +39,9 @@ public class NetworkCmProxyQueryServiceImpl implements NetworkCmProxyQueryServic private final CpsQueryService cpsQueryService; @Override - public Object queryResourceDataOperational(final String cmHandleId, - final String cpsPath, - final FetchDescendantsOption fetchDescendantsOption) { + public Collection<DataNode> queryResourceDataOperational(final String cmHandleId, + final String cpsPath, + final FetchDescendantsOption fetchDescendantsOption) { return cpsQueryService.queryDataNodes(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, cmHandleId, cpsPath, fetchDescendantsOption); } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/client/DmiRestClient.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/client/DmiRestClient.java index 798a280c8..5811cf97d 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/client/DmiRestClient.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/client/DmiRestClient.java @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2021-2023 Nordix Foundation + * Copyright (C) 2021-2024 Nordix Foundation * Modifications Copyright (C) 2022 Bell Canada * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,20 +21,36 @@ package org.onap.cps.ncmp.api.impl.client; +import static org.onap.cps.ncmp.api.NcmpResponseStatus.DMI_SERVICE_NOT_RESPONDING; +import static org.onap.cps.ncmp.api.NcmpResponseStatus.UNABLE_TO_READ_RESOURCE_DATA; +import static org.onap.cps.ncmp.api.NcmpResponseStatus.UNKNOWN_ERROR; +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; +import static org.springframework.http.HttpStatus.REQUEST_TIMEOUT; + import com.fasterxml.jackson.databind.JsonNode; +import java.net.URI; +import java.net.URISyntaxException; import java.util.Locale; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.onap.cps.ncmp.api.impl.config.NcmpConfiguration.DmiProperties; -import org.onap.cps.ncmp.api.impl.exception.HttpClientRequestException; +import org.onap.cps.ncmp.api.impl.config.DmiProperties; +import org.onap.cps.ncmp.api.impl.exception.DmiClientRequestException; +import org.onap.cps.ncmp.api.impl.exception.InvalidDmiResourceUrlException; import org.onap.cps.ncmp.api.impl.operations.OperationType; -import org.springframework.http.HttpEntity; +import org.onap.cps.ncmp.api.impl.operations.RequiredDmiService; +import org.onap.cps.utils.JsonObjectMapper; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; -import org.springframework.web.client.HttpStatusCodeException; -import org.springframework.web.client.RestTemplate; +import org.springframework.web.client.HttpServerErrorException; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientRequestException; +import org.springframework.web.reactive.function.client.WebClientResponseException; +import reactor.core.publisher.Mono; @Component @RequiredArgsConstructor @@ -43,60 +59,134 @@ public class DmiRestClient { private static final String HEALTH_CHECK_URL_EXTENSION = "/actuator/health"; private static final String NOT_SPECIFIED = ""; - private final RestTemplate restTemplate; + private static final String NO_AUTHORIZATION = null; + private final DmiProperties dmiProperties; + private final JsonObjectMapper jsonObjectMapper; + @Qualifier("dataServicesWebClient") + private final WebClient dataServicesWebClient; + @Qualifier("modelServicesWebClient") + private final WebClient modelServicesWebClient; + @Qualifier("healthChecksWebClient") + private final WebClient healthChecksWebClient; /** - * Sends POST operation to DMI with json body containing module references. + * Sends a POST operation to the DMI with a JSON body containing module references. * - * @param dmiResourceUrl dmi resource url - * @param requestBodyAsJsonString json data body - * @param operationType the type of operation being executed (for error reporting only) - * @param authorization contents of Authorization header, or null if not present - * @return response entity of type String + * @param requiredDmiService Determines if the required service is for a data or model operation. + * @param dmiUrl The DMI resource URL. + * @param requestBodyAsJsonString JSON data body. + * @param operationType The type of operation being executed (for error reporting only). + * @param authorization Contents of the Authorization header, or null if not present. + * @return ResponseEntity containing the response from the DMI. + * @throws DmiClientRequestException If there is an error during the DMI request. */ - public ResponseEntity<Object> postOperationWithJsonData(final String dmiResourceUrl, + public ResponseEntity<Object> postOperationWithJsonData(final RequiredDmiService requiredDmiService, + final String dmiUrl, final String requestBodyAsJsonString, final OperationType operationType, final String authorization) { - final var httpEntity = new HttpEntity<>(requestBodyAsJsonString, configureHttpHeaders(new HttpHeaders(), - authorization)); try { - return restTemplate.postForEntity(dmiResourceUrl, httpEntity, Object.class); - } catch (final HttpStatusCodeException httpStatusCodeException) { - final String exceptionMessage = "Unable to " + operationType.toString() + " resource data."; - throw new HttpClientRequestException(exceptionMessage, httpStatusCodeException.getResponseBodyAsString(), - httpStatusCodeException.getStatusCode().value()); + return postOperationWithJsonDataAsync(requiredDmiService, dmiUrl, requestBodyAsJsonString, operationType, + authorization).block(); + } catch (final HttpServerErrorException e) { + throw handleDmiClientException(e, operationType.getOperationName()); } } /** + * Asynchronously performs an HTTP POST operation with the given JSON data. + * + * @param requiredDmiService The service object required for retrieving or configuring the WebClient. + * @param dmiUrl The URL to which the POST request is sent. + * @param requestBodyAsJsonString The JSON string that will be sent as the request body. + * @param operationType An enumeration or object that holds information about the type of operation + * being performed. + * @param authorization The authorization token to be added to the request headers. + * @return A Mono emitting the response entity containing the server's response. + */ + public Mono<ResponseEntity<Object>> postOperationWithJsonDataAsync(final RequiredDmiService requiredDmiService, + final String dmiUrl, + final String requestBodyAsJsonString, + final OperationType operationType, + final String authorization) { + final WebClient webClient = getWebClient(requiredDmiService); + return webClient.post() + .uri(toUri(dmiUrl)) + .headers(httpHeaders -> configureHttpHeaders(httpHeaders, authorization)) + .body(BodyInserters.fromValue(requestBodyAsJsonString)) + .retrieve() + .toEntity(Object.class) + .onErrorMap(throwable -> handleDmiClientException(throwable, operationType.getOperationName())); + } + + /** * Get DMI plugin health status. * - * @param dmiPluginBaseUrl the base URL of the dmi-plugin - * @return plugin health status ("UP" is all OK, "" (not-specified) in case of any exception) + * @param dmiUrl the base URL of the dmi-plugin + * @return plugin health status ("UP" is all OK, "" (not-specified) in case of any exception) */ - public String getDmiHealthStatus(final String dmiPluginBaseUrl) { - final HttpEntity<Object> httpHeaders = new HttpEntity<>(configureHttpHeaders(new HttpHeaders(), null)); + public String getDmiHealthStatus(final String dmiUrl) { try { - final JsonNode responseHealthStatus = - restTemplate.getForObject(dmiPluginBaseUrl + HEALTH_CHECK_URL_EXTENSION, - JsonNode.class, httpHeaders); + final URI dmiHealthCheckUri = toUri(dmiUrl + HEALTH_CHECK_URL_EXTENSION); + final JsonNode responseHealthStatus = healthChecksWebClient.get() + .uri(dmiHealthCheckUri) + .headers(httpHeaders -> configureHttpHeaders(httpHeaders, NO_AUTHORIZATION)) + .retrieve() + .bodyToMono(JsonNode.class).block(); return responseHealthStatus == null ? NOT_SPECIFIED : - responseHealthStatus.get("status").asText(); + responseHealthStatus.path("status").asText(); } catch (final Exception e) { - log.warn("Failed to retrieve health status from {}. Error Message: {}", dmiPluginBaseUrl, e.getMessage()); + log.warn("Failed to retrieve health status from {}. Error Message: {}", dmiUrl, e.getMessage()); return NOT_SPECIFIED; } } - private HttpHeaders configureHttpHeaders(final HttpHeaders httpHeaders, final String authorization) { + private WebClient getWebClient(final RequiredDmiService requiredDmiService) { + return requiredDmiService.equals(RequiredDmiService.DATA) ? dataServicesWebClient : modelServicesWebClient; + } + + private void configureHttpHeaders(final HttpHeaders httpHeaders, final String authorization) { if (dmiProperties.isDmiBasicAuthEnabled()) { httpHeaders.setBasicAuth(dmiProperties.getAuthUsername(), dmiProperties.getAuthPassword()); } else if (authorization != null && authorization.toLowerCase(Locale.getDefault()).startsWith("bearer ")) { httpHeaders.add(HttpHeaders.AUTHORIZATION, authorization); } - httpHeaders.setContentType(MediaType.APPLICATION_JSON); - return httpHeaders; + } + + private static URI toUri(final String dmiResourceUrl) { + try { + return new URI(dmiResourceUrl); + } catch (final URISyntaxException e) { + throw new InvalidDmiResourceUrlException(dmiResourceUrl, BAD_REQUEST.value()); + } + } + + private DmiClientRequestException handleDmiClientException(final Throwable throwable, final String operationType) { + if (throwable instanceof WebClientResponseException webClientResponseException) { + if (webClientResponseException.getStatusCode().isSameCodeAs(REQUEST_TIMEOUT)) { + throw new DmiClientRequestException(webClientResponseException.getStatusCode().value(), + webClientResponseException.getMessage(), + jsonObjectMapper.asJsonString(webClientResponseException.getResponseBodyAsString()), + DMI_SERVICE_NOT_RESPONDING); + } + throw new DmiClientRequestException(webClientResponseException.getStatusCode().value(), + webClientResponseException.getMessage(), + jsonObjectMapper.asJsonString(webClientResponseException.getResponseBodyAsString()), + UNABLE_TO_READ_RESOURCE_DATA); + + } + final String exceptionMessage = "Unable to " + operationType + " resource data."; + if (throwable instanceof WebClientRequestException webClientRequestException) { + throw new DmiClientRequestException(HttpStatus.SERVICE_UNAVAILABLE.value(), + webClientRequestException.getMessage(), + exceptionMessage, DMI_SERVICE_NOT_RESPONDING); + } + if (throwable instanceof HttpServerErrorException httpServerErrorException) { + throw new DmiClientRequestException(httpServerErrorException.getStatusCode().value(), exceptionMessage, + httpServerErrorException.getResponseBodyAsString(), DMI_SERVICE_NOT_RESPONDING); + } + throw new DmiClientRequestException(INTERNAL_SERVER_ERROR.value(), exceptionMessage, throwable.getMessage(), + UNKNOWN_ERROR); } } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/DmiProperties.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/DmiProperties.java new file mode 100644 index 000000000..5453efecd --- /dev/null +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/DmiProperties.java @@ -0,0 +1,55 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps.ncmp.api.impl.config; + +import lombok.AccessLevel; +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Getter +@Component +public class DmiProperties { + @Value("${ncmp.dmi.auth.username}") + private String authUsername; + @Value("${ncmp.dmi.auth.password}") + private String authPassword; + @Getter(AccessLevel.NONE) + @Value("${ncmp.dmi.api.base-path}") + private String dmiBasePath; + @Value("${ncmp.dmi.auth.enabled}") + private boolean dmiBasicAuthEnabled; + + /** + * Removes both leading and trailing slashes if they are present. + * + * @return dmi base path without any slashes ("/") + */ + public String getDmiBasePath() { + if (dmiBasePath.startsWith("/")) { + dmiBasePath = dmiBasePath.substring(1); + } + if (dmiBasePath.endsWith("/")) { + dmiBasePath = dmiBasePath.substring(0, dmiBasePath.length() - 1); + } + return dmiBasePath; + } +} diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/DmiWebClientConfiguration.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/DmiWebClientConfiguration.java new file mode 100644 index 000000000..08885a9e0 --- /dev/null +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/DmiWebClientConfiguration.java @@ -0,0 +1,125 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps.ncmp.api.impl.config; + +import io.netty.channel.ChannelOption; +import io.netty.handler.timeout.ReadTimeoutHandler; +import io.netty.handler.timeout.WriteTimeoutHandler; +import java.util.concurrent.TimeUnit; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.netty.http.client.HttpClient; +import reactor.netty.resources.ConnectionProvider; + +/** + * Configures and creates a WebClient bean that triggers an initialization (warmup) of the host name resolver and + * loads the necessary native libraries to avoid the extra time needed to load resources for first request. + */ +@Configuration +@RequiredArgsConstructor +public class DmiWebClientConfiguration { + + private final HttpClientConfiguration httpClientConfiguration; + + /** + * Configures and create a WebClient bean for DMI data service. + * + * @return a WebClient instance for data services. + */ + @Bean + public WebClient dataServicesWebClient() { + final HttpClientConfiguration.DataServices httpClientConfiguration + = this.httpClientConfiguration.getDataServices(); + + final HttpClient httpClient = createHttpClient("dataConnectionPool", + httpClientConfiguration.getMaximumConnectionsTotal(), + httpClientConfiguration.getConnectionTimeoutInSeconds(), + httpClientConfiguration.getReadTimeoutInSeconds(), + httpClientConfiguration.getWriteTimeoutInSeconds()); + return buildAndGetWebClient(httpClient, httpClientConfiguration.getMaximumInMemorySizeInMegabytes()); + } + + /** + * Configures and creates a WebClient bean for DMI model service. + * + * @return a WebClient instance for model services. + */ + @Bean + public WebClient modelServicesWebClient() { + final HttpClientConfiguration.ModelServices httpClientConfiguration + = this.httpClientConfiguration.getModelServices(); + + final HttpClient httpClient = createHttpClient("modelConnectionPool", + httpClientConfiguration.getMaximumConnectionsTotal(), + httpClientConfiguration.getConnectionTimeoutInSeconds(), + httpClientConfiguration.getReadTimeoutInSeconds(), + httpClientConfiguration.getWriteTimeoutInSeconds()); + return buildAndGetWebClient(httpClient, httpClientConfiguration.getMaximumInMemorySizeInMegabytes()); + } + + /** + * Configures and creates a WebClient bean for DMI health service. + * + * @return a WebClient instance for health checks. + */ + @Bean + public WebClient healthChecksWebClient() { + final HttpClientConfiguration.HealthCheckServices httpClientConfiguration + = this.httpClientConfiguration.getHealthCheckServices(); + + final HttpClient httpClient = createHttpClient("healthConnectionPool", + httpClientConfiguration.getMaximumConnectionsTotal(), + httpClientConfiguration.getConnectionTimeoutInSeconds(), + httpClientConfiguration.getReadTimeoutInSeconds(), + httpClientConfiguration.getWriteTimeoutInSeconds()); + return buildAndGetWebClient(httpClient, httpClientConfiguration.getMaximumInMemorySizeInMegabytes()); + } + + private static HttpClient createHttpClient(final String connectionProviderName, + final Integer maximumConnectionsTotal, + final Integer connectionTimeoutInSeconds, + final Integer readTimeoutInSeconds, + final Integer writeTimeoutInSeconds) { + final ConnectionProvider dmiWebClientConnectionProvider = ConnectionProvider.create(connectionProviderName, + maximumConnectionsTotal); + + return HttpClient.create(dmiWebClientConnectionProvider) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectionTimeoutInSeconds * 1000) + .doOnConnected(connection -> connection.addHandlerLast(new ReadTimeoutHandler(readTimeoutInSeconds, + TimeUnit.SECONDS)).addHandlerLast(new WriteTimeoutHandler(writeTimeoutInSeconds, + TimeUnit.SECONDS))); + } + + private static WebClient buildAndGetWebClient(final HttpClient httpClient, + final Integer maximumInMemorySizeInMegabytes) { + return WebClient.builder() + .defaultHeaders(header -> header.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)) + .defaultHeaders(header -> header.set(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)) + .clientConnector(new ReactorClientHttpConnector(httpClient)) + .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize( + maximumInMemorySizeInMegabytes * 1024 * 1024)).build(); + } +} diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/HttpClientConfiguration.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/HttpClientConfiguration.java index d547e31c6..62432f6ca 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/HttpClientConfiguration.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/HttpClientConfiguration.java @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2023 Nordix Foundation. + * Copyright (C) 2023-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. @@ -20,38 +20,47 @@ package org.onap.cps.ncmp.api.impl.config; -import java.time.Duration; -import java.time.temporal.ChronoUnit; import lombok.Getter; import lombok.Setter; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.convert.DurationUnit; +import org.springframework.stereotype.Component; @Getter @Setter -@ConfigurationProperties(prefix = "ncmp.dmi.httpclient", ignoreUnknownFields = true) +@Component +@ConfigurationProperties(prefix = "ncmp.dmi.httpclient") public class HttpClientConfiguration { - /** - * The maximum time to establish a connection. - */ - @DurationUnit(ChronoUnit.SECONDS) - private Duration connectionTimeoutInSeconds = Duration.ofSeconds(180); + private final DataServices dataServices = new DataServices(); + private final ModelServices modelServices = new ModelServices(); + private final HealthCheckServices healthCheckServices = new HealthCheckServices(); - /** - * The maximum number of open connections per route. - */ - private int maximumConnectionsPerRoute = 50; + @Getter + @Setter + public static class DataServices { + private Integer maximumConnectionsTotal = 100; + private Integer connectionTimeoutInSeconds = 30; + private Integer readTimeoutInSeconds = 30; + private Integer writeTimeoutInSeconds = 30; + private Integer maximumInMemorySizeInMegabytes = 1; + } - /** - * The maximum total number of open connections. - */ - private int maximumConnectionsTotal = maximumConnectionsPerRoute * 2; - - /** - * The duration after which idle connections are evicted. - */ - @DurationUnit(ChronoUnit.SECONDS) - private Duration idleConnectionEvictionThresholdInSeconds = Duration.ofSeconds(5); + @Getter + @Setter + public static class ModelServices { + private Integer maximumConnectionsTotal = 100; + private Integer connectionTimeoutInSeconds = 30; + private Integer readTimeoutInSeconds = 30; + private Integer writeTimeoutInSeconds = 30; + private Integer maximumInMemorySizeInMegabytes = 1; + } + @Getter + public static class HealthCheckServices { + private final Integer maximumConnectionsTotal = 10; + private final Integer connectionTimeoutInSeconds = 30; + private final Integer readTimeoutInSeconds = 30; + private final Integer writeTimeoutInSeconds = 30; + private final Integer maximumInMemorySizeInMegabytes = 1; + } } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/NcmpConfiguration.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/NcmpConfiguration.java deleted file mode 100644 index c6ff116a7..000000000 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/NcmpConfiguration.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * Copyright (C) 2021-2023 Nordix Foundation - * ================================================================================ - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - * ============LICENSE_END========================================================= - */ - -package org.onap.cps.ncmp.api.impl.config; - -import java.util.Arrays; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import org.apache.hc.client5.http.config.ConnectionConfig; -import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; -import org.apache.hc.client5.http.impl.classic.HttpClients; -import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; -import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; -import org.apache.hc.core5.util.TimeValue; -import org.apache.hc.core5.util.Timeout; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.beans.factory.config.ConfigurableBeanFactory; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.boot.web.client.RestTemplateBuilder; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Scope; -import org.springframework.http.MediaType; -import org.springframework.http.client.ClientHttpRequestFactory; -import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; -import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; -import org.springframework.stereotype.Component; -import org.springframework.web.client.RestTemplate; - -@Configuration -@EnableConfigurationProperties(HttpClientConfiguration.class) -@RequiredArgsConstructor(access = AccessLevel.PROTECTED) -public class NcmpConfiguration { - - @Getter - @Component - public static class DmiProperties { - @Value("${ncmp.dmi.auth.username}") - private String authUsername; - @Value("${ncmp.dmi.auth.password}") - private String authPassword; - @Value("${ncmp.dmi.api.base-path}") - private String dmiBasePath; - @Value("${ncmp.dmi.auth.enabled}") - private boolean dmiBasicAuthEnabled; - } - - /** - * Rest template bean. - * - * @param restTemplateBuilder the rest template builder - * @param httpClientConfiguration the http client configuration - * @return rest template instance - */ - @Bean - @Scope(ConfigurableBeanFactory.SCOPE_SINGLETON) - public static RestTemplate restTemplate(final RestTemplateBuilder restTemplateBuilder, - final HttpClientConfiguration httpClientConfiguration) { - - final ConnectionConfig connectionConfig = ConnectionConfig.copy(ConnectionConfig.DEFAULT) - .setConnectTimeout(Timeout.of(httpClientConfiguration.getConnectionTimeoutInSeconds())) - .build(); - - final PoolingHttpClientConnectionManager connectionManager = PoolingHttpClientConnectionManagerBuilder.create() - .setDefaultConnectionConfig(connectionConfig) - .setMaxConnTotal(httpClientConfiguration.getMaximumConnectionsTotal()) - .setMaxConnPerRoute(httpClientConfiguration.getMaximumConnectionsPerRoute()) - .build(); - - final CloseableHttpClient httpClient = HttpClients.custom() - .setConnectionManager(connectionManager) - .evictExpiredConnections() - .evictIdleConnections( - TimeValue.of(httpClientConfiguration.getIdleConnectionEvictionThresholdInSeconds())) - .build(); - - final ClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient); - - final RestTemplate restTemplate = restTemplateBuilder - .requestFactory(() -> requestFactory) - .setConnectTimeout(httpClientConfiguration.getConnectionTimeoutInSeconds()) - .build(); - - setRestTemplateMessageConverters(restTemplate); - return restTemplate; - } - - private static void setRestTemplateMessageConverters(final RestTemplate restTemplate) { - final MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter = - new MappingJackson2HttpMessageConverter(); - mappingJackson2HttpMessageConverter.setSupportedMediaTypes( - Arrays.asList(MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN)); - restTemplate.getMessageConverters().add(mappingJackson2HttpMessageConverter); - } - -} diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/OpenTelemetryConfig.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/OpenTelemetryConfig.java new file mode 100644 index 000000000..bcbacbd42 --- /dev/null +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/OpenTelemetryConfig.java @@ -0,0 +1,111 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps.ncmp.api.impl.config; + +import io.micrometer.observation.ObservationPredicate; +import io.micrometer.observation.ObservationRegistry; +import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter; +import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; +import io.opentelemetry.sdk.extension.trace.jaeger.sampler.JaegerRemoteSampler; +import io.opentelemetry.sdk.trace.samplers.Sampler; +import java.time.Duration; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.actuate.autoconfigure.observation.ObservationRegistryCustomizer; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.server.observation.ServerRequestObservationContext; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.PathMatcher; + +@Configuration +public class OpenTelemetryConfig { + + public static final int JAEGER_REMOTE_SAMPLER_POLLING_INTERVAL_IN_SECOND = 30; + + @Value("${spring.application.name:cps-application}") + private String serviceId; + + @Value("${cps.tracing.exporter.endpoint:http://onap-otel-collector:4317}") + private String tracingExporterEndpointUrl; + + @Value("${cps.tracing.sampler.jaeger_remote.endpoint:http://onap-otel-collector:14250}") + private String jaegerRemoteSamplerUrl; + + /** + * OTLP Exporter with Grpc exporter protocol. + */ + @Bean + @ConditionalOnExpression( + "${cps.tracing.enabled} && 'grpc'.equals('${cps.tracing.exporter.protocol}')") + public OtlpGrpcSpanExporter createOtlpExporterGrpc() { + return OtlpGrpcSpanExporter.builder().setEndpoint(tracingExporterEndpointUrl).build(); + } + + /** + * OTLP Exporter with HTTP exporter protocol. + */ + @Bean + @ConditionalOnExpression( + "${cps.tracing.enabled} && 'http'.equals('${cps.tracing.exporter.protocol}')") + public OtlpHttpSpanExporter createOtlpExporterHttp() { + return OtlpHttpSpanExporter.builder().setEndpoint(tracingExporterEndpointUrl).build(); + } + + /** + * Jaeger Remote Sampler. + */ + @Bean + @ConditionalOnProperty("cps.tracing.enabled") + public JaegerRemoteSampler createJaegerRemoteSampler() { + return JaegerRemoteSampler.builder() + .setEndpoint(jaegerRemoteSamplerUrl) + .setPollingInterval(Duration.ofSeconds(JAEGER_REMOTE_SAMPLER_POLLING_INTERVAL_IN_SECOND)) + .setInitialSampler(Sampler.alwaysOff()) + .setServiceName(serviceId) + .build(); + } + + /** + * Excluding /actuator/** endpoints. + */ + @Bean + @ConditionalOnProperty("cps.tracing.enabled") + ObservationRegistryCustomizer<ObservationRegistry> skipActuatorEndpointsFromObservation() { + final PathMatcher pathMatcher = new AntPathMatcher("/"); + return registry -> + registry.observationConfig().observationPredicate(observationPredicate(pathMatcher)); + } + + /** + * Excluding /actuator/** endpoints. + */ + static ObservationPredicate observationPredicate(final PathMatcher pathMatcher) { + return (name, context) -> { + if (context instanceof ServerRequestObservationContext observationContext) { + return !pathMatcher.match("/actuator/**", observationContext.getCarrier().getRequestURI()); + } else { + return true; + } + }; + } +}
\ No newline at end of file diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/kafka/KafkaConfig.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/kafka/KafkaConfig.java index 167df5a98..cf6f1c5b1 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/kafka/KafkaConfig.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/kafka/KafkaConfig.java @@ -21,10 +21,14 @@ package org.onap.cps.ncmp.api.impl.config.kafka; import io.cloudevents.CloudEvent; +import io.opentelemetry.instrumentation.kafkaclients.v2_6.TracingConsumerInterceptor; +import io.opentelemetry.instrumentation.kafkaclients.v2_6.TracingProducerInterceptor; import java.time.Duration; import java.util.Map; import lombok.RequiredArgsConstructor; +import org.apache.kafka.clients.consumer.ConsumerConfig; import org.apache.kafka.clients.producer.ProducerConfig; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.kafka.KafkaProperties; import org.springframework.boot.ssl.SslBundles; import org.springframework.context.annotation.Bean; @@ -52,6 +56,9 @@ public class KafkaConfig<T> { private final KafkaProperties kafkaProperties; + @Value("${cps.tracing.enabled:false}") + private boolean tracingEnabled; + private static final SslBundles NO_SSL = null; /** @@ -64,6 +71,10 @@ public class KafkaConfig<T> { public ProducerFactory<String, T> legacyEventProducerFactory() { final Map<String, Object> producerConfigProperties = kafkaProperties.buildProducerProperties(NO_SSL); producerConfigProperties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class); + if (tracingEnabled) { + producerConfigProperties.put( + ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, TracingProducerInterceptor.class.getName()); + } return new DefaultKafkaProducerFactory<>(producerConfigProperties); } @@ -77,6 +88,10 @@ public class KafkaConfig<T> { public ConsumerFactory<String, T> legacyEventConsumerFactory() { final Map<String, Object> consumerConfigProperties = kafkaProperties.buildConsumerProperties(NO_SSL); consumerConfigProperties.put("spring.deserializer.value.delegate.class", JsonDeserializer.class); + if (tracingEnabled) { + consumerConfigProperties.put( + ConsumerConfig.INTERCEPTOR_CLASSES_CONFIG, TracingConsumerInterceptor.class.getName()); + } return new DefaultKafkaConsumerFactory<>(consumerConfigProperties); } @@ -90,6 +105,9 @@ public class KafkaConfig<T> { public KafkaTemplate<String, T> legacyEventKafkaTemplate() { final KafkaTemplate<String, T> kafkaTemplate = new KafkaTemplate<>(legacyEventProducerFactory()); kafkaTemplate.setConsumerFactory(legacyEventConsumerFactory()); + if (tracingEnabled) { + kafkaTemplate.setObservationEnabled(true); + } return kafkaTemplate; } @@ -104,6 +122,9 @@ public class KafkaConfig<T> { new ConcurrentKafkaListenerContainerFactory<>(); containerFactory.setConsumerFactory(legacyEventConsumerFactory()); containerFactory.getContainerProperties().setAuthExceptionRetryInterval(Duration.ofSeconds(10)); + if (tracingEnabled) { + containerFactory.getContainerProperties().setObservationEnabled(true); + } return containerFactory; } @@ -116,6 +137,10 @@ public class KafkaConfig<T> { @Bean public ProducerFactory<String, CloudEvent> cloudEventProducerFactory() { final Map<String, Object> producerConfigProperties = kafkaProperties.buildProducerProperties(NO_SSL); + if (tracingEnabled) { + producerConfigProperties.put( + ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, TracingProducerInterceptor.class.getName()); + } return new DefaultKafkaProducerFactory<>(producerConfigProperties); } @@ -128,6 +153,10 @@ public class KafkaConfig<T> { @Bean public ConsumerFactory<String, CloudEvent> cloudEventConsumerFactory() { final Map<String, Object> consumerConfigProperties = kafkaProperties.buildConsumerProperties(NO_SSL); + if (tracingEnabled) { + consumerConfigProperties.put( + ConsumerConfig.INTERCEPTOR_CLASSES_CONFIG, TracingConsumerInterceptor.class.getName()); + } return new DefaultKafkaConsumerFactory<>(consumerConfigProperties); } @@ -142,6 +171,9 @@ public class KafkaConfig<T> { final KafkaTemplate<String, CloudEvent> kafkaTemplate = new KafkaTemplate<>(cloudEventProducerFactory()); kafkaTemplate.setConsumerFactory(cloudEventConsumerFactory()); + if (tracingEnabled) { + kafkaTemplate.setObservationEnabled(true); + } return kafkaTemplate; } @@ -157,6 +189,9 @@ public class KafkaConfig<T> { new ConcurrentKafkaListenerContainerFactory<>(); containerFactory.setConsumerFactory(cloudEventConsumerFactory()); containerFactory.getContainerProperties().setAuthExceptionRetryInterval(Duration.ofSeconds(10)); + if (tracingEnabled) { + containerFactory.getContainerProperties().setObservationEnabled(true); + } return containerFactory; } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/CmNotificationSubscriptionDelta.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/CmNotificationSubscriptionDelta.java index 8a4beb956..4e2062fed 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/CmNotificationSubscriptionDelta.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/CmNotificationSubscriptionDelta.java @@ -64,12 +64,19 @@ public class CmNotificationSubscriptionDelta { } } - final DmiCmNotificationSubscriptionPredicate predicateDelta = - new DmiCmNotificationSubscriptionPredicate(targetCmHandleIds, datastoreType, xpaths); + populateValidDmiCmNotificationSubscriptionPredicateDelta(targetCmHandleIds, xpaths, datastoreType, delta); + } + return delta; + } + private void populateValidDmiCmNotificationSubscriptionPredicateDelta(final Set<String> targetCmHandleIds, + final Set<String> xpaths, final DatastoreType datastoreType, + final List<DmiCmNotificationSubscriptionPredicate> delta) { + if (!(targetCmHandleIds.isEmpty() || xpaths.isEmpty())) { + final DmiCmNotificationSubscriptionPredicate predicateDelta = + new DmiCmNotificationSubscriptionPredicate(targetCmHandleIds, datastoreType, xpaths); delta.add(predicateDelta); } - return delta; } } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/DmiCmNotificationSubscriptionCacheHandler.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/DmiCmNotificationSubscriptionCacheHandler.java index b5370bf1e..368e27a7d 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/DmiCmNotificationSubscriptionCacheHandler.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/DmiCmNotificationSubscriptionCacheHandler.java @@ -123,12 +123,12 @@ public class DmiCmNotificationSubscriptionCacheHandler { * */ public void updateDmiCmNotificationSubscriptionStatusPerDmi(final String subscriptionId, - final String dmiServiceName, final CmNotificationSubscriptionStatus status) { + final String dmiServiceName, + final CmNotificationSubscriptionStatus status) { final Map<String, DmiCmNotificationSubscriptionDetails> dmiCmNotificationSubscriptionDetailsPerDmi = cmNotificationSubscriptionCache.get(subscriptionId); dmiCmNotificationSubscriptionDetailsPerDmi.get(dmiServiceName).setCmNotificationSubscriptionStatus(status); cmNotificationSubscriptionCache.put(subscriptionId, dmiCmNotificationSubscriptionDetailsPerDmi); - } /** @@ -157,6 +157,32 @@ public class DmiCmNotificationSubscriptionCacheHandler { } } + /** + * Remove subscription from database per DMI service name. + * + * @param subscriptionId String of subscription id + * @param dmiServiceName String of dmiServiceName + * + */ + public void removeFromDatabasePerDmi(final String subscriptionId, final String dmiServiceName) { + final List<DmiCmNotificationSubscriptionPredicate> dmiCmNotificationSubscriptionPredicateList = + cmNotificationSubscriptionCache.get(subscriptionId).get(dmiServiceName) + .getDmiCmNotificationSubscriptionPredicates(); + for (final DmiCmNotificationSubscriptionPredicate dmiCmNotificationSubscriptionPredicate: + dmiCmNotificationSubscriptionPredicateList) { + final DatastoreType datastoreType = dmiCmNotificationSubscriptionPredicate.getDatastoreType(); + final Set<String> cmHandles = dmiCmNotificationSubscriptionPredicate.getTargetCmHandleIds(); + final Set<String> xpaths = dmiCmNotificationSubscriptionPredicate.getXpaths(); + + for (final String cmHandle: cmHandles) { + for (final String xpath: xpaths) { + cmNotificationSubscriptionPersistenceService.removeCmNotificationSubscription(datastoreType, + cmHandle, xpath, subscriptionId); + } + } + } + } + private void updateDmiCmNotificationSubscriptionDetailsPerDmi( final String dmiServiceName, final DmiCmNotificationSubscriptionPredicate dmiCmNotificationSubscriptionPredicate, diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/consumer/CmNotificationSubscriptionDmiOutEventConsumer.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/consumer/CmNotificationSubscriptionDmiOutEventConsumer.java index fb89aae3f..051949c5e 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/consumer/CmNotificationSubscriptionDmiOutEventConsumer.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/consumer/CmNotificationSubscriptionDmiOutEventConsumer.java @@ -20,6 +20,8 @@ package org.onap.cps.ncmp.api.impl.events.cmsubscription.consumer; +import static org.onap.cps.ncmp.api.NcmpResponseStatus.CM_DATA_SUBSCRIPTION_ACCEPTED; +import static org.onap.cps.ncmp.api.NcmpResponseStatus.CM_DATA_SUBSCRIPTION_REJECTED; import static org.onap.cps.ncmp.api.impl.events.mapper.CloudEventMapper.toTargetEvent; import io.cloudevents.CloudEvent; @@ -27,12 +29,14 @@ import java.util.Map; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.onap.cps.ncmp.api.NcmpResponseStatus; import org.onap.cps.ncmp.api.impl.events.cmsubscription.CmNotificationSubscriptionEventsHandler; import org.onap.cps.ncmp.api.impl.events.cmsubscription.CmNotificationSubscriptionMappersHandler; import org.onap.cps.ncmp.api.impl.events.cmsubscription.DmiCmNotificationSubscriptionCacheHandler; import org.onap.cps.ncmp.api.impl.events.cmsubscription.model.CmNotificationSubscriptionStatus; import org.onap.cps.ncmp.api.impl.events.cmsubscription.model.DmiCmNotificationSubscriptionDetails; import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.dmi_to_ncmp.CmNotificationSubscriptionDmiOutEvent; +import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.dmi_to_ncmp.Data; import org.onap.cps.ncmp.events.cmsubscription_merge1_0_0.ncmp_to_client.CmNotificationSubscriptionNcmpOutEvent; import org.springframework.kafka.annotation.KafkaListener; import org.springframework.stereotype.Component; @@ -46,12 +50,14 @@ public class CmNotificationSubscriptionDmiOutEventConsumer { private final CmNotificationSubscriptionEventsHandler cmNotificationSubscriptionEventsHandler; private final CmNotificationSubscriptionMappersHandler cmNotificationSubscriptionMappersHandler; + private static final String CM_DATA_SUBSCRIPTION_CORRELATION_ID_SEPARATOR = "#"; + /** * Consume the Cm Notification Subscription event from the dmi-plugin. * * @param cmNotificationSubscriptionDmiOutEventConsumerRecord the event to be consumed */ - @KafkaListener(topics = "${app.ncmp.avc.subscription-response-topic}", + @KafkaListener(topics = "${app.ncmp.avc.cm-subscription-dmi-out}", containerFactory = "cloudEventConcurrentKafkaListenerContainerFactory") public void consumeCmNotificationSubscriptionDmiOutEvent( final ConsumerRecord<String, CloudEvent> cmNotificationSubscriptionDmiOutEventConsumerRecord) { @@ -59,26 +65,33 @@ public class CmNotificationSubscriptionDmiOutEventConsumer { final CmNotificationSubscriptionDmiOutEvent cmNotificationSubscriptionDmiOutEvent = toTargetEvent(cloudEvent, CmNotificationSubscriptionDmiOutEvent.class); final String correlationId = String.valueOf(cloudEvent.getExtension("correlationid")); - if ("subscriptionCreateResponse".equals(cloudEvent.getType()) && cmNotificationSubscriptionDmiOutEvent != null - && correlationId != null) { - handleCmSubscriptionCreate(correlationId, cmNotificationSubscriptionDmiOutEvent); + if (cmNotificationSubscriptionDmiOutEvent != null && correlationId != null) { + final String eventType = cloudEvent.getType(); + handleCmSubscriptionDmiOutEvent(correlationId, eventType, cmNotificationSubscriptionDmiOutEvent); } } - private void handleCmSubscriptionCreate(final String correlationId, - final CmNotificationSubscriptionDmiOutEvent cmNotificationSubscriptionDmiOutEvent) { - final String subscriptionId = correlationId.split("#")[0]; - final String dmiPluginName = correlationId.split("#")[1]; + private void handleCmSubscriptionDmiOutEvent(final String correlationId, + final String eventType, + final CmNotificationSubscriptionDmiOutEvent + cmNotificationSubscriptionDmiOutEvent) { + final String subscriptionId = correlationId.split(CM_DATA_SUBSCRIPTION_CORRELATION_ID_SEPARATOR)[0]; + final String dmiPluginName = correlationId.split(CM_DATA_SUBSCRIPTION_CORRELATION_ID_SEPARATOR)[1]; - if ("ACCEPTED".equals(cmNotificationSubscriptionDmiOutEvent.getData().getStatusMessage())) { + if (checkStatusCodeAndMessage(CM_DATA_SUBSCRIPTION_ACCEPTED, cmNotificationSubscriptionDmiOutEvent.getData())) { handleCacheStatusPerDmi(subscriptionId, dmiPluginName, CmNotificationSubscriptionStatus.ACCEPTED); - dmiCmNotificationSubscriptionCacheHandler.persistIntoDatabasePerDmi(subscriptionId, dmiPluginName); - handleEventsStatusPerDmi(subscriptionId); + if (eventType.equals("subscriptionCreateResponse")) { + dmiCmNotificationSubscriptionCacheHandler.persistIntoDatabasePerDmi(subscriptionId, dmiPluginName); + } + if (eventType.equals("subscriptionDeleteResponse")) { + dmiCmNotificationSubscriptionCacheHandler.removeFromDatabasePerDmi(subscriptionId, dmiPluginName); + } + handleEventsStatusPerDmi(subscriptionId, eventType); } - if ("REJECTED".equals(cmNotificationSubscriptionDmiOutEvent.getData().getStatusMessage())) { + if (checkStatusCodeAndMessage(CM_DATA_SUBSCRIPTION_REJECTED, cmNotificationSubscriptionDmiOutEvent.getData())) { handleCacheStatusPerDmi(subscriptionId, dmiPluginName, CmNotificationSubscriptionStatus.REJECTED); - handleEventsStatusPerDmi(subscriptionId); + handleEventsStatusPerDmi(subscriptionId, eventType); } log.info("Cm Subscription with id : {} handled by the dmi-plugin : {} has the status : {}", subscriptionId, @@ -86,18 +99,25 @@ public class CmNotificationSubscriptionDmiOutEventConsumer { } private void handleCacheStatusPerDmi(final String subscriptionId, final String dmiPluginName, - final CmNotificationSubscriptionStatus cmNotificationSubscriptionStatus) { + final CmNotificationSubscriptionStatus cmNotificationSubscriptionStatus) { dmiCmNotificationSubscriptionCacheHandler.updateDmiCmNotificationSubscriptionStatusPerDmi(subscriptionId, dmiPluginName, cmNotificationSubscriptionStatus); } - private void handleEventsStatusPerDmi(final String subscriptionId) { + private void handleEventsStatusPerDmi(final String subscriptionId, final String eventType) { final Map<String, DmiCmNotificationSubscriptionDetails> dmiCmNotificationSubscriptionDetailsPerDmi = dmiCmNotificationSubscriptionCacheHandler.get(subscriptionId); final CmNotificationSubscriptionNcmpOutEvent cmNotificationSubscriptionNcmpOutEvent = cmNotificationSubscriptionMappersHandler.toCmNotificationSubscriptionNcmpOutEvent(subscriptionId, dmiCmNotificationSubscriptionDetailsPerDmi); cmNotificationSubscriptionEventsHandler.publishCmNotificationSubscriptionNcmpOutEvent(subscriptionId, - "subscriptionCreateResponse", cmNotificationSubscriptionNcmpOutEvent, false); + eventType, cmNotificationSubscriptionNcmpOutEvent, false); + } + + private boolean checkStatusCodeAndMessage(final NcmpResponseStatus ncmpResponseStatus, + final Data cmNotificationSubscriptionDmiOutData) { + return ncmpResponseStatus.getCode().equals(cmNotificationSubscriptionDmiOutData.getStatusCode()) + && ncmpResponseStatus.getMessage() + .equals(cmNotificationSubscriptionDmiOutData.getStatusMessage()); } -} +}
\ No newline at end of file diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/consumer/CmNotificationSubscriptionNcmpInEventConsumer.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/consumer/CmNotificationSubscriptionNcmpInEventConsumer.java index 70135b307..fb3388c11 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/consumer/CmNotificationSubscriptionNcmpInEventConsumer.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/consumer/CmNotificationSubscriptionNcmpInEventConsumer.java @@ -23,12 +23,13 @@ package org.onap.cps.ncmp.api.impl.events.cmsubscription.consumer; import static org.onap.cps.ncmp.api.impl.events.mapper.CloudEventMapper.toTargetEvent; import io.cloudevents.CloudEvent; +import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.kafka.clients.consumer.ConsumerRecord; import org.onap.cps.ncmp.api.impl.events.cmsubscription.service.CmNotificationSubscriptionHandlerService; import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.client_to_ncmp.CmNotificationSubscriptionNcmpInEvent; -import org.springframework.beans.factory.annotation.Value; +import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.client_to_ncmp.Predicate; import org.springframework.kafka.annotation.KafkaListener; import org.springframework.stereotype.Component; @@ -39,15 +40,12 @@ public class CmNotificationSubscriptionNcmpInEventConsumer { private final CmNotificationSubscriptionHandlerService cmNotificationSubscriptionHandlerService; - @Value("${notification.enabled:true}") - private boolean notificationFeatureEnabled; - /** * Consume the specified event. * * @param subscriptionEventConsumerRecord the event to be consumed */ - @KafkaListener(topics = "${app.ncmp.avc.subscription-topic}", + @KafkaListener(topics = "${app.ncmp.avc.cm-subscription-ncmp-in}", containerFactory = "cloudEventConcurrentKafkaListenerContainerFactory") public void consumeSubscriptionEvent(final ConsumerRecord<String, CloudEvent> subscriptionEventConsumerRecord) { final CloudEvent cloudEvent = subscriptionEventConsumerRecord.value(); @@ -57,10 +55,16 @@ public class CmNotificationSubscriptionNcmpInEventConsumer { cmNotificationSubscriptionNcmpInEvent.getData().getSubscriptionId()); final String subscriptionId = cmNotificationSubscriptionNcmpInEvent.getData().getSubscriptionId(); + final List<Predicate> predicates = cmNotificationSubscriptionNcmpInEvent.getData().getPredicates(); if ("subscriptionCreateRequest".equals(cloudEvent.getType())) { - log.info("Subscription for source {} with subscription id {} ...", cloudEvent.getSource(), subscriptionId); - cmNotificationSubscriptionHandlerService.processSubscriptionCreateRequest( - cmNotificationSubscriptionNcmpInEvent); + log.info("Subscription create request for source {} with subscription id {} ...", + cloudEvent.getSource(), subscriptionId); + cmNotificationSubscriptionHandlerService.processSubscriptionCreateRequest(subscriptionId, predicates); + } + if ("subscriptionDeleteRequest".equals(cloudEvent.getType())) { + log.info("Subscription delete request for source {} with subscription id {} ...", + cloudEvent.getSource(), subscriptionId); + cmNotificationSubscriptionHandlerService.processSubscriptionDeleteRequest(subscriptionId, predicates); } } }
\ No newline at end of file diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/mapper/CmNotificationSubscriptionDmiInEventMapper.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/mapper/CmNotificationSubscriptionDmiInEventMapper.java index 489401f26..761068748 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/mapper/CmNotificationSubscriptionDmiInEventMapper.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/mapper/CmNotificationSubscriptionDmiInEventMapper.java @@ -29,8 +29,8 @@ import java.util.Set; import lombok.RequiredArgsConstructor; import org.onap.cps.ncmp.api.impl.events.cmsubscription.model.DmiCmNotificationSubscriptionPredicate; import org.onap.cps.ncmp.api.impl.inventory.InventoryPersistence; +import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.ncmp_to_dmi.CmHandle; import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.ncmp_to_dmi.CmNotificationSubscriptionDmiInEvent; -import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.ncmp_to_dmi.Cmhandle; import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.ncmp_to_dmi.Data; import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.ncmp_to_dmi.Predicate; import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.ncmp_to_dmi.ScopeFilter; @@ -54,7 +54,7 @@ public class CmNotificationSubscriptionDmiInEventMapper { new CmNotificationSubscriptionDmiInEvent(); final Data cmSubscriptionData = new Data(); cmSubscriptionData.setPredicates(mapToDmiInEventPredicates(dmiCmNotificationSubscriptionPredicates)); - cmSubscriptionData.setCmhandles(mapToCmSubscriptionCmhandleWithPrivateProperties( + cmSubscriptionData.setCmHandles(mapToCmSubscriptionCmhandleWithPrivateProperties( extractUniqueCmHandleIds(dmiCmNotificationSubscriptionPredicates))); cmNotificationSubscriptionDmiInEvent.setData(cmSubscriptionData); return cmNotificationSubscriptionDmiInEvent; @@ -81,12 +81,12 @@ public class CmNotificationSubscriptionDmiInEventMapper { } - private List<Cmhandle> mapToCmSubscriptionCmhandleWithPrivateProperties(final Set<String> cmHandleIds) { + private List<CmHandle> mapToCmSubscriptionCmhandleWithPrivateProperties(final Set<String> cmHandleIds) { - final List<Cmhandle> cmSubscriptionCmHandles = new ArrayList<>(); + final List<CmHandle> cmSubscriptionCmHandles = new ArrayList<>(); inventoryPersistence.getYangModelCmHandles(cmHandleIds).forEach(yangModelCmHandle -> { - final Cmhandle cmhandle = new Cmhandle(); + final CmHandle cmhandle = new CmHandle(); final Map<String, String> cmhandleDmiProperties = new LinkedHashMap<>(); yangModelCmHandle.getDmiProperties() .forEach(dmiProperty -> cmhandleDmiProperties.put(dmiProperty.getName(), dmiProperty.getValue())); diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/producer/CmNotificationSubscriptionDmiInEventProducer.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/producer/CmNotificationSubscriptionDmiInEventProducer.java index 9fbe26848..3273c556c 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/producer/CmNotificationSubscriptionDmiInEventProducer.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/producer/CmNotificationSubscriptionDmiInEventProducer.java @@ -25,7 +25,6 @@ import io.cloudevents.core.builder.CloudEventBuilder; import java.net.URI; import java.util.UUID; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.onap.cps.events.EventsPublisher; import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.ncmp_to_dmi.CmNotificationSubscriptionDmiInEvent; import org.onap.cps.utils.JsonObjectMapper; @@ -34,7 +33,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Component; @Component -@Slf4j @RequiredArgsConstructor @ConditionalOnProperty(name = "notification.enabled", havingValue = "true", matchIfMissing = true) public class CmNotificationSubscriptionDmiInEventProducer { @@ -42,7 +40,7 @@ public class CmNotificationSubscriptionDmiInEventProducer { private final EventsPublisher<CloudEvent> eventsPublisher; private final JsonObjectMapper jsonObjectMapper; - @Value("${app.ncmp.avc.subscription-forward-topic-prefix}") + @Value("${app.ncmp.avc.cm-subscription-dmi-in}") private String cmNotificationSubscriptionDmiInEventTopic; /** @@ -65,9 +63,10 @@ public class CmNotificationSubscriptionDmiInEventProducer { final String dmiPluginName, final String eventType, final CmNotificationSubscriptionDmiInEvent cmNotificationSubscriptionDmiInEvent) { return CloudEventBuilder.v1().withId(UUID.randomUUID().toString()).withType(eventType) - .withSource(URI.create("NCMP")).withDataSchema(URI.create("org.onap.ncmp.dmi.cm.subscription:1.0.0")) - .withExtension("correlationid", subscriptionId.concat("#").concat(dmiPluginName)) - .withData(jsonObjectMapper.asJsonBytes(cmNotificationSubscriptionDmiInEvent)).build(); + .withSource(URI.create("NCMP")) + .withDataSchema(URI.create("org.onap.ncmp.dmi.cm.subscription:1.0.0")) + .withExtension("correlationid", subscriptionId.concat("#").concat(dmiPluginName)) + .withData(jsonObjectMapper.asJsonBytes(cmNotificationSubscriptionDmiInEvent)).build(); } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/producer/CmNotificationSubscriptionNcmpOutEventProducer.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/producer/CmNotificationSubscriptionNcmpOutEventProducer.java index ac5de07f0..ed7ed2a0b 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/producer/CmNotificationSubscriptionNcmpOutEventProducer.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/producer/CmNotificationSubscriptionNcmpOutEventProducer.java @@ -48,7 +48,7 @@ import org.springframework.stereotype.Component; @ConditionalOnProperty(name = "notification.enabled", havingValue = "true", matchIfMissing = true) public class CmNotificationSubscriptionNcmpOutEventProducer { - @Value("${app.ncmp.avc.subscription-outcome-topic}") + @Value("${app.ncmp.avc.cm-subscription-ncmp-out}") private String cmNotificationSubscriptionNcmpOutEventTopic; @Value("${ncmp.timers.subscription-forwarding.dmi-response-timeout-ms}") diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/service/CmNotificationSubscriptionHandlerService.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/service/CmNotificationSubscriptionHandlerService.java index 536693ee4..1c52ffa79 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/service/CmNotificationSubscriptionHandlerService.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/service/CmNotificationSubscriptionHandlerService.java @@ -20,16 +20,25 @@ package org.onap.cps.ncmp.api.impl.events.cmsubscription.service; -import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.client_to_ncmp.CmNotificationSubscriptionNcmpInEvent; +import java.util.List; +import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.client_to_ncmp.Predicate; public interface CmNotificationSubscriptionHandlerService { /** - * Process cm notification subscription request. + * Process cm notification subscription create request. * - * @param cmNotificationSubscriptionNcmpInEvent CM Notification Subscription event + * @param subscriptionId subscription id + * @param predicates subscription predicates */ - void processSubscriptionCreateRequest( - final CmNotificationSubscriptionNcmpInEvent cmNotificationSubscriptionNcmpInEvent); + void processSubscriptionCreateRequest(final String subscriptionId, final List<Predicate> predicates); -} + /** + * Process cm notification subscription delete request. + * + * @param subscriptionId subscription id + * @param predicates subscription predicates + */ + void processSubscriptionDeleteRequest(final String subscriptionId, final List<Predicate> predicates); + +}
\ No newline at end of file diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/service/CmNotificationSubscriptionHandlerServiceImpl.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/service/CmNotificationSubscriptionHandlerServiceImpl.java index 7872ba0a3..08e3c9552 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/service/CmNotificationSubscriptionHandlerServiceImpl.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/service/CmNotificationSubscriptionHandlerServiceImpl.java @@ -30,9 +30,9 @@ import org.onap.cps.ncmp.api.impl.events.cmsubscription.CmNotificationSubscripti import org.onap.cps.ncmp.api.impl.events.cmsubscription.CmNotificationSubscriptionEventsHandler; import org.onap.cps.ncmp.api.impl.events.cmsubscription.CmNotificationSubscriptionMappersHandler; import org.onap.cps.ncmp.api.impl.events.cmsubscription.DmiCmNotificationSubscriptionCacheHandler; +import org.onap.cps.ncmp.api.impl.events.cmsubscription.model.CmNotificationSubscriptionStatus; import org.onap.cps.ncmp.api.impl.events.cmsubscription.model.DmiCmNotificationSubscriptionDetails; import org.onap.cps.ncmp.api.impl.events.cmsubscription.model.DmiCmNotificationSubscriptionPredicate; -import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.client_to_ncmp.CmNotificationSubscriptionNcmpInEvent; import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.client_to_ncmp.Predicate; import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.ncmp_to_dmi.CmNotificationSubscriptionDmiInEvent; import org.onap.cps.ncmp.events.cmsubscription_merge1_0_0.ncmp_to_client.CmNotificationSubscriptionNcmpOutEvent; @@ -49,28 +49,32 @@ public class CmNotificationSubscriptionHandlerServiceImpl implements CmNotificat private final DmiCmNotificationSubscriptionCacheHandler dmiCmNotificationSubscriptionCacheHandler; @Override - public void processSubscriptionCreateRequest( - final CmNotificationSubscriptionNcmpInEvent cmNotificationSubscriptionNcmpInEvent) { - final String subscriptionId = cmNotificationSubscriptionNcmpInEvent.getData().getSubscriptionId(); - final List<Predicate> predicates = cmNotificationSubscriptionNcmpInEvent.getData().getPredicates(); - + public void processSubscriptionCreateRequest(final String subscriptionId, final List<Predicate> predicates) { if (cmNotificationSubscriptionPersistenceService.isUniqueSubscriptionId(subscriptionId)) { dmiCmNotificationSubscriptionCacheHandler.add(subscriptionId, predicates); - sendSubscriptionCreateRequestToDmi(subscriptionId); - scheduleCmNotificationSubscriptionNcmpOutEventResponse(subscriptionId); + handleCmNotificationSubscriptionDelta(subscriptionId); + scheduleCmNotificationSubscriptionNcmpOutEventResponse(subscriptionId, + "subscriptionCreateResponse"); } else { - rejectAndPublishCmNotificationSubscriptionCreateRequest(subscriptionId, - predicates); + rejectAndPublishCmNotificationSubscriptionCreateRequest(subscriptionId, predicates); } } - private void scheduleCmNotificationSubscriptionNcmpOutEventResponse(final String subscriptionId) { + @Override + public void processSubscriptionDeleteRequest(final String subscriptionId, final List<Predicate> predicates) { + dmiCmNotificationSubscriptionCacheHandler.add(subscriptionId, predicates); + sendSubscriptionDeleteRequestToDmi(subscriptionId); + scheduleCmNotificationSubscriptionNcmpOutEventResponse(subscriptionId, "subscriptionDeleteResponse"); + } + + private void scheduleCmNotificationSubscriptionNcmpOutEventResponse(final String subscriptionId, + final String eventType) { cmNotificationSubscriptionEventsHandler.publishCmNotificationSubscriptionNcmpOutEvent(subscriptionId, - "subscriptionCreateResponse", null, true); + eventType, null, true); } private void rejectAndPublishCmNotificationSubscriptionCreateRequest(final String subscriptionId, - final List<Predicate> predicates) { + final List<Predicate> predicates) { final Set<String> subscriptionTargetFilters = predicates.stream().flatMap(predicate -> predicate.getTargetFilter().stream()) .collect(Collectors.toSet()); @@ -81,18 +85,50 @@ public class CmNotificationSubscriptionHandlerServiceImpl implements CmNotificat "subscriptionCreateResponse", cmNotificationSubscriptionNcmpOutEvent, false); } - private void sendSubscriptionCreateRequestToDmi(final String subscriptionId) { + private void handleCmNotificationSubscriptionDelta(final String subscriptionId) { final Map<String, DmiCmNotificationSubscriptionDetails> dmiCmNotificationSubscriptionDetailsMap = dmiCmNotificationSubscriptionCacheHandler.get(subscriptionId); dmiCmNotificationSubscriptionDetailsMap.forEach((dmiPluginName, dmiCmNotificationSubscriptionDetails) -> { final List<DmiCmNotificationSubscriptionPredicate> dmiCmNotificationSubscriptionPredicates = cmNotificationSubscriptionDelta.getDelta( dmiCmNotificationSubscriptionDetails.getDmiCmNotificationSubscriptionPredicates()); + + if (dmiCmNotificationSubscriptionPredicates.isEmpty()) { + acceptAndPublishCmNotificationSubscriptionNcmpOutEventPerDmi(subscriptionId, dmiPluginName); + } else { + publishCmNotificationSubscriptionDmiInEventPerDmi(subscriptionId, dmiPluginName, + dmiCmNotificationSubscriptionPredicates); + } + }); + } + + private void publishCmNotificationSubscriptionDmiInEventPerDmi(final String subscriptionId, + final String dmiPluginName, + final List<DmiCmNotificationSubscriptionPredicate> + dmiCmNotificationSubscriptionPredicates) { + final CmNotificationSubscriptionDmiInEvent cmNotificationSubscriptionDmiInEvent = + cmNotificationSubscriptionMappersHandler.toCmNotificationSubscriptionDmiInEvent( + dmiCmNotificationSubscriptionPredicates); + cmNotificationSubscriptionEventsHandler.publishCmNotificationSubscriptionDmiInEvent(subscriptionId, + dmiPluginName, "subscriptionCreateRequest", cmNotificationSubscriptionDmiInEvent); + } + + private void acceptAndPublishCmNotificationSubscriptionNcmpOutEventPerDmi(final String subscriptionId, + final String dmiPluginName) { + dmiCmNotificationSubscriptionCacheHandler.updateDmiCmNotificationSubscriptionStatusPerDmi(subscriptionId, + dmiPluginName, CmNotificationSubscriptionStatus.ACCEPTED); + dmiCmNotificationSubscriptionCacheHandler.persistIntoDatabasePerDmi(subscriptionId, dmiPluginName); + } + + private void sendSubscriptionDeleteRequestToDmi(final String subscriptionId) { + final Map<String, DmiCmNotificationSubscriptionDetails> dmiCmNotificationSubscriptionDetailsMap = + dmiCmNotificationSubscriptionCacheHandler.get(subscriptionId); + dmiCmNotificationSubscriptionDetailsMap.forEach((dmiPluginName, dmiCmNotificationSubscriptionDetails) -> { final CmNotificationSubscriptionDmiInEvent cmNotificationSubscriptionDmiInEvent = cmNotificationSubscriptionMappersHandler.toCmNotificationSubscriptionDmiInEvent( - dmiCmNotificationSubscriptionPredicates); + dmiCmNotificationSubscriptionDetails.getDmiCmNotificationSubscriptionPredicates()); cmNotificationSubscriptionEventsHandler.publishCmNotificationSubscriptionDmiInEvent(subscriptionId, - dmiPluginName, "subscriptionCreateRequest", cmNotificationSubscriptionDmiInEvent); + dmiPluginName, "subscriptionDeleteRequest", cmNotificationSubscriptionDmiInEvent); }); } }
\ No newline at end of file diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/service/CmNotificationSubscriptionPersistenceServiceImpl.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/service/CmNotificationSubscriptionPersistenceServiceImpl.java index 0adf225fe..a9b1e26f5 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/service/CmNotificationSubscriptionPersistenceServiceImpl.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/service/CmNotificationSubscriptionPersistenceServiceImpl.java @@ -1,6 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2024 Nordix Foundation + * Modifications Copyright (C) 2024 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +21,7 @@ package org.onap.cps.ncmp.api.impl.events.cmsubscription.service; +import static org.onap.cps.spi.FetchDescendantsOption.DIRECT_CHILDREN_ONLY; import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS; import java.io.Serializable; @@ -45,10 +47,14 @@ public class CmNotificationSubscriptionPersistenceServiceImpl implements CmNotif private static final String SUBSCRIPTION_ANCHOR_NAME = "cm-data-subscriptions"; private static final String CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_WITH_DATASTORE_AND_CMHANDLE = """ - /datastores/datastore[@name='%s']/cm-handles/cm-handle[@id='%s']/filters + /datastores/datastore[@name='%s']/cm-handles/cm-handle[@id='%s'] """.trim(); + private static final String CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_FILTERS_WITH_DATASTORE_AND_CMHANDLE = + CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_WITH_DATASTORE_AND_CMHANDLE + "/filters"; + private static final String CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_WITH_DATASTORE_CMHANDLE_AND_XPATH = - CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_WITH_DATASTORE_AND_CMHANDLE + "/filter[@xpath='%s']"; + CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_FILTERS_WITH_DATASTORE_AND_CMHANDLE + "/filter[@xpath='%s']"; + private static final String CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_WITH_ID = """ //filter/subscriptionIds[text()='%s'] @@ -106,12 +112,11 @@ public class CmNotificationSubscriptionPersistenceServiceImpl implements CmNotif final Collection<String> subscriptionIds = getOngoingCmNotificationSubscriptionIds(datastoreType, cmHandleId, xpath); if (subscriptionIds.remove(subscriptionId)) { - if (isOngoingCmNotificationSubscription(datastoreType, cmHandleId, xpath)) { - saveSubscriptionDetails(datastoreType, cmHandleId, xpath, subscriptionIds); - log.info("There are subscribers left for the following cps path {} :", - CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_WITH_DATASTORE_CMHANDLE_AND_XPATH.formatted( - datastoreType.getDatastoreName(), cmHandleId, escapeQuotesByDoublingThem(xpath))); - } else { + saveSubscriptionDetails(datastoreType, cmHandleId, xpath, subscriptionIds); + log.info("There are subscribers left for the following cps path {} :", + CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_WITH_DATASTORE_CMHANDLE_AND_XPATH.formatted( + datastoreType.getDatastoreName(), cmHandleId, escapeQuotesByDoublingThem(xpath))); + if (subscriptionIds.isEmpty()) { log.info("No subscribers left for the following cps path {} :", CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_WITH_DATASTORE_CMHANDLE_AND_XPATH.formatted( datastoreType.getDatastoreName(), cmHandleId, escapeQuotesByDoublingThem(xpath))); @@ -126,11 +131,25 @@ public class CmNotificationSubscriptionPersistenceServiceImpl implements CmNotif CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_WITH_DATASTORE_CMHANDLE_AND_XPATH.formatted( datastoreType.getDatastoreName(), cmHandleId, escapeQuotesByDoublingThem(xpath)), OffsetDateTime.now()); + final Collection<DataNode> existingFiltersForCmHandle = + cpsQueryService.queryDataNodes(NCMP_DATASPACE_NAME, CM_SUBSCRIPTIONS_ANCHOR_NAME, + CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_FILTERS_WITH_DATASTORE_AND_CMHANDLE.formatted( + datastoreType.getDatastoreName(), cmHandleId), + DIRECT_CHILDREN_ONLY).iterator().next().getChildDataNodes(); + if (existingFiltersForCmHandle.isEmpty()) { + removeCmHandleFromDatastore(datastoreType.getDatastoreName(), cmHandleId); + } + } + + private void removeCmHandleFromDatastore(final String datastoreName, final String cmHandleId) { + cpsDataService.deleteDataNode(NCMP_DATASPACE_NAME, SUBSCRIPTION_ANCHOR_NAME, + CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_WITH_DATASTORE_AND_CMHANDLE.formatted( + datastoreName, cmHandleId), OffsetDateTime.now()); } private boolean isFirstSubscriptionForCmHandle(final DatastoreType datastoreType, final String cmHandleId) { return cpsQueryService.queryDataNodes(NCMP_DATASPACE_NAME, CM_SUBSCRIPTIONS_ANCHOR_NAME, - CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_WITH_DATASTORE_AND_CMHANDLE.formatted( + CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_FILTERS_WITH_DATASTORE_AND_CMHANDLE.formatted( datastoreType.getDatastoreName(), cmHandleId), OMIT_DESCENDANTS).isEmpty(); } @@ -150,7 +169,7 @@ public class CmNotificationSubscriptionPersistenceServiceImpl implements CmNotif OffsetDateTime.now(), ContentType.JSON); } else { cpsDataService.saveListElements(NCMP_DATASPACE_NAME, CM_SUBSCRIPTIONS_ANCHOR_NAME, - CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_WITH_DATASTORE_AND_CMHANDLE.formatted( + CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_FILTERS_WITH_DATASTORE_AND_CMHANDLE.formatted( datastoreType.getDatastoreName(), cmHandleId), subscriptionDetailsAsJson, OffsetDateTime.now()); } @@ -161,8 +180,9 @@ public class CmNotificationSubscriptionPersistenceServiceImpl implements CmNotif final Collection<String> subscriptionIds) { final String subscriptionDetailsAsJson = getSubscriptionDetailsAsJson(xpath, subscriptionIds); cpsDataService.updateNodeLeaves(NCMP_DATASPACE_NAME, CM_SUBSCRIPTIONS_ANCHOR_NAME, - CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_WITH_DATASTORE_AND_CMHANDLE.formatted( - datastoreType.getDatastoreName(), cmHandleId), subscriptionDetailsAsJson, OffsetDateTime.now()); + CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_FILTERS_WITH_DATASTORE_AND_CMHANDLE.formatted( + datastoreType.getDatastoreName(), cmHandleId), subscriptionDetailsAsJson, + OffsetDateTime.now(), ContentType.JSON); } private String getSubscriptionDetailsAsJson(final String xpath, final Collection<String> subscriptionIds) { diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/exception/DmiClientRequestException.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/exception/DmiClientRequestException.java new file mode 100644 index 000000000..ab0fa6893 --- /dev/null +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/exception/DmiClientRequestException.java @@ -0,0 +1,54 @@ +/* + * ============LICENSE_START======================================================= + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps.ncmp.api.impl.exception; + +import lombok.Getter; +import org.onap.cps.ncmp.api.NcmpResponseStatus; + +/** + * Http Client Request exception from dmi service. + */ +@Getter +public class DmiClientRequestException extends NcmpException { + + private static final long serialVersionUID = 6659897770659834797L; + final NcmpResponseStatus ncmpResponseStatus; + final String message; + final String responseBodyAsString; + final int httpStatusCode; + + /** + * Constructor to form exception for dmi service response. + * + * @param httpStatusCode http response code from the client + * @param message response message from the client + * @param responseBodyAsString response body from the client + * @param ncmpResponseStatus ncmp status message and code + */ + public DmiClientRequestException(final int httpStatusCode, final String message, final String responseBodyAsString, + final NcmpResponseStatus ncmpResponseStatus) { + super(message, responseBodyAsString); + this.httpStatusCode = httpStatusCode; + this.message = message; + this.responseBodyAsString = responseBodyAsString; + this.ncmpResponseStatus = ncmpResponseStatus; + } +} diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/exception/HttpClientRequestException.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/exception/InvalidDmiResourceUrlException.java index 9d307e5d2..270988b63 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/exception/HttpClientRequestException.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/exception/InvalidDmiResourceUrlException.java @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2022 Nordix Foundation + * 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. @@ -22,24 +22,16 @@ package org.onap.cps.ncmp.api.impl.exception; import lombok.Getter; -/** - * Http Client Request exception for passthrough scenarios. - */ @Getter -public class HttpClientRequestException extends NcmpException { +public class InvalidDmiResourceUrlException extends RuntimeException { + + private static final long serialVersionUID = 2928476384584894968L; - private static final long serialVersionUID = 6659897770659834797L; + private static final String INVALID_DMI_URL = "Invalid dmi resource url"; final Integer httpStatus; - /** - * Constructor to form exception for passthrough scenarios. - * - * @param message message details from NCMP - * @param details response body from the client available as details - * @param httpStatus http status code from the client - */ - public HttpClientRequestException(final String message, final String details, final Integer httpStatus) { - super(message, details); + public InvalidDmiResourceUrlException(final String details, final Integer httpStatus) { + super(String.format(INVALID_DMI_URL + ": %s", details)); this.httpStatus = httpStatus; } } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/inventory/InventoryPersistence.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/inventory/InventoryPersistence.java index 184b12570..e230b3fcb 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/inventory/InventoryPersistence.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/inventory/InventoryPersistence.java @@ -130,16 +130,6 @@ public interface InventoryPersistence extends NcmpPersistence { DataNode getCmHandleDataNodeByAlternateId(String alternateId); /** - * Get data node that matches longest alternate id by removing elements (as defined by the separator string) - * from right to left. - * - * @param alternateId alternate ID - * @param separator a string that separates each element from the next. - * @return data node - */ - DataNode getCmHandleDataNodeByLongestMatchAlternateId(final String alternateId, final String separator); - - /** * Get collection of data nodes of given cm handles. * * @param cmHandleIds collection of cmHandle IDs diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/inventory/InventoryPersistenceImpl.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/inventory/InventoryPersistenceImpl.java index bf54fe5d9..c4cab31ab 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/inventory/InventoryPersistenceImpl.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/inventory/InventoryPersistenceImpl.java @@ -33,11 +33,9 @@ import java.util.HashMap; import java.util.List; import java.util.Map; 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.api.impl.exception.NoAlternateIdParentFoundException; import org.onap.cps.ncmp.api.impl.utils.YangDataConverter; import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle; import org.onap.cps.spi.FetchDescendantsOption; @@ -182,19 +180,6 @@ public class InventoryPersistenceImpl extends NcmpPersistenceImpl implements Inv } @Override - public DataNode getCmHandleDataNodeByLongestMatchAlternateId(final String alternateId, final String separator) { - String bestMatch = alternateId; - while (StringUtils.isNotEmpty(bestMatch)) { - try { - return getCmHandleDataNodeByAlternateId(bestMatch); - } catch (final DataNodeNotFoundException ignored) { - bestMatch = getParentPath(bestMatch, separator); - } - } - throw new NoAlternateIdParentFoundException(alternateId); - } - - @Override public Collection<DataNode> getCmHandleDataNodes(final Collection<String> cmHandleIds) { final Collection<String> xpaths = new ArrayList<>(cmHandleIds.size()); cmHandleIds.forEach(cmHandleId -> xpaths.add(getXPathForCmHandleById(cmHandleId))); @@ -221,9 +206,4 @@ public class InventoryPersistenceImpl extends NcmpPersistenceImpl implements Inv private String createCmHandlesJsonData(final List<YangModelCmHandle> yangModelCmHandles) { return "{\"cm-handles\":" + jsonObjectMapper.asJsonString(yangModelCmHandles) + "}"; } - - private static 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/main/java/org/onap/cps/ncmp/api/impl/inventory/sync/ModuleSyncService.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/inventory/sync/ModuleSyncService.java index e257112fc..45156ce88 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/inventory/sync/ModuleSyncService.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/inventory/sync/ModuleSyncService.java @@ -1,6 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2022-2024 Nordix Foundation + * Modifications Copyright (C) 2024 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,6 +48,7 @@ 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; import org.springframework.stereotype.Service; @@ -146,6 +148,6 @@ public class ModuleSyncService { final String jsonForUpdate = jsonObjectMapper.asJsonString(Map.of( "cm-handles", Map.of("id", yangModelCmHandle.getId(), "module-set-tag", newModuleSetTag))); cpsDataService.updateNodeLeaves(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, NCMP_DMI_REGISTRY_PARENT, - jsonForUpdate, OffsetDateTime.now()); + jsonForUpdate, OffsetDateTime.now(), ContentType.JSON); } } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiDataOperation.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiDataOperation.java index 2e66ac0bf..7baac34b1 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiDataOperation.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiDataOperation.java @@ -40,7 +40,7 @@ public class DmiDataOperation { private String options; private String resourceIdentifier; - private final List<CmHandle> cmHandles = new ArrayList<>(); + private final List<DmiOperationCmHandle> cmHandles = new ArrayList<>(); /** * Create and initialise a (outgoing) DMI data operation. diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiDataOperations.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiDataOperations.java index a9ec1241b..3db84556e 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiDataOperations.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiDataOperations.java @@ -21,10 +21,9 @@ package org.onap.cps.ncmp.api.impl.operations; -import static org.onap.cps.ncmp.api.NcmpResponseStatus.DMI_SERVICE_NOT_RESPONDING; -import static org.onap.cps.ncmp.api.NcmpResponseStatus.UNABLE_TO_READ_RESOURCE_DATA; import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.PASSTHROUGH_RUNNING; import static org.onap.cps.ncmp.api.impl.operations.OperationType.READ; +import static org.onap.cps.ncmp.api.impl.operations.RequiredDmiService.DATA; import io.micrometer.core.annotation.Timed; import java.util.Collection; @@ -32,11 +31,11 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; -import lombok.extern.slf4j.Slf4j; +import lombok.RequiredArgsConstructor; import org.onap.cps.ncmp.api.NcmpResponseStatus; import org.onap.cps.ncmp.api.impl.client.DmiRestClient; -import org.onap.cps.ncmp.api.impl.config.NcmpConfiguration; -import org.onap.cps.ncmp.api.impl.exception.HttpClientRequestException; +import org.onap.cps.ncmp.api.impl.config.DmiProperties; +import org.onap.cps.ncmp.api.impl.exception.DmiClientRequestException; import org.onap.cps.ncmp.api.impl.inventory.CmHandleState; import org.onap.cps.ncmp.api.impl.inventory.InventoryPersistence; import org.onap.cps.ncmp.api.impl.utils.DmiServiceUrlBuilder; @@ -47,41 +46,40 @@ import org.onap.cps.ncmp.api.models.DataOperationRequest; import org.onap.cps.spi.exceptions.CpsException; import org.onap.cps.utils.JsonObjectMapper; import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.util.UriComponentsBuilder; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; /** * Operations class for DMI data. */ -@Component -@Slf4j -public class DmiDataOperations extends DmiOperations { +@RequiredArgsConstructor +@Service +public class DmiDataOperations { - public DmiDataOperations(final InventoryPersistence inventoryPersistence, - final JsonObjectMapper jsonObjectMapper, - final NcmpConfiguration.DmiProperties dmiProperties, - final DmiRestClient dmiRestClient, - final DmiServiceUrlBuilder dmiServiceUrlBuilder) { - super(inventoryPersistence, jsonObjectMapper, dmiProperties, dmiRestClient, dmiServiceUrlBuilder); - } + private final InventoryPersistence inventoryPersistence; + private final JsonObjectMapper jsonObjectMapper; + private final DmiProperties dmiProperties; + private final DmiRestClient dmiRestClient; /** - * This method fetches the resource data from operational data store for given cm handle - * identifier on given resource using dmi client. + * This method fetches the resource data from the operational data store for a given CM handle + * identifier on the specified resource using the DMI client. * - * @param cmResourceAddress target datastore, cm handle and resource identifier - * @param optionsParamInQuery options query - * @param topicParamInQuery topic name for (triggering) async responses - * @param requestId requestId for async responses - * @param authorization contents of Authorization header, or null if not present - * @return {@code ResponseEntity} response entity + * @param cmResourceAddress Target datastore, CM handle, and resource identifier. + * @param optionsParamInQuery Options query string. + * @param topicParamInQuery Topic name for triggering asynchronous responses. + * @param requestId Request ID for asynchronous responses. + * @param authorization Contents of the Authorization header, or null if not present. + * @return {@code Mono<ResponseEntity<Object>>} A reactive type representing the response entity. */ @Timed(value = "cps.ncmp.dmi.get", description = "Time taken to fetch the resource data from operational data store for given cm handle " + "identifier on given resource using dmi client") - public ResponseEntity<Object> getResourceDataFromDmi(final CmResourceAddress cmResourceAddress, + public Mono<ResponseEntity<Object>> getResourceDataFromDmi(final CmResourceAddress cmResourceAddress, final String optionsParamInQuery, final String topicParamInQuery, final String requestId, @@ -90,33 +88,30 @@ public class DmiDataOperations extends DmiOperations { final CmHandleState cmHandleState = yangModelCmHandle.getCompositeState().getCmHandleState(); validateIfCmHandleStateReady(yangModelCmHandle, cmHandleState); final String jsonRequestBody = getDmiRequestBody(READ, requestId, null, null, yangModelCmHandle); - final String dmiResourceDataUrl = getDmiRequestUrl(cmResourceAddress.datastoreName(), - cmResourceAddress.cmHandleId(), cmResourceAddress.resourceIdentifier(), optionsParamInQuery, - topicParamInQuery, yangModelCmHandle.resolveDmiServiceName(RequiredDmiService.DATA)); - return dmiRestClient.postOperationWithJsonData(dmiResourceDataUrl, jsonRequestBody, READ, authorization); + final String dmiUrl = getDmiResourceDataUrl(cmResourceAddress.datastoreName(), yangModelCmHandle, + cmResourceAddress.resourceIdentifier(), optionsParamInQuery, topicParamInQuery); + return dmiRestClient.postOperationWithJsonDataAsync(DATA, dmiUrl, jsonRequestBody, READ, authorization); } /** * This method fetches all the resource data from operational data store for given cm handle * identifier using dmi client. * - * @param dataStoreName data store name + * @param datastoreName data store name * @param cmHandleId network resource identifier * @param requestId requestId for async responses * @return {@code ResponseEntity} response entity */ - public ResponseEntity<Object> getResourceDataFromDmi(final String dataStoreName, + public ResponseEntity<Object> getResourceDataFromDmi(final String datastoreName, final String cmHandleId, final String requestId) { final YangModelCmHandle yangModelCmHandle = getYangModelCmHandle(cmHandleId); - final String jsonRequestBody = getDmiRequestBody(READ, requestId, null, null, - yangModelCmHandle); - final String dmiResourceDataUrl = getDmiRequestUrl(dataStoreName, cmHandleId, "/", - null, null, - yangModelCmHandle.resolveDmiServiceName(RequiredDmiService.DATA)); final CmHandleState cmHandleState = yangModelCmHandle.getCompositeState().getCmHandleState(); validateIfCmHandleStateReady(yangModelCmHandle, cmHandleState); - return dmiRestClient.postOperationWithJsonData(dmiResourceDataUrl, jsonRequestBody, READ, null); + + final String jsonRequestBody = getDmiRequestBody(READ, requestId, null, null, yangModelCmHandle); + final String dmiUrl = getDmiResourceDataUrl(datastoreName, yangModelCmHandle, "/", null, null); + return dmiRestClient.postOperationWithJsonData(DATA, dmiUrl, jsonRequestBody, READ, null); } /** @@ -137,13 +132,13 @@ public class DmiDataOperations extends DmiOperations { = getDistinctCmHandleIdsFromDataOperationRequest(dataOperationRequest); final Collection<YangModelCmHandle> yangModelCmHandles - = inventoryPersistence.getYangModelCmHandles(cmHandlesIds); + = inventoryPersistence.getYangModelCmHandles(cmHandlesIds); final Map<String, List<DmiDataOperation>> operationsOutPerDmiServiceName = ResourceDataOperationRequestUtils.processPerDefinitionInDataOperationsRequest(topicParamInQuery, requestId, dataOperationRequest, yangModelCmHandles); - buildDataOperationRequestUrlAndSendToDmiService(topicParamInQuery, requestId, operationsOutPerDmiServiceName, + buildDataOperationRequestUrlAndSendToDmiService(requestId, topicParamInQuery, operationsOutPerDmiServiceName, authorization); } @@ -166,14 +161,14 @@ public class DmiDataOperations extends DmiOperations { final String dataType, final String authorization) { final YangModelCmHandle yangModelCmHandle = getYangModelCmHandle(cmHandleId); - final String jsonRequestBody = getDmiRequestBody(operationType, null, requestData, dataType, - yangModelCmHandle); - final String dmiUrl = getDmiRequestUrl(PASSTHROUGH_RUNNING.getDatastoreName(), cmHandleId, resourceId, - null, null, - yangModelCmHandle.resolveDmiServiceName(RequiredDmiService.DATA)); final CmHandleState cmHandleState = yangModelCmHandle.getCompositeState().getCmHandleState(); validateIfCmHandleStateReady(yangModelCmHandle, cmHandleState); - return dmiRestClient.postOperationWithJsonData(dmiUrl, jsonRequestBody, operationType, authorization); + + final String jsonRequestBody = getDmiRequestBody(operationType, null, requestData, dataType, + yangModelCmHandle); + final String dmiUrl = getDmiResourceDataUrl(PASSTHROUGH_RUNNING.getDatastoreName(), + yangModelCmHandle, resourceId, null, null); + return dmiRestClient.postOperationWithJsonData(DATA, dmiUrl, jsonRequestBody, operationType, authorization); } private YangModelCmHandle getYangModelCmHandle(final String cmHandleId) { @@ -190,30 +185,28 @@ public class DmiDataOperations extends DmiOperations { .requestId(requestId) .data(requestData) .dataType(dataType) + .moduleSetTag(yangModelCmHandle.getModuleSetTag()) .build(); dmiRequestBody.asDmiProperties(yangModelCmHandle.getDmiProperties()); return jsonObjectMapper.asJsonString(dmiRequestBody); } - private String getDmiRequestUrl(final String dataStoreName, - final String cmHandleId, - final String resourceId, - final String optionsParamInQuery, - final String topicParamInQuery, - final String dmiServiceName) { - return dmiServiceUrlBuilder.getDmiDatastoreUrl( - dmiServiceUrlBuilder.populateQueryParams(resourceId, optionsParamInQuery, - topicParamInQuery), dmiServiceUrlBuilder.populateUriVariables(dataStoreName, dmiServiceName, - cmHandleId)); - } - - private String getDmiServiceDataOperationRequestUrl(final String dmiServiceName, - final String topicParamInQuery, - final String requestId) { - final MultiValueMap<String, String> dataOperationRequestQueryParams = dmiServiceUrlBuilder - .getDataOperationRequestQueryParams(topicParamInQuery, requestId); - return dmiServiceUrlBuilder.getDataOperationRequestUrl(dataOperationRequestQueryParams, - dmiServiceUrlBuilder.populateDataOperationRequestUriVariables(dmiServiceName)); + private String getDmiResourceDataUrl(final String datastoreName, + final YangModelCmHandle yangModelCmHandle, + final String resourceIdentifier, + final String optionsParamInQuery, + final String topicParamInQuery) { + final String dmiServiceName = yangModelCmHandle.resolveDmiServiceName(RequiredDmiService.DATA); + return DmiServiceUrlBuilder.newInstance() + .pathSegment("ch") + .variablePathSegment("cmHandleId", yangModelCmHandle.getId()) + .pathSegment("data") + .pathSegment("ds") + .variablePathSegment("datastore", datastoreName) + .queryParameter("resourceIdentifier", resourceIdentifier) + .queryParameter("options", optionsParamInQuery) + .queryParameter("topic", topicParamInQuery) + .build(dmiServiceName, dmiProperties.getDmiBasePath()); } private void validateIfCmHandleStateReady(final YangModelCmHandle yangModelCmHandle, @@ -226,66 +219,78 @@ public class DmiDataOperations extends DmiOperations { } private static Set<String> getDistinctCmHandleIdsFromDataOperationRequest(final DataOperationRequest - dataOperationRequest) { + dataOperationRequest) { return dataOperationRequest.getDataOperationDefinitions().stream() .flatMap(dataOperationDefinition -> dataOperationDefinition.getCmHandleIds().stream()).collect(Collectors.toSet()); } - private void buildDataOperationRequestUrlAndSendToDmiService(final String topicParamInQuery, - final String requestId, + private void buildDataOperationRequestUrlAndSendToDmiService(final String requestId, + final String topicParamInQuery, final Map<String, List<DmiDataOperation>> - groupsOutPerDmiServiceName, + groupsOutPerDmiServiceName, final String authorization) { - groupsOutPerDmiServiceName.forEach((dmiServiceName, dmiDataOperationRequestBodies) -> { - final String dmiDataOperationResourceUrl = - getDmiServiceDataOperationRequestUrl(dmiServiceName, topicParamInQuery, requestId); - sendDataOperationRequestToDmiService(dmiDataOperationResourceUrl, dmiDataOperationRequestBodies, - authorization); - }); + Flux.fromIterable(groupsOutPerDmiServiceName.entrySet()) + .flatMap(dmiDataOperationsByDmiServiceName -> { + final String dmiServiceName = dmiDataOperationsByDmiServiceName.getKey(); + final String dmiUrl = buildDmiServiceUrl(dmiServiceName, requestId, topicParamInQuery); + final List<DmiDataOperation> dmiDataOperationRequestBodies + = dmiDataOperationsByDmiServiceName.getValue(); + return sendDataOperationRequestToDmiService(dmiUrl, dmiDataOperationRequestBodies, authorization); + }) + .subscribe(); } - private void sendDataOperationRequestToDmiService(final String dataOperationResourceUrl, - final List<DmiDataOperation> dmiDataOperationRequestBodies, - final String authorization) { + private String buildDmiServiceUrl(final String dmiServiceName, final String requestId, + final String topicParamInQuery) { + return DmiServiceUrlBuilder.newInstance() + .pathSegment("data") + .queryParameter("requestId", requestId) + .queryParameter("topic", topicParamInQuery) + .build(dmiServiceName, dmiProperties.getDmiBasePath()); + } + + private Mono<Void> sendDataOperationRequestToDmiService(final String dmiUrl, + final List<DmiDataOperation> dmiDataOperationRequestBodies, + final String authorization) { + final String dmiDataOperationRequestAsJsonString + = createDmiDataOperationRequestAsJsonString(dmiDataOperationRequestBodies); + return dmiRestClient.postOperationWithJsonDataAsync(DATA, dmiUrl, dmiDataOperationRequestAsJsonString, + READ, authorization) + .then() + .onErrorResume(DmiClientRequestException.class, dmiClientRequestException -> { + handleTaskCompletionException(dmiClientRequestException, dmiUrl, dmiDataOperationRequestBodies); + return Mono.empty(); + }); + } + + private String createDmiDataOperationRequestAsJsonString( + final List<DmiDataOperation> dmiDataOperationRequestBodies) { final DmiDataOperationRequest dmiDataOperationRequest = DmiDataOperationRequest.builder() - .operations(dmiDataOperationRequestBodies).build(); - final String dmiDataOperationRequestAsJsonString = - jsonObjectMapper.asJsonString(dmiDataOperationRequest); - try { - dmiRestClient.postOperationWithJsonData(dataOperationResourceUrl, dmiDataOperationRequestAsJsonString, READ, - authorization); - } catch (final Exception exception) { - handleTaskCompletionException(exception, dataOperationResourceUrl, dmiDataOperationRequestBodies); - } + .operations(dmiDataOperationRequestBodies) + .build(); + return jsonObjectMapper.asJsonString(dmiDataOperationRequest); } - private void handleTaskCompletionException(final Throwable throwable, + private void handleTaskCompletionException(final DmiClientRequestException dmiClientRequestException, final String dataOperationResourceUrl, final List<DmiDataOperation> dmiDataOperationRequestBodies) { - if (throwable != null) { - final MultiValueMap<String, String> dataOperationResourceUrlParameters = - UriComponentsBuilder.fromUriString(dataOperationResourceUrl).build().getQueryParams(); - final String topicName = dataOperationResourceUrlParameters.get("topic").get(0); - final String requestId = dataOperationResourceUrlParameters.get("requestId").get(0); + final MultiValueMap<String, String> dataOperationResourceUrlParameters = + UriComponentsBuilder.fromUriString(dataOperationResourceUrl).build().getQueryParams(); + final String topicName = dataOperationResourceUrlParameters.get("topic").get(0); + final String requestId = dataOperationResourceUrlParameters.get("requestId").get(0); - final MultiValueMap<DmiDataOperation, Map<NcmpResponseStatus, List<String>>> - cmHandleIdsPerResponseCodesPerOperation = new LinkedMultiValueMap<>(); + final MultiValueMap<DmiDataOperation, Map<NcmpResponseStatus, List<String>>> + cmHandleIdsPerResponseCodesPerOperation = new LinkedMultiValueMap<>(); - dmiDataOperationRequestBodies.forEach(dmiDataOperationRequestBody -> { - final List<String> cmHandleIds = dmiDataOperationRequestBody.getCmHandles().stream() - .map(CmHandle::getId).toList(); - if (throwable.getCause() instanceof HttpClientRequestException) { - cmHandleIdsPerResponseCodesPerOperation.add(dmiDataOperationRequestBody, - Map.of(UNABLE_TO_READ_RESOURCE_DATA, cmHandleIds)); - } else { - cmHandleIdsPerResponseCodesPerOperation.add(dmiDataOperationRequestBody, - Map.of(DMI_SERVICE_NOT_RESPONDING, cmHandleIds)); - } - }); - ResourceDataOperationRequestUtils.publishErrorMessageToClientTopic(topicName, requestId, - cmHandleIdsPerResponseCodesPerOperation); - } + dmiDataOperationRequestBodies.forEach(dmiDataOperationRequestBody -> { + final List<String> cmHandleIds = dmiDataOperationRequestBody.getCmHandles().stream() + .map(DmiOperationCmHandle::getId).toList(); + cmHandleIdsPerResponseCodesPerOperation.add(dmiDataOperationRequestBody, + Map.of(dmiClientRequestException.getNcmpResponseStatus(), cmHandleIds)); + }); + ResourceDataOperationRequestUtils.publishErrorMessageToClientTopic(topicName, requestId, + cmHandleIdsPerResponseCodesPerOperation); } -} +}
\ No newline at end of file diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiModelOperations.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiModelOperations.java index 798f6de81..77dfcb7a2 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiModelOperations.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiModelOperations.java @@ -33,34 +33,27 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import lombok.RequiredArgsConstructor; import org.onap.cps.ncmp.api.impl.client.DmiRestClient; -import org.onap.cps.ncmp.api.impl.config.NcmpConfiguration; -import org.onap.cps.ncmp.api.impl.inventory.InventoryPersistence; +import org.onap.cps.ncmp.api.impl.config.DmiProperties; import org.onap.cps.ncmp.api.impl.utils.DmiServiceUrlBuilder; import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle; import org.onap.cps.ncmp.api.models.YangResource; import org.onap.cps.spi.model.ModuleReference; import org.onap.cps.utils.JsonObjectMapper; import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; /** * Operations class for DMI Model. */ -@Component -public class DmiModelOperations extends DmiOperations { +@RequiredArgsConstructor +@Service +public class DmiModelOperations { - /** - * Constructor for {@code DmiOperations}. This method also manipulates url properties. - * - * @param dmiRestClient {@code DmiRestClient} - */ - public DmiModelOperations(final InventoryPersistence inventoryPersistence, - final JsonObjectMapper jsonObjectMapper, - final NcmpConfiguration.DmiProperties dmiProperties, - final DmiRestClient dmiRestClient, final DmiServiceUrlBuilder dmiServiceUrlBuilder) { - super(inventoryPersistence, jsonObjectMapper, dmiProperties, dmiRestClient, dmiServiceUrlBuilder); - } + private final JsonObjectMapper jsonObjectMapper; + private final DmiProperties dmiProperties; + private final DmiRestClient dmiRestClient; /** * Retrieves module references. @@ -113,9 +106,12 @@ public class DmiModelOperations extends DmiOperations { final String jsonRequestBody, final String cmHandle, final String resourceName) { - final String dmiResourceDataUrl = getDmiResourceUrl(dmiServiceName, cmHandle, resourceName); - return dmiRestClient.postOperationWithJsonData(dmiResourceDataUrl, jsonRequestBody, - OperationType.READ, null); + final String dmiUrl = DmiServiceUrlBuilder.newInstance() + .pathSegment("ch") + .variablePathSegment("cmHandleId", cmHandle) + .variablePathSegment("resourceName", resourceName) + .build(dmiServiceName, dmiProperties.getDmiBasePath()); + return dmiRestClient.postOperationWithJsonData(MODEL, dmiUrl, jsonRequestBody, OperationType.READ, null); } private static String getRequestBodyToFetchYangResources(final Collection<ModuleReference> newModuleReferences, diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/CmHandle.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiOperationCmHandle.java index 618da7454..1bf2b77dc 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/CmHandle.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiOperationCmHandle.java @@ -29,14 +29,21 @@ import lombok.Getter; @JsonInclude(JsonInclude.Include.NON_NULL) @Getter @Builder -public class CmHandle { +public class DmiOperationCmHandle { private String id; @JsonProperty("cmHandleProperties") private Map<String, String> dmiProperties; + private String moduleSetTag; - public static CmHandle buildCmHandleWithProperties(final String cmHandleId, - final Map<String, String> dmiProperties) { - return CmHandle.builder().id(cmHandleId).dmiProperties(dmiProperties).build(); + /** + * Builds Dmi Operation Cm Handle object with all its associated properties. + */ + public static DmiOperationCmHandle buildDmiOperationCmHandle(final String cmHandleId, + final Map<String, String> dmiProperties, + final String moduleSetTag) { + return DmiOperationCmHandle.builder().id(cmHandleId) + .dmiProperties(dmiProperties).moduleSetTag(moduleSetTag) + .build(); } } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiOperations.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiOperations.java deleted file mode 100644 index c8d73eac6..000000000 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiOperations.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * Copyright (C) 2021-2023 Nordix Foundation - * 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. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - * ============LICENSE_END========================================================= - */ - -package org.onap.cps.ncmp.api.impl.operations; - -import lombok.RequiredArgsConstructor; -import org.onap.cps.ncmp.api.impl.client.DmiRestClient; -import org.onap.cps.ncmp.api.impl.config.NcmpConfiguration; -import org.onap.cps.ncmp.api.impl.inventory.InventoryPersistence; -import org.onap.cps.ncmp.api.impl.utils.DmiServiceUrlBuilder; -import org.onap.cps.utils.JsonObjectMapper; -import org.springframework.stereotype.Service; - -@RequiredArgsConstructor -@Service -public class DmiOperations { - - protected final InventoryPersistence inventoryPersistence; - protected final JsonObjectMapper jsonObjectMapper; - protected final NcmpConfiguration.DmiProperties dmiProperties; - protected final DmiRestClient dmiRestClient; - protected final DmiServiceUrlBuilder dmiServiceUrlBuilder; - - String getDmiResourceUrl(final String dmiServiceName, final String cmHandle, final String resourceName) { - return dmiServiceUrlBuilder.getResourceDataBasePathUriBuilder() - .pathSegment("{resourceName}") - .buildAndExpand(dmiServiceName, dmiProperties.getDmiBasePath(), cmHandle, resourceName).toUriString(); - } - - -} diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/OperationType.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/OperationType.java index fa00d1a15..e863228ed 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/OperationType.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/OperationType.java @@ -21,9 +21,7 @@ package org.onap.cps.ncmp.api.impl.operations; import com.fasterxml.jackson.annotation.JsonValue; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; +import java.util.Locale; import lombok.Getter; import org.onap.cps.ncmp.api.impl.exception.InvalidOperationException; @@ -48,13 +46,6 @@ public enum OperationType { return String.valueOf(operationName); } - private static final Map<String, OperationType> operationNameToOperationEnum = new HashMap<>(); - - static { - Arrays.stream(OperationType.values()).forEach( - operationType -> operationNameToOperationEnum.put(operationType.getOperationName(), operationType)); - } - /** * From operation name get operation enum type. * @@ -62,10 +53,10 @@ public enum OperationType { * @return the operation enum type */ public static OperationType fromOperationName(final String operationName) { - final OperationType operationType = operationNameToOperationEnum.get(operationName); - if (null == operationType) { + try { + return OperationType.valueOf(operationName.toUpperCase(Locale.ENGLISH)); + } catch (final IllegalArgumentException e) { throw new InvalidOperationException(operationName + " is an invalid operation name"); } - return operationType; } } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/DmiServiceUrlBuilder.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/DmiServiceUrlBuilder.java index 04acaa5e9..aeeeb6430 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/DmiServiceUrlBuilder.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/DmiServiceUrlBuilder.java @@ -21,166 +21,91 @@ package org.onap.cps.ncmp.api.impl.utils; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; -import lombok.RequiredArgsConstructor; +import lombok.NoArgsConstructor; import org.apache.logging.log4j.util.Strings; -import org.apache.logging.log4j.util.TriConsumer; -import org.onap.cps.ncmp.api.impl.config.NcmpConfiguration; -import org.onap.cps.spi.utils.CpsValidator; -import org.springframework.stereotype.Component; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; import org.springframework.web.util.UriComponentsBuilder; -@Component -@RequiredArgsConstructor +@NoArgsConstructor public class DmiServiceUrlBuilder { - private final NcmpConfiguration.DmiProperties dmiProperties; - private final CpsValidator cpsValidator; + private static final String FIXED_PATH_SEGMENT = null; - /** - * This method creates the dmi service url. - * - * @param queryParams query param map as key,value pair - * @param uriVariables uri param map as key (placeholder),value pair - * @return {@code String} dmi service url as string - */ - public String getDmiDatastoreUrl(final MultiValueMap<String, String> queryParams, - final Map<String, Object> uriVariables) { - return getUriComponentsBuilder(getResourceDataBasePathUriBuilder(), queryParams, uriVariables) - .buildAndExpand().toUriString(); - } + final UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.newInstance(); + final Map<String, Object> pathSegments = new LinkedHashMap<>(); - /** - * This method builds data operation request url. - * - * @param dataoperationRequestQueryParams query param map as key, value pair - * @param dataoperationRequestUriVariables uri param map as key (placeholder), value pair - * @return {@code String} data operation request url as string - */ - public String getDataOperationRequestUrl(final MultiValueMap<String, String> dataoperationRequestQueryParams, - final Map<String, Object> dataoperationRequestUriVariables) { - return getDataOperationResourceDataBasePathUriBuilder() - .queryParams(dataoperationRequestQueryParams) - .uriVariables(dataoperationRequestUriVariables) - .buildAndExpand().toUriString(); + public static DmiServiceUrlBuilder newInstance() { + return new DmiServiceUrlBuilder(); } /** - * This method creates the dmi service url builder object with path variables. + * Add a fixed pathSegment to the URI. * - * @return {@code UriComponentsBuilder} dmi service url builder object + * @param pathSegment the path segment + * @return this builder */ - public UriComponentsBuilder getResourceDataBasePathUriBuilder() { - return UriComponentsBuilder.newInstance() - .path("{dmiServiceName}") - .pathSegment("{dmiBasePath}") - .pathSegment("v1") - .pathSegment("ch") - .pathSegment("{cmHandleId}"); + public DmiServiceUrlBuilder pathSegment(final String pathSegment) { + pathSegments.put(pathSegment, FIXED_PATH_SEGMENT); + return this; } /** - * This method creates the dmi service url builder object with path variables for data operation request. + * Add a variable pathSegment to the URI. + * Do NOT add { } braces. the builder will take care of that * - * @return {@code UriComponentsBuilder} dmi service url builder object + * @param pathSegment the name of the variable path segment (with { and } + * @param value the value to be insert in teh URI for the given variable path segment + * @return this builder */ - public UriComponentsBuilder getDataOperationResourceDataBasePathUriBuilder() { - return UriComponentsBuilder.newInstance() - .path("{dmiServiceName}") - .pathSegment("{dmiBasePath}") - .pathSegment("v1") - .pathSegment("data"); + public DmiServiceUrlBuilder variablePathSegment(final String pathSegment, final Object value) { + pathSegments.put(pathSegment, value); + return this; } /** - * This method populates uri variables. + * Add a query parameter to the URI. + * Do NOT encode as the builder wil take care of encoding * - * @param dataStoreName data store name - * @param dmiServiceName dmi service name - * @param cmHandleId cm handle id for dmi registration - * @return {@code String} dmi service url as string + * @param name the name of the variable + * @param value the value of the variable (only Strings are supported). + * + * @return this builder */ - public Map<String, Object> populateUriVariables(final String dataStoreName, - final String dmiServiceName, - final String cmHandleId) { - cpsValidator.validateNameCharacters(cmHandleId); - final Map<String, Object> uriVariables = new HashMap<>(); - final String dmiBasePath = dmiProperties.getDmiBasePath(); - uriVariables.put("dmiServiceName", dmiServiceName); - uriVariables.put("dmiBasePath", dmiBasePath); - uriVariables.put("cmHandleId", cmHandleId); - uriVariables.put("dataStore", dataStoreName); - return uriVariables; + public DmiServiceUrlBuilder queryParameter(final String name, final String value) { + if (Strings.isNotBlank(value)) { + uriComponentsBuilder.queryParam(name, value); + } + return this; } /** - * This method populates uri variables for data operation request. + * Build the URI as a correctly percentage-encoded String. + * + * @param dmiServiceName the name of the dmi service + * @param dmiBasePath the base path of the dmi service * - * @param dmiServiceName dmi service name - * @return {@code Map<String, Object>} uri variables as map + * @return URI as a string */ - public Map<String, Object> populateDataOperationRequestUriVariables(final String dmiServiceName) { + public String build(final String dmiServiceName, final String dmiBasePath) { + uriComponentsBuilder + .path("{dmiServiceName}") + .pathSegment("{dmiBasePath}") + .pathSegment("v1"); + final Map<String, Object> uriVariables = new HashMap<>(); - final String dmiBasePath = dmiProperties.getDmiBasePath(); uriVariables.put("dmiServiceName", dmiServiceName); uriVariables.put("dmiBasePath", dmiBasePath); - return uriVariables; - } - - /** - * This method is used to populate map from query params. - * - * @param resourceId unique id of response for valid topic - * @param optionsParamInQuery options into url param - * @param topicParamInQuery topic into url param - * @return all valid query params as map - */ - public MultiValueMap<String, String> populateQueryParams(final String resourceId, - final String optionsParamInQuery, - final String topicParamInQuery) { - final MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>(); - getQueryParamConsumer().accept("resourceIdentifier", - resourceId, queryParams); - getQueryParamConsumer().accept("options", optionsParamInQuery, queryParams); - if (Strings.isNotEmpty(topicParamInQuery)) { - getQueryParamConsumer().accept("topic", topicParamInQuery, queryParams); - } - return queryParams; - } - /** - * This method is used to populate map from query params for data operation request. - * - * @param topicParamInQuery topic into url param - * @param requestId unique id of response for valid topic - * @return all valid query params as map - */ - public MultiValueMap<String, String> getDataOperationRequestQueryParams(final String topicParamInQuery, - final String requestId) { - final MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>(); - getQueryParamConsumer().accept("topic", topicParamInQuery, queryParams); - getQueryParamConsumer().accept("requestId", requestId, queryParams); - return queryParams; - } - - private TriConsumer<String, String, MultiValueMap<String, String>> getQueryParamConsumer() { - return (paramName, paramValue, paramMap) -> { - if (Strings.isNotEmpty(paramValue)) { - paramMap.add(paramName, paramValue); + pathSegments.forEach((pathSegment, variablePathValue) -> { + if (variablePathValue == FIXED_PATH_SEGMENT) { + uriComponentsBuilder.pathSegment(pathSegment); + } else { + uriComponentsBuilder.pathSegment("{" + pathSegment + "}"); + uriVariables.put(pathSegment, variablePathValue); } - }; + }); + return uriComponentsBuilder.buildAndExpand(uriVariables).encode().toUriString(); } - private UriComponentsBuilder getUriComponentsBuilder(final UriComponentsBuilder uriComponentsBuilder, - final MultiValueMap<String, String> queryParams, - final Map<String, Object> uriVariables) { - return uriComponentsBuilder - .pathSegment("data") - .pathSegment("ds") - .pathSegment("{dataStore}") - .queryParams(queryParams) - .uriVariables(uriVariables); - } } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/data/operation/ResourceDataOperationRequestUtils.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/data/operation/ResourceDataOperationRequestUtils.java index 4b016b37d..407fcf034 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/data/operation/ResourceDataOperationRequestUtils.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/data/operation/ResourceDataOperationRequestUtils.java @@ -31,7 +31,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; import lombok.AccessLevel; import lombok.NoArgsConstructor; @@ -39,8 +38,8 @@ import lombok.extern.slf4j.Slf4j; import org.onap.cps.events.EventsPublisher; import org.onap.cps.ncmp.api.NcmpResponseStatus; import org.onap.cps.ncmp.api.impl.inventory.CmHandleState; -import org.onap.cps.ncmp.api.impl.operations.CmHandle; import org.onap.cps.ncmp.api.impl.operations.DmiDataOperation; +import org.onap.cps.ncmp.api.impl.operations.DmiOperationCmHandle; import org.onap.cps.ncmp.api.impl.utils.DmiServiceNameOrganizer; import org.onap.cps.ncmp.api.impl.utils.context.CpsApplicationContext; import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle; @@ -49,8 +48,8 @@ import org.onap.cps.ncmp.api.models.DataOperationRequest; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; -@Slf4j @NoArgsConstructor(access = AccessLevel.PRIVATE) +@Slf4j public class ResourceDataOperationRequestUtils { private static final String UNKNOWN_SERVICE_NAME = null; @@ -81,6 +80,8 @@ public class ResourceDataOperationRequestUtils { final Map<String, String> dmiServiceNamesPerCmHandleId = getDmiServiceNamesPerCmHandleId(dmiPropertiesPerCmHandleIdPerServiceName); + final Map<String, String> moduleSetTagPerCmHandle = getModuleSetTagPerCmHandleId(yangModelCmHandles); + for (final DataOperationDefinition dataOperationDefinitionIn : dataOperationRequestIn.getDataOperationDefinitions()) { final List<String> nonExistingCmHandleIds = new ArrayList<>(); @@ -97,9 +98,10 @@ public class ResourceDataOperationRequestUtils { } else { final DmiDataOperation dmiBatchOperationOut = getOrAddDmiBatchOperation(dmiServiceName, dataOperationDefinitionIn, dmiDataOperationsOutPerDmiServiceName); - final CmHandle cmHandle = CmHandle.buildCmHandleWithProperties(cmHandleId, - cmHandleIdProperties); - dmiBatchOperationOut.getCmHandles().add(cmHandle); + final DmiOperationCmHandle dmiOperationCmHandle = DmiOperationCmHandle + .buildDmiOperationCmHandle(cmHandleId, cmHandleIdProperties, + moduleSetTagPerCmHandle.get(cmHandleId)); + dmiBatchOperationOut.getCmHandles().add(dmiOperationCmHandle); } } } @@ -114,56 +116,12 @@ public class ResourceDataOperationRequestUtils { return dmiDataOperationsOutPerDmiServiceName; } - /** - * Handles the async task completion for an entire data, publishing errors to client topic on task failure. - * - * @param topicParamInQuery client given topic - * @param requestId unique identifier per request - * @param dataOperationRequest incoming data operation request details - * @param throwable error cause, or null if task completed with no exception - */ - public static void handleAsyncTaskCompletionForDataOperationsRequest( - final String topicParamInQuery, - final String requestId, - final DataOperationRequest dataOperationRequest, - final Throwable throwable) { - if (throwable == null) { - log.info("Data operations request {} completed.", requestId); - } else if (throwable instanceof TimeoutException) { - log.error("Data operations request {} timed out.", requestId); - ResourceDataOperationRequestUtils.publishErrorMessageToClientTopicForEntireOperation(topicParamInQuery, - requestId, dataOperationRequest, NcmpResponseStatus.DMI_SERVICE_NOT_RESPONDING); - } else { - log.error("Data operations request {} failed.", requestId, throwable); - ResourceDataOperationRequestUtils.publishErrorMessageToClientTopicForEntireOperation(topicParamInQuery, - requestId, dataOperationRequest, NcmpResponseStatus.UNKNOWN_ERROR); - } - } - - /** - * Creates data operation cloud event for when the entire data operation fails and publishes it to client topic. - * - * @param topicParamInQuery client given topic - * @param requestId unique identifier per request - * @param dataOperationRequestIn incoming data operation request details - * @param ncmpResponseStatus response code to be sent for all cm handle ids in all operations - */ - private static void publishErrorMessageToClientTopicForEntireOperation( - final String topicParamInQuery, - final String requestId, - final DataOperationRequest dataOperationRequestIn, - final NcmpResponseStatus ncmpResponseStatus) { - - final MultiValueMap<DmiDataOperation, Map<NcmpResponseStatus, List<String>>> - cmHandleIdsPerResponseCodesPerOperation = new LinkedMultiValueMap<>(); - - for (final DataOperationDefinition dataOperationDefinitionIn : - dataOperationRequestIn.getDataOperationDefinitions()) { - cmHandleIdsPerResponseCodesPerOperation.add( - DmiDataOperation.buildDmiDataOperationRequestBodyWithoutCmHandles(dataOperationDefinitionIn), - Map.of(ncmpResponseStatus, dataOperationDefinitionIn.getCmHandleIds())); - } - publishErrorMessageToClientTopic(topicParamInQuery, requestId, cmHandleIdsPerResponseCodesPerOperation); + private static Map<String, String> getModuleSetTagPerCmHandleId( + final Collection<YangModelCmHandle> yangModelCmHandles) { + final Map<String, String> moduleSetTagPerCmHandle = new HashMap<>(yangModelCmHandles.size()); + yangModelCmHandles.forEach(yangModelCmHandle -> + moduleSetTagPerCmHandle.put(yangModelCmHandle.getId(), yangModelCmHandle.getModuleSetTag())); + return moduleSetTagPerCmHandle; } /** @@ -182,6 +140,8 @@ public class ResourceDataOperationRequestUtils { final CloudEvent dataOperationCloudEvent = DataOperationEventCreator.createDataOperationEvent(clientTopic, requestId, cmHandleIdsPerResponseCodesPerOperation); final EventsPublisher<CloudEvent> eventsPublisher = CpsApplicationContext.getCpsBean(EventsPublisher.class); + log.warn("publishing error message to client topic: {} ,requestId: {}, data operation cloud event id: {}", + clientTopic, requestId, dataOperationCloudEvent.getId()); eventsPublisher.publishCloudEvent(clientTopic, requestId, dataOperationCloudEvent); } } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/exception/NoAlternateIdParentFoundException.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/exceptions/NoAlternateIdMatchFoundException.java index 2e6cd3308..510a6f51a 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/exception/NoAlternateIdParentFoundException.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/exceptions/NoAlternateIdMatchFoundException.java @@ -18,22 +18,23 @@ * ============LICENSE_END========================================================= */ -package org.onap.cps.ncmp.api.impl.exception; +package org.onap.cps.ncmp.exceptions; import java.io.Serial; +import org.onap.cps.ncmp.api.impl.exception.NcmpException; -public class NoAlternateIdParentFoundException extends NcmpException { +public class NoAlternateIdMatchFoundException extends NcmpException { @Serial private static final long serialVersionUID = -2412915490233422945L; - private static final String ALTERNATE_ID_NOT_FOUND = "No matching (parent) cm handle found using alternate ids"; + private static final String ALTERNATE_ID_NOT_FOUND = "No matching cm handle found using alternate ids"; /** * Constructor. * * @param cpsPath datanode cpsPath */ - public NoAlternateIdParentFoundException(final String cpsPath) { + public NoAlternateIdMatchFoundException(final String cpsPath) { super(ALTERNATE_ID_NOT_FOUND, String.format("cannot find a datanode with alternate id %s", cpsPath)); } } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/DataJobServiceImpl.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/datajobs/DataJobServiceImpl.java index b4377b84f..7db6c5c27 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/DataJobServiceImpl.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/datajobs/DataJobServiceImpl.java @@ -18,13 +18,13 @@ * ============LICENSE_END========================================================= */ -package org.onap.cps.ncmp.api.impl; +package org.onap.cps.ncmp.impl.datajobs; import lombok.extern.slf4j.Slf4j; -import org.onap.cps.ncmp.api.DataJobService; -import org.onap.cps.ncmp.api.models.datajob.DataJobMetadata; -import org.onap.cps.ncmp.api.models.datajob.DataJobReadRequest; -import org.onap.cps.ncmp.api.models.datajob.DataJobWriteRequest; +import org.onap.cps.ncmp.api.datajobs.DataJobService; +import org.onap.cps.ncmp.api.datajobs.models.DataJobMetadata; +import org.onap.cps.ncmp.api.datajobs.models.DataJobReadRequest; +import org.onap.cps.ncmp.api.datajobs.models.DataJobWriteRequest; @Slf4j public class DataJobServiceImpl implements DataJobService { diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/init/AbstractModelLoader.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/init/AbstractModelLoader.java index 4cc8cdaa6..554501127 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/init/AbstractModelLoader.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/init/AbstractModelLoader.java @@ -39,7 +39,7 @@ import org.onap.cps.spi.exceptions.AlreadyDefinedException; import org.onap.cps.utils.JsonObjectMapper; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; -import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.boot.context.event.ApplicationStartedEvent; @Slf4j @RequiredArgsConstructor @@ -61,12 +61,12 @@ abstract class AbstractModelLoader implements ModelLoader { long retryTimeMs; @Override - public void onApplicationEvent(@NonNull final ApplicationReadyEvent applicationReadyEvent) { + public void onApplicationEvent(@NonNull final ApplicationStartedEvent applicationStartedEvent) { try { onboardOrUpgradeModel(); } catch (final NcmpStartUpException ncmpStartUpException) { log.error("Onboarding model for NCMP failed: {} ", ncmpStartUpException.getMessage()); - SpringApplication.exit(applicationReadyEvent.getApplicationContext(), () -> EXIT_CODE_ON_ERROR); + SpringApplication.exit(applicationStartedEvent.getApplicationContext(), () -> EXIT_CODE_ON_ERROR); } } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/init/ModelLoader.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/init/ModelLoader.java index c61bf1c9b..9832ba3f9 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/init/ModelLoader.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/init/ModelLoader.java @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2023 Nordix Foundation + * Copyright (C) 2023-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,13 +21,13 @@ package org.onap.cps.ncmp.init; import lombok.NonNull; -import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.boot.context.event.ApplicationStartedEvent; import org.springframework.context.ApplicationListener; -public interface ModelLoader extends ApplicationListener<ApplicationReadyEvent> { +public interface ModelLoader extends ApplicationListener<ApplicationStartedEvent> { @Override - void onApplicationEvent(@NonNull ApplicationReadyEvent applicationReadyEvent); + void onApplicationEvent(@NonNull ApplicationStartedEvent applicationStartedEvent); void onboardOrUpgradeModel(); diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/utils/AlternateIdMatcher.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/utils/AlternateIdMatcher.java new file mode 100644 index 000000000..8385f19f7 --- /dev/null +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/utils/AlternateIdMatcher.java @@ -0,0 +1,63 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps.ncmp.utils; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.onap.cps.ncmp.api.impl.inventory.InventoryPersistence; +import org.onap.cps.ncmp.exceptions.NoAlternateIdMatchFoundException; +import org.onap.cps.spi.exceptions.DataNodeNotFoundException; +import org.onap.cps.spi.model.DataNode; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class AlternateIdMatcher { + + private final InventoryPersistence inventoryPersistence; + + /** + * Get data node that matches longest alternate id by removing elements (as defined by the separator string) + * from right to left. + * + * @param alternateId alternate ID + * @param separator a string that separates each element from the next. + * @return data node + */ + public DataNode getCmHandleDataNodeByLongestMatchAlternateId(final String alternateId, final String separator) { + String bestMatch = alternateId; + while (StringUtils.isNotEmpty(bestMatch)) { + try { + return inventoryPersistence.getCmHandleDataNodeByAlternateId(bestMatch); + } catch (final DataNodeNotFoundException ignored) { + bestMatch = getParentPath(bestMatch, separator); + } + } + throw new NoAlternateIdMatchFoundException(alternateId); + } + + 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/api/impl/DataJobServiceImplSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/DataJobServiceImplSpec.groovy index 43787640a..bef0adc9c 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/DataJobServiceImplSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/DataJobServiceImplSpec.groovy @@ -24,12 +24,13 @@ import ch.qos.logback.classic.Level import ch.qos.logback.classic.Logger import ch.qos.logback.classic.spi.ILoggingEvent import ch.qos.logback.core.read.ListAppender +import org.onap.cps.ncmp.impl.datajobs.DataJobServiceImpl import org.slf4j.LoggerFactory -import org.onap.cps.ncmp.api.models.datajob.DataJobReadRequest -import org.onap.cps.ncmp.api.models.datajob.DataJobWriteRequest -import org.onap.cps.ncmp.api.models.datajob.DataJobMetadata -import org.onap.cps.ncmp.api.models.datajob.ReadOperation -import org.onap.cps.ncmp.api.models.datajob.WriteOperation +import org.onap.cps.ncmp.api.datajobs.models.DataJobReadRequest +import org.onap.cps.ncmp.api.datajobs.models.DataJobWriteRequest +import org.onap.cps.ncmp.api.datajobs.models.DataJobMetadata +import org.onap.cps.ncmp.api.datajobs.models.ReadOperation +import org.onap.cps.ncmp.api.datajobs.models.WriteOperation import spock.lang.Specification class DataJobServiceImplSpec extends Specification{ diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplSpec.groovy index 4d0af6f49..d91c79d33 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplSpec.groovy @@ -23,6 +23,8 @@ package org.onap.cps.ncmp.api.impl +import reactor.core.publisher.Mono + import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DATASPACE_NAME import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DMI_REGISTRY_ANCHOR @@ -120,16 +122,16 @@ class NetworkCmProxyDataServiceImplSpec extends Specification { >> { new ResponseEntity<>(HttpStatus.CREATED) } } - def 'Get resource data for from DMI.'() { + def 'Get resource data from DMI.'() { given: 'cpsDataService returns valid data node' mockDataNode() and: 'some cm resource address' - def cmResourceAddress = new CmResourceAddress('some datastore','some CM Handle', 'some resource Id') + def cmResourceAddress = new CmResourceAddress('some datastore', 'some CM Handle', 'some resource Id') and: 'get resource data from DMI is called' mockDmiDataOperations.getResourceDataFromDmi(cmResourceAddress, OPTIONS_PARAM, NO_TOPIC, NO_REQUEST_ID, NO_AUTH_HEADER) >> - new ResponseEntity<>('dmi-response', HttpStatus.OK) + Mono.just(new ResponseEntity<>('dmi-response', HttpStatus.OK)) when: 'get resource data operational for the given cm resource address is called' - def response = objectUnderTest.getResourceDataForCmHandle(cmResourceAddress, OPTIONS_PARAM, NO_TOPIC, NO_REQUEST_ID, NO_AUTH_HEADER) + def response = objectUnderTest.getResourceDataForCmHandle(cmResourceAddress, OPTIONS_PARAM, NO_TOPIC, NO_REQUEST_ID, NO_AUTH_HEADER).block() then: 'DMI returns a json response' assert response == 'dmi-response' } diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandlerSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandlerSpec.groovy index 260772714..b0024b19b 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandlerSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandlerSpec.groovy @@ -2,7 +2,7 @@ * ============LICENSE_START======================================================= * Copyright (C) 2022-2024 Nordix Foundation * Modifications Copyright (C) 2022 Bell Canada - * Modifications Copyright (C) 2023 TechMahindra Ltd. + * Modifications Copyright (C) 2024 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,6 +35,7 @@ import org.onap.cps.spi.exceptions.DataNodeNotFoundException import org.onap.cps.spi.exceptions.DataValidationException import org.onap.cps.spi.model.DataNode import org.onap.cps.spi.model.DataNodeBuilder +import org.onap.cps.utils.ContentType import org.onap.cps.utils.JsonObjectMapper import org.slf4j.LoggerFactory import spock.lang.Specification @@ -209,7 +210,7 @@ class NetworkCmProxyDataServicePropertyHandlerSpec extends Specification { when: 'cm handle properties is updated' def response = objectUnderTest.updateCmHandleProperties(cmHandleUpdateRequest) then: 'the update is delegated to cps data service with correct parameters' - 1 * mockCpsDataService.updateNodeLeaves('NCMP-Admin', 'ncmp-dmi-registry', '/dmi-registry', _, _) >> + 1 * mockCpsDataService.updateNodeLeaves('NCMP-Admin', 'ncmp-dmi-registry', '/dmi-registry', _, _, ContentType.JSON) >> { args -> assert args[3].contains('alt-1') } @@ -245,7 +246,7 @@ class NetworkCmProxyDataServicePropertyHandlerSpec extends Specification { when: 'data producer identifier updated' objectUnderTest.updateDataProducerIdentifier(existingCmHandleDataNode, ncmpServiceCmHandle) then: 'the update node leaves method is invoked once' - 1 * mockCpsDataService.updateNodeLeaves('NCMP-Admin', 'ncmp-dmi-registry', '/dmi-registry', _, _) >> { args -> + 1 * mockCpsDataService.updateNodeLeaves('NCMP-Admin', 'ncmp-dmi-registry', '/dmi-registry', _, _, ContentType.JSON) >> { args -> assert args[3].contains('someDataProducerIdentifier') } and: 'correct information is logged' diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/async/DataOperationEventConsumerSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/async/DataOperationEventConsumerSpec.groovy index 369b496ca..b095f904a 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/async/DataOperationEventConsumerSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/async/DataOperationEventConsumerSpec.groovy @@ -20,6 +20,8 @@ package org.onap.cps.ncmp.api.impl.async +import static org.onap.cps.ncmp.api.impl.events.mapper.CloudEventMapper.toTargetEvent + import com.fasterxml.jackson.databind.ObjectMapper import io.cloudevents.CloudEvent import io.cloudevents.kafka.CloudEventDeserializer @@ -42,8 +44,6 @@ import org.springframework.test.annotation.DirtiesContext import org.testcontainers.spock.Testcontainers import java.time.Duration -import static org.onap.cps.ncmp.api.impl.events.mapper.CloudEventMapper.toTargetEvent - @SpringBootTest(classes = [EventsPublisher, DataOperationEventConsumer, RecordFilterStrategies, JsonObjectMapper, ObjectMapper]) @Testcontainers @DirtiesContext diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/client/DmiRestClientSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/client/DmiRestClientSpec.groovy index c8e34b1a5..2c2212773 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/client/DmiRestClientSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/client/DmiRestClientSpec.groovy @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2021-2023 Nordix Foundation + * Copyright (C) 2021-2024 Nordix Foundation * Modifications Copyright (C) 2022 Bell Canada * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,92 +21,133 @@ package org.onap.cps.ncmp.api.impl.client +import static org.onap.cps.ncmp.api.NcmpResponseStatus.UNKNOWN_ERROR +import static org.onap.cps.ncmp.api.impl.operations.OperationType.CREATE +import static org.onap.cps.ncmp.api.impl.operations.OperationType.PATCH +import static org.onap.cps.ncmp.api.impl.operations.OperationType.READ +import static org.onap.cps.ncmp.api.impl.operations.RequiredDmiService.DATA +import static org.springframework.http.HttpStatus.SERVICE_UNAVAILABLE +import static org.onap.cps.ncmp.api.NcmpResponseStatus.DMI_SERVICE_NOT_RESPONDING +import static org.onap.cps.ncmp.api.NcmpResponseStatus.UNABLE_TO_READ_RESOURCE_DATA + import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.node.ObjectNode -import org.onap.cps.ncmp.api.impl.config.NcmpConfiguration -import org.onap.cps.ncmp.api.impl.config.NcmpConfiguration.DmiProperties; -import org.onap.cps.ncmp.api.impl.exception.HttpClientRequestException +import org.onap.cps.ncmp.api.impl.exception.DmiClientRequestException +import org.onap.cps.ncmp.api.impl.exception.InvalidDmiResourceUrlException +import org.onap.cps.ncmp.api.impl.config.DmiProperties import org.onap.cps.ncmp.utils.TestUtils -import org.spockframework.spring.SpringBean -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.context.SpringBootTest -import org.springframework.http.HttpEntity +import org.onap.cps.utils.JsonObjectMapper import org.springframework.http.HttpHeaders import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity -import org.springframework.test.context.ContextConfiguration import org.springframework.web.client.HttpServerErrorException -import org.springframework.web.client.RestTemplate +import org.springframework.web.reactive.function.client.WebClient +import org.springframework.web.reactive.function.client.WebClientResponseException +import org.springframework.web.reactive.function.client.WebClientRequestException +import reactor.core.publisher.Mono import spock.lang.Specification -import static org.onap.cps.ncmp.api.impl.operations.OperationType.READ -import static org.onap.cps.ncmp.api.impl.operations.OperationType.PATCH -import static org.onap.cps.ncmp.api.impl.operations.OperationType.CREATE - -@SpringBootTest -@ContextConfiguration(classes = [DmiProperties, DmiRestClient, ObjectMapper]) class DmiRestClientSpec extends Specification { static final NO_AUTH_HEADER = null - static final BASIC_AUTH_HEADER = 'Basic c29tZS11c2VyOnNvbWUtcGFzc3dvcmQ=' + static final BASIC_AUTH_HEADER = 'Basic c29tZSB1c2VyOnNvbWUgcGFzc3dvcmQ=' static final BEARER_AUTH_HEADER = 'Bearer my-bearer-token' - @SpringBean - RestTemplate mockRestTemplate = Mock(RestTemplate) + def mockDataServicesWebClient = Mock(WebClient) + def mockModelServicesWebClient = Mock(WebClient) + def mockHealthChecksWebClient = Mock(WebClient) - @Autowired - NcmpConfiguration.DmiProperties dmiProperties + def mockRequestBody = Mock(WebClient.RequestBodyUriSpec) + def mockResponse = Mock(WebClient.ResponseSpec) - @Autowired - DmiRestClient objectUnderTest + def responseBody = [message: 'Success'] + def mockDmiProperties = Mock(DmiProperties) - @Autowired - ObjectMapper objectMapper + JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(new ObjectMapper()) - def responseFromRestTemplate = Mock(ResponseEntity) + DmiRestClient objectUnderTest = new DmiRestClient(mockDmiProperties, jsonObjectMapper, mockDataServicesWebClient, mockModelServicesWebClient, mockHealthChecksWebClient) + + def setup() { + mockRequestBody.uri(_) >> mockRequestBody + mockRequestBody.headers(_) >> mockRequestBody + mockRequestBody.body(_) >> mockRequestBody + mockRequestBody.retrieve() >> mockResponse + } - def 'DMI POST operation with JSON.'() { - given: 'the rest template returns a valid response entity for the expected parameters' - mockRestTemplate.postForEntity('my url', _ as HttpEntity, Object.class) >> responseFromRestTemplate + def 'DMI POST Operation with JSON for status #httpStatusCode'() { + given: 'the web client returns a valid response entity for the expected parameters' + mockDataServicesWebClient.post() >> mockRequestBody + mockResponse.toEntity(Object.class) >> Mono.just(new ResponseEntity<>(responseBody, httpStatusCode)) when: 'POST operation is invoked' - def result = objectUnderTest.postOperationWithJsonData('my url', 'some json', READ, null) + def response = objectUnderTest.postOperationWithJsonData(DATA, '/my/url', 'some json', READ, NO_AUTH_HEADER) then: 'the output of the method is equal to the output from the test template' - result == responseFromRestTemplate + assert response.statusCode == httpStatusCode + assert response.body == responseBody + where: 'the following status codes are used' + httpStatusCode << [HttpStatus.OK, HttpStatus.CREATED, HttpStatus.ACCEPTED] } - def 'Failing DMI POST operation.'() { - given: 'the rest template returns a valid response entity' - def serverResponse = 'server response'.getBytes() - def httpServerErrorException = new HttpServerErrorException(HttpStatus.FORBIDDEN, 'status text', serverResponse, null) - mockRestTemplate.postForEntity(*_) >> { throw httpServerErrorException } + def 'Failing DMI POST operation for server error'() { + given: 'the web client throws an exception' + mockDataServicesWebClient.post() >> { throw new HttpServerErrorException(SERVICE_UNAVAILABLE, null, null, null) } when: 'POST operation is invoked' - def result = objectUnderTest.postOperationWithJsonData('some url', 'some json', operation, null) - then: 'a Http Client Exception is thrown' - def thrown = thrown(HttpClientRequestException) + objectUnderTest.postOperationWithJsonData(DATA, '/some', 'some json', READ, NO_AUTH_HEADER) + then: 'a http client exception is thrown' + def thrown = thrown(DmiClientRequestException) and: 'the exception has the relevant details from the error response' - assert thrown.httpStatus == 403 - assert thrown.message == "Unable to ${operation} resource data." - assert thrown.details == 'server response' - where: 'the following operation is executed' + thrown.ncmpResponseStatus.code == '102' + thrown.httpStatusCode == 503 + } + + def 'Failing DMI POST operation due to invalid dmi resource url.'() { + when: 'POST operation is invoked with invalid dmi resource url' + objectUnderTest.postOperationWithJsonData(DATA, '/invalid dmi url', null, null, NO_AUTH_HEADER) + then: 'invalid dmi resource url exception is thrown' + def thrown = thrown(InvalidDmiResourceUrlException) + and: 'the exception has the relevant details from the error response' + thrown.httpStatus == 400 + thrown.message == 'Invalid dmi resource url: /invalid dmi url' + where: 'the following operations are executed' operation << [CREATE, READ, PATCH] } + def 'Dmi service sends client error response when #scenario'() { + given: 'the web client unable to return response entity but error' + mockDataServicesWebClient.post() >> mockRequestBody + mockResponse.toEntity(Object.class) >> Mono.error(exceptionType) + when: 'POST operation is invoked' + objectUnderTest.postOperationWithJsonData(DATA, '/my/url', 'some json', READ, NO_AUTH_HEADER) + then: 'a http client exception is thrown' + def thrown = thrown(DmiClientRequestException) + and: 'the exception has the relevant details from the error response' + assert thrown.ncmpResponseStatus == expectedNcmpResponseStatusCode + assert thrown.httpStatusCode == httpStatusCode + where: 'the following errors occur' + scenario | httpStatusCode | exceptionType || expectedNcmpResponseStatusCode + 'dmi service unavailable' | 503 | new WebClientRequestException(new RuntimeException('some-error'), null, null, new HttpHeaders()) || DMI_SERVICE_NOT_RESPONDING + 'dmi request timeout' | 408 | new WebClientResponseException('message', httpStatusCode, 'statusText', null, null, null) || DMI_SERVICE_NOT_RESPONDING + 'dmi server error' | 500 | new WebClientResponseException('message', httpStatusCode, 'statusText', null, null, null) || UNABLE_TO_READ_RESOURCE_DATA + 'unknown error' | 500 | new Throwable('message') || UNKNOWN_ERROR + } + def 'Dmi trust level is determined by spring boot health status'() { given: 'a health check response' def dmiPluginHealthCheckResponseJsonData = TestUtils.getResourceFileContent('dmiPluginHealthCheckResponse.json') - def jsonNode = objectMapper.readValue(dmiPluginHealthCheckResponseJsonData, JsonNode.class) + def jsonNode = jsonObjectMapper.convertJsonString(dmiPluginHealthCheckResponseJsonData, JsonNode.class) ((ObjectNode) jsonNode).put('status', 'my status') - mockRestTemplate.getForObject(*_) >> {jsonNode} + mockHealthChecksWebClient.get() >> mockRequestBody + mockResponse.bodyToMono(JsonNode.class) >> Mono.just(jsonNode) when: 'get trust level of the dmi plugin' - def result = objectUnderTest.getDmiHealthStatus('some url') - then: 'the status value from the json is return' + def result = objectUnderTest.getDmiHealthStatus('some/url') + then: 'the status value from the json is returned' assert result == 'my status' } def 'Failing to get dmi plugin health status #scenario'() { given: 'rest template with #scenario' - mockRestTemplate.getForObject(*_) >> healthStatusResponse + mockHealthChecksWebClient.get() >> mockRequestBody + mockResponse.bodyToMono(_) >> healthStatusResponse when: 'attempt to get health status of the dmi plugin' def result = objectUnderTest.getDmiHealthStatus('some url') then: 'result will be empty' @@ -114,15 +155,18 @@ class DmiRestClientSpec extends Specification { where: 'the following responses are used' scenario | healthStatusResponse 'null' | null - 'exception' | {throw new Exception()} + 'exception' | { throw new Exception() } } def 'DMI auth header #scenario'() { when: 'Specific dmi properties are provided' - dmiProperties.dmiBasicAuthEnabled = authEnabled + mockDmiProperties.dmiBasicAuthEnabled >> authEnabled + mockDmiProperties.authUsername >> 'some user' + mockDmiProperties.authPassword >> 'some password' then: 'http headers to conditionally have Authorization header' - def authHeaderValues = objectUnderTest.configureHttpHeaders(new HttpHeaders(), ncmpAuthHeader).getOrEmpty('Authorization') - def outputAuthHeader = (authHeaderValues == null ? null : authHeaderValues[0]) + def httpHeaders = new HttpHeaders() + objectUnderTest.configureHttpHeaders(httpHeaders, ncmpAuthHeader) + def outputAuthHeader = (httpHeaders.Authorization == null ? null : httpHeaders.Authorization[0]) assert outputAuthHeader == expectedAuthHeader where: 'the following configurations are used' scenario | authEnabled | ncmpAuthHeader || expectedAuthHeader @@ -132,5 +176,4 @@ class DmiRestClientSpec extends Specification { 'DMI basic auth disabled, with NCMP bearer token' | false | BEARER_AUTH_HEADER || BEARER_AUTH_HEADER 'DMI basic auth disabled, with NCMP basic auth' | false | BASIC_AUTH_HEADER || NO_AUTH_HEADER } - -} +}
\ No newline at end of file diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/DmiPropertiesSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/DmiPropertiesSpec.groovy new file mode 100644 index 000000000..c763c522c --- /dev/null +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/DmiPropertiesSpec.groovy @@ -0,0 +1,37 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps.ncmp.api.impl.config + +import spock.lang.Specification + +class DmiPropertiesSpec extends Specification { + + def objectUnderTest = new DmiProperties() + + def 'Geting dmi base path.'() { + given: 'base path of #dmiBasePath' + objectUnderTest.dmiBasePath = dmiBasePath + expect: 'Preceding and trailing slash wil be removed' + assert objectUnderTest.getDmiBasePath() == 'test' + where: 'the following dmi base paths are used' + dmiBasePath << [ 'test' , '/test', 'test/', '/test/' ] + } +} diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/DmiWebClientConfigurationSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/DmiWebClientConfigurationSpec.groovy new file mode 100644 index 000000000..05ecaa11b --- /dev/null +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/DmiWebClientConfigurationSpec.groovy @@ -0,0 +1,68 @@ +/*- + * ============LICENSE_START======================================================= + * Copyright (C) 2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps.ncmp.api.impl.config + +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.context.TestPropertySource +import org.springframework.web.reactive.function.client.WebClient +import spock.lang.Specification + +@SpringBootTest +@ContextConfiguration(classes = [HttpClientConfiguration]) +@TestPropertySource(properties = ['ncmp.dmi.httpclient.data-services.connectionTimeoutInSeconds=1', 'ncmp.dmi.httpclient.model-services.maximumInMemorySizeInMegabytes=1']) +@EnableConfigurationProperties +class DmiWebClientConfigurationSpec extends Specification { + + def httpClientConfiguration = Spy(HttpClientConfiguration.class) + + def objectUnderTest = new DmiWebClientConfiguration(httpClientConfiguration) + + def 'Web Client Configuration construction.'() { + expect: 'the system can create an instance' + new DmiWebClientConfiguration(httpClientConfiguration) != null + } + + def 'Creating a web client instance data service.'() { + given: 'Web client configuration is invoked' + def dataServicesWebClient = objectUnderTest.dataServicesWebClient() + expect: 'the system can create an instance for data service' + assert dataServicesWebClient != null + assert dataServicesWebClient instanceof WebClient + } + + def 'Creating a web client instance model service.'() { + given: 'Web client configuration invoked' + def modelServicesWebClient = objectUnderTest.modelServicesWebClient() + expect: 'the system can create an instance for model service' + assert modelServicesWebClient != null + assert modelServicesWebClient instanceof WebClient + } + + def 'Creating a web client instance health service.'() { + given: 'Web client configuration invoked' + def healthChecksWebClient = objectUnderTest.healthChecksWebClient() + expect: 'the system can create an instance for health service' + assert healthChecksWebClient != null + assert healthChecksWebClient instanceof WebClient + } +} diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/HttpClientConfigurationSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/HttpClientConfigurationSpec.groovy index 2c76b5bb4..b7ced2382 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/HttpClientConfigurationSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/HttpClientConfigurationSpec.groovy @@ -1,6 +1,6 @@ /*- * ============LICENSE_START======================================================= - * Copyright (C) 2023 Nordix Foundation. + * Copyright (C) 2023-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. @@ -17,32 +17,55 @@ * SPDX-License-Identifier: Apache-2.0 * ============LICENSE_END========================================================= */ + package org.onap.cps.ncmp.api.impl.config -import java.time.Duration import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.boot.test.context.SpringBootTest import org.springframework.test.context.ContextConfiguration import org.springframework.test.context.TestPropertySource -import org.springframework.test.context.support.AnnotationConfigContextLoader import spock.lang.Specification @SpringBootTest @ContextConfiguration(classes = [HttpClientConfiguration]) @EnableConfigurationProperties(HttpClientConfiguration.class) -@TestPropertySource(properties = ["ncmp.dmi.httpclient.connectionTimeoutInSeconds=1", "ncmp.dmi.httpclient.maximumConnectionsTotal=200"]) +@TestPropertySource(properties = ["ncmp.dmi.httpclient.data-services.readTimeoutInSeconds=789", "ncmp.dmi.httpclient.model-services.maximumConnectionsTotal=111"]) class HttpClientConfigurationSpec extends Specification { @Autowired private HttpClientConfiguration httpClientConfiguration - def 'Test HttpClientConfiguration properties with custom and default values'() { - expect: 'custom property values' - assert httpClientConfiguration.getConnectionTimeoutInSeconds() == Duration.ofSeconds(1) - assert httpClientConfiguration.getMaximumConnectionsTotal() == 200 - and: 'default property values' - assert httpClientConfiguration.getMaximumConnectionsPerRoute() == 50 - assert httpClientConfiguration.getIdleConnectionEvictionThresholdInSeconds() == Duration.ofSeconds(5) + def 'Test http client configuration properties of data with custom and default values'() { + expect: 'properties are populated correctly for data' + with(httpClientConfiguration.dataServices) { + assert connectionTimeoutInSeconds == 123 + assert readTimeoutInSeconds == 789 + assert writeTimeoutInSeconds == 30 + assert maximumConnectionsTotal == 100 + assert maximumInMemorySizeInMegabytes == 7 + } + } + + def 'Test http client configuration properties of model with custom and default values'() { + expect: 'properties are populated correctly for model' + with(httpClientConfiguration.modelServices) { + assert connectionTimeoutInSeconds == 456 + assert readTimeoutInSeconds == 30 + assert writeTimeoutInSeconds == 30 + assert maximumConnectionsTotal == 111 + assert maximumInMemorySizeInMegabytes == 8 + } + } + + def 'Test http client configuration properties of health with default values'() { + expect: 'properties are populated correctly for health' + with(httpClientConfiguration.healthCheckServices) { + assert connectionTimeoutInSeconds == 30 + assert readTimeoutInSeconds == 30 + assert writeTimeoutInSeconds == 30 + assert maximumConnectionsTotal == 10 + assert maximumInMemorySizeInMegabytes == 1 + } } } diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/NcmpConfigurationSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/NcmpConfigurationSpec.groovy deleted file mode 100644 index 74e342405..000000000 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/NcmpConfigurationSpec.groovy +++ /dev/null @@ -1,70 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * Copyright (C) 2021-2023 Nordix Foundation - * ================================================================================ - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - * ============LICENSE_END========================================================= - */ -package org.onap.cps.ncmp.api.impl.config - -import org.apache.hc.client5.http.impl.classic.CloseableHttpClient -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.context.SpringBootTest -import org.springframework.boot.web.client.RestTemplateBuilder -import org.springframework.http.MediaType -import org.springframework.http.client.HttpComponentsClientHttpRequestFactory -import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter -import org.springframework.test.context.ContextConfiguration -import org.springframework.web.client.RestTemplate -import spock.lang.Specification - -@SpringBootTest -@ContextConfiguration(classes = [NcmpConfiguration.DmiProperties, HttpClientConfiguration]) -class NcmpConfigurationSpec extends Specification{ - - @Autowired - NcmpConfiguration.DmiProperties dmiProperties - - @Autowired - HttpClientConfiguration httpClientConfiguration - - def mockRestTemplateBuilder = new RestTemplateBuilder() - - def 'NcmpConfiguration Construction.'() { - expect: 'the system can create an instance' - new NcmpConfiguration() != null - } - - def 'DMI Properties.'() { - expect: 'properties are set to values in test configuration yaml file' - dmiProperties.authUsername == 'some-user' - dmiProperties.authPassword == 'some-password' - } - - def 'Rest Template creation with CloseableHttpClient and MappingJackson2HttpMessageConverter.'() { - when: 'a rest template is created' - def result = NcmpConfiguration.restTemplate(mockRestTemplateBuilder, httpClientConfiguration) - then: 'the rest template is returned' - assert result instanceof RestTemplate - and: 'the rest template is created with httpclient5' - assert result.getRequestFactory() instanceof HttpComponentsClientHttpRequestFactory - assert ((HttpComponentsClientHttpRequestFactory) result.getRequestFactory()).getHttpClient() instanceof CloseableHttpClient; - and: 'a jackson media converter has been added' - def lastMessageConverter = result.getMessageConverters().get(result.getMessageConverters().size()-1) - lastMessageConverter instanceof MappingJackson2HttpMessageConverter - and: 'the jackson media converters supports the expected media types' - lastMessageConverter.getSupportedMediaTypes() == [MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN]; - } -} diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/OpenTelemetryConfigSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/OpenTelemetryConfigSpec.groovy new file mode 100644 index 000000000..07395cf5b --- /dev/null +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/OpenTelemetryConfigSpec.groovy @@ -0,0 +1,81 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps.ncmp.api.impl.config + +import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter +import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter +import io.opentelemetry.sdk.extension.trace.jaeger.sampler.JaegerRemoteSampler +import org.spockframework.spring.SpringBean +import org.springframework.boot.actuate.autoconfigure.observation.ObservationRegistryCustomizer +import spock.lang.Shared +import spock.lang.Specification + +class OpenTelemetryConfigSpec extends Specification{ + + @Shared + @SpringBean + OpenTelemetryConfig openTelemetryConfig = new OpenTelemetryConfig() + + def setupSpec() { + openTelemetryConfig.tracingExporterEndpointUrl="http://tracingExporterEndpointUrl" + openTelemetryConfig.jaegerRemoteSamplerUrl="http://jaegerremotesamplerurl" + openTelemetryConfig.serviceId ="cps-application" + } + + def 'OpenTelemetryConfig Construction.'() { + expect: 'the system can create an instance' + new OpenTelemetryConfig() != null + } + + def 'OTLP Exporter creation with Grpc protocol'(){ + when: 'an OTLP exporter is created' + def result = openTelemetryConfig.createOtlpExporterGrpc() + then: 'an OTLP Exporter is created' + assert result instanceof OtlpGrpcSpanExporter + } + + def 'OTLP Exporter creation with HTTP protocol'(){ + when: 'an OTLP exporter is created' + def result = openTelemetryConfig.createOtlpExporterHttp() + then: 'an OTLP Exporter is created' + assert result instanceof OtlpHttpSpanExporter + and: + assert result.builder.endpoint=="http://tracingExporterEndpointUrl" + } + + def 'Jaeger Remote Sampler Creation'(){ + when: 'an OTLP exporter is created' + def result = openTelemetryConfig.createJaegerRemoteSampler() + then: 'an OTLP Exporter is created' + assert result instanceof JaegerRemoteSampler + and: + assert result.delegate.type=="remoteSampling" + and: + assert result.delegate.url.toString().startsWith("http://jaegerremotesamplerurl") + } + + def 'Skipping Acutator endpoints'(){ + when: 'an OTLP exporter is created' + def result = openTelemetryConfig.skipActuatorEndpointsFromObservation() + then: 'an OTLP Exporter is created' + assert result instanceof ObservationRegistryCustomizer + } +} diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/kafka/KafkaConfigSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/kafka/KafkaConfigSpec.groovy index 16f27d081..4d3fd6616 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/kafka/KafkaConfigSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/kafka/KafkaConfigSpec.groovy @@ -18,7 +18,7 @@ * ============LICENSE_END========================================================= */ -package org.onap.cps.ncmp.api.impl.config.kafka; +package org.onap.cps.ncmp.api.impl.config.kafka import io.cloudevents.CloudEvent import io.cloudevents.kafka.CloudEventDeserializer @@ -31,12 +31,14 @@ import org.springframework.boot.test.context.SpringBootTest import org.springframework.kafka.core.KafkaTemplate import org.springframework.kafka.support.serializer.JsonDeserializer import org.springframework.kafka.support.serializer.JsonSerializer +import org.springframework.test.context.TestPropertySource import spock.lang.Shared import spock.lang.Specification @SpringBootTest(classes = [KafkaProperties, KafkaConfig]) @EnableSharedInjection @EnableConfigurationProperties +@TestPropertySource(properties = ["cps.tracing.enabled=true"]) class KafkaConfigSpec extends Specification { @Shared diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/CmNotificationSubscriptionDeltaSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/CmNotificationSubscriptionDeltaSpec.groovy index e50652689..75db0bfe5 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/CmNotificationSubscriptionDeltaSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/CmNotificationSubscriptionDeltaSpec.groovy @@ -46,4 +46,15 @@ class CmNotificationSubscriptionDeltaSpec extends Specification { } + def 'Find Delta of given list of predicates when it is an ongoing Cm Subscription'() { + given: 'A list of predicates' + def predicateList = [new DmiCmNotificationSubscriptionPredicate(['ch-1'].toSet(), DatastoreType.PASSTHROUGH_OPERATIONAL, ['a/1/'].toSet())] + and: 'its already present' + mockCmNotificationSubscriptionPersistenceService.isOngoingCmNotificationSubscription(DatastoreType.PASSTHROUGH_OPERATIONAL, 'ch-1', 'a/1/') >>> true + when: 'getDelta is called' + def result = objectUnderTest.getDelta(predicateList) + then: 'verify correct delta is returned' + assert result.size() == 0 + } + } diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/CmNotificationSubscriptionDmiInEventProducerSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/CmNotificationSubscriptionDmiInEventProducerSpec.groovy index cfb28a0ad..039a18949 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/CmNotificationSubscriptionDmiInEventProducerSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/CmNotificationSubscriptionDmiInEventProducerSpec.groovy @@ -25,8 +25,8 @@ import io.cloudevents.CloudEvent import org.onap.cps.events.EventsPublisher import org.onap.cps.ncmp.api.impl.events.cmsubscription.producer.CmNotificationSubscriptionDmiInEventProducer import org.onap.cps.ncmp.api.impl.events.mapper.CloudEventMapper +import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.ncmp_to_dmi.CmHandle import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.ncmp_to_dmi.CmNotificationSubscriptionDmiInEvent -import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.ncmp_to_dmi.Cmhandle import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.ncmp_to_dmi.Data import org.onap.cps.utils.JsonObjectMapper import spock.lang.Specification @@ -43,7 +43,7 @@ class CmNotificationSubscriptionDmiInEventProducerSpec extends Specification { def subscriptionId = 'test-subscription-id' def dmiPluginName = 'test-dmiplugin' def eventType = 'subscriptionCreateRequest' - def cmNotificationSubscriptionDmiInEvent = new CmNotificationSubscriptionDmiInEvent(data: new Data(cmhandles: [new Cmhandle(cmhandleId: 'test-1', privateProperties: [:])])) + def cmNotificationSubscriptionDmiInEvent = new CmNotificationSubscriptionDmiInEvent(data: new Data(cmHandles: [new CmHandle(cmhandleId: 'test-1', privateProperties: [:])])) and: 'also we have target topic for dmiPlugin' objectUnderTest.cmNotificationSubscriptionDmiInEventTopic = 'dmiplugin-test-topic' when: 'the event is published' diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/CmNotificationSubscriptionDmiOutEventConsumerSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/CmNotificationSubscriptionDmiOutEventConsumerSpec.groovy index 488879db7..9b0a48d93 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/CmNotificationSubscriptionDmiOutEventConsumerSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/CmNotificationSubscriptionDmiOutEventConsumerSpec.groovy @@ -36,7 +36,6 @@ import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.dmi_to_ncm import org.onap.cps.ncmp.utils.TestUtils import org.onap.cps.utils.JsonObjectMapper import org.slf4j.LoggerFactory -import org.spockframework.spring.SpringBean import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest @@ -108,9 +107,9 @@ class CmNotificationSubscriptionDmiOutEventConsumerSpec extends MessagingBaseSpe and: 'correct number of calls to publish the ncmp out event to client' 1 * mockCmNotificationSubscriptionEventsHandler.publishCmNotificationSubscriptionNcmpOutEvent('sub-1', 'subscriptionCreateResponse', _, false) where: 'the following parameters are used' - scenario | subscriptionStatus | statusCode || expectedCacheCalls | expectedPersistenceCalls - 'Accepted Status' | CmNotificationSubscriptionStatus.ACCEPTED | '1' || 1 | 1 - 'Rejected Status' | CmNotificationSubscriptionStatus.REJECTED | '2' || 1 | 0 + scenario | subscriptionStatus | statusCode || expectedCacheCalls | expectedPersistenceCalls + 'Accepted Status' | CmNotificationSubscriptionStatus.ACCEPTED | '1' || 1 | 1 + 'Rejected Status' | CmNotificationSubscriptionStatus.REJECTED | '104' || 1 | 0 } def getLoggingEvent() { diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/CmNotificationSubscriptionNcmpInEventConsumerSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/CmNotificationSubscriptionNcmpInEventConsumerSpec.groovy index 9c84c51b2..01a92c02f 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/CmNotificationSubscriptionNcmpInEventConsumerSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/CmNotificationSubscriptionNcmpInEventConsumerSpec.groovy @@ -72,19 +72,40 @@ class CmNotificationSubscriptionNcmpInEventConsumerSpec extends MessagingBaseSpe .withSource(URI.create('some-resource')) .withExtension('correlationid', 'test-cmhandle1').build() def consumerRecord = new ConsumerRecord<String, CloudEvent>('topic-name', 0, 0, 'event-key', testCloudEventSent) - and: 'notifications are enabled' - objectUnderTest.notificationFeatureEnabled = true when: 'the valid event is consumed' objectUnderTest.consumeSubscriptionEvent(consumerRecord) then: 'an event is logged with level INFO' def loggingEvent = getLoggingEvent() assert loggingEvent.level == Level.INFO and: 'the log indicates the task completed successfully' - assert loggingEvent.formattedMessage == 'Subscription for source some-resource with subscription id test-id ...' + assert loggingEvent.formattedMessage == 'Subscription create request for source some-resource with subscription id test-id ...' and: 'the subscription handler service is called once' - 1 * mockCmNotificationSubscriptionHandlerService.processSubscriptionCreateRequest(_) + 1 * mockCmNotificationSubscriptionHandlerService.processSubscriptionCreateRequest('test-id',_) } + def 'Consume valid CmNotificationSubscriptionNcmpInEvent delete message'() { + given: 'a cmNotificationSubscription event' + def jsonData = TestUtils.getResourceFileContent('cmSubscription/cmNotificationSubscriptionNcmpInEvent.json') + def testEventSent = jsonObjectMapper.convertJsonString(jsonData, CmNotificationSubscriptionNcmpInEvent.class) + def testCloudEventSent = CloudEventBuilder.v1() + .withData(objectMapper.writeValueAsBytes(testEventSent)) + .withId('sub-id') + .withType('subscriptionDeleteRequest') + .withSource(URI.create('some-resource')) + .withExtension('correlationid', 'test-cmhandle1').build() + def consumerRecord = new ConsumerRecord<String, CloudEvent>('topic-name', 0, 0, 'event-key', testCloudEventSent) + when: 'the valid event is consumed' + objectUnderTest.consumeSubscriptionEvent(consumerRecord) + then: 'an event is logged with level INFO' + def loggingEvent = getLoggingEvent() + assert loggingEvent.level == Level.INFO + and: 'the log indicates the task completed successfully' + assert loggingEvent.formattedMessage == 'Subscription delete request for source some-resource with subscription id test-id ...' + and: 'the subscription handler service is called once' + 1 * mockCmNotificationSubscriptionHandlerService.processSubscriptionDeleteRequest('test-id',_) + } + + def getLoggingEvent() { return logger.list[1] } diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/DmiCmNotificationSubscriptionCacheHandlerSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/DmiCmNotificationSubscriptionCacheHandlerSpec.groovy index 43568be50..8d7a4b9cf 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/DmiCmNotificationSubscriptionCacheHandlerSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/DmiCmNotificationSubscriptionCacheHandlerSpec.groovy @@ -133,10 +133,10 @@ class DmiCmNotificationSubscriptionCacheHandlerSpec extends MessagingBaseSpec { assert resultMapForDmi2.dmiCmNotificationSubscriptionPredicates[1].targetCmHandleIds == ['ch4'].toSet() and: 'the list of xpath for each is correct' assert resultMapForDmi1.dmiCmNotificationSubscriptionPredicates[0].xpaths - && resultMapForDmi2.dmiCmNotificationSubscriptionPredicates[0].xpaths == ['/x1/y1','x2/y2'].toSet() + && resultMapForDmi2.dmiCmNotificationSubscriptionPredicates[0].xpaths == ['/x1/y1','x2/y2'].toSet() assert resultMapForDmi1.dmiCmNotificationSubscriptionPredicates[1].xpaths - && resultMapForDmi2.dmiCmNotificationSubscriptionPredicates[1].xpaths == ['/x3/y3','x4/y4'].toSet() + && resultMapForDmi2.dmiCmNotificationSubscriptionPredicates[1].xpaths == ['/x3/y3','x4/y4'].toSet() } def 'Get map for cm handle IDs by DMI service name'() { @@ -164,7 +164,7 @@ class DmiCmNotificationSubscriptionCacheHandlerSpec extends MessagingBaseSpec { } def 'Persist Cache into database per dmi'() { - given: 'populate cache' + given: 'populated cache' def predicates = cmNotificationSubscriptionNcmpInEvent.getData().getPredicates() def subscriptionId = cmNotificationSubscriptionNcmpInEvent.getData().getSubscriptionId() objectUnderTest.add(subscriptionId, predicates) @@ -174,15 +174,26 @@ class DmiCmNotificationSubscriptionCacheHandlerSpec extends MessagingBaseSpec { 4 * mockCmNotificationSubscriptionPersistenceService.addCmNotificationSubscription(_,_,_,subscriptionId) } + def 'Remove subscription from database per dmi'() { + given: 'populated cache' + def predicates = cmNotificationSubscriptionNcmpInEvent.getData().getPredicates() + def subscriptionId = cmNotificationSubscriptionNcmpInEvent.getData().getSubscriptionId() + objectUnderTest.add(subscriptionId, predicates) + when: 'subscription is persisted in database' + objectUnderTest.removeFromDatabasePerDmi(subscriptionId,'dmi-1') + then: 'persistence service is called the correct number of times per dmi' + 4 * mockCmNotificationSubscriptionPersistenceService.removeCmNotificationSubscription(_,_,_,subscriptionId) + } + def setUpTestEvent(){ def jsonData = TestUtils.getResourceFileContent('cmSubscription/cmNotificationSubscriptionNcmpInEvent.json') def testEventSent = jsonObjectMapper.convertJsonString(jsonData, CmNotificationSubscriptionNcmpInEvent.class) def testCloudEventSent = CloudEventBuilder.v1() - .withData(objectMapper.writeValueAsBytes(testEventSent)) - .withId('subscriptionCreated') - .withType('subscriptionCreated') - .withSource(URI.create('some-resource')) - .withExtension('correlationid', 'test-cmhandle1').build() + .withData(objectMapper.writeValueAsBytes(testEventSent)) + .withId('subscriptionCreated') + .withType('subscriptionCreated') + .withSource(URI.create('some-resource')) + .withExtension('correlationid', 'test-cmhandle1').build() def consumerRecord = new ConsumerRecord<String, CloudEvent>('topic-name', 0, 0, 'event-key', testCloudEventSent) def cloudEvent = consumerRecord.value() @@ -191,10 +202,10 @@ class DmiCmNotificationSubscriptionCacheHandlerSpec extends MessagingBaseSpec { def initialiseMockInventoryPersistenceResponses(){ mockInventoryPersistence.getYangModelCmHandles(['ch1','ch2']) - >> [yangModelCmHandle1, yangModelCmHandle2] + >> [yangModelCmHandle1, yangModelCmHandle2] mockInventoryPersistence.getYangModelCmHandles(['ch3','ch4']) - >> [yangModelCmHandle3, yangModelCmHandle4] + >> [yangModelCmHandle3, yangModelCmHandle4] } }
\ No newline at end of file diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/mapper/CmNotificationSubscriptionDmiInEventMapperSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/mapper/CmNotificationSubscriptionDmiInEventMapperSpec.groovy index 763aedaa0..cf72b2925 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/mapper/CmNotificationSubscriptionDmiInEventMapperSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/mapper/CmNotificationSubscriptionDmiInEventMapperSpec.groovy @@ -48,8 +48,8 @@ class CmNotificationSubscriptionDmiInEventMapperSpec extends Specification { when: 'we try to map the values' def result = objectUnderTest.toCmNotificationSubscriptionDmiInEvent(dmiCmNotificationSubscriptionPredicates) then: 'it contains correct cm notification subscription cmhandle object' - assert result.data.cmhandles.cmhandleId.containsAll(['ch-1', 'ch-2']) - assert result.data.cmhandles.privateProperties.containsAll([['k1': 'v1'], ['k2': 'v2']]) + assert result.data.cmHandles.cmhandleId.containsAll(['ch-1', 'ch-2']) + assert result.data.cmHandles.privateProperties.containsAll([['k1': 'v1'], ['k2': 'v2']]) and: 'also has the correct dmi cm notification subscription predicates' assert result.data.predicates.targetFilter.containsAll([['ch-1'], ['ch-2']]) diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/service/CmNotificationSubscriptionHandlerServiceImplSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/service/CmNotificationSubscriptionHandlerServiceImplSpec.groovy index 98b4ee267..982150ec0 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/service/CmNotificationSubscriptionHandlerServiceImplSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/service/CmNotificationSubscriptionHandlerServiceImplSpec.groovy @@ -25,10 +25,10 @@ import org.onap.cps.ncmp.api.impl.events.cmsubscription.CmNotificationSubscripti import org.onap.cps.ncmp.api.impl.events.cmsubscription.CmNotificationSubscriptionEventsHandler import org.onap.cps.ncmp.api.impl.events.cmsubscription.CmNotificationSubscriptionMappersHandler import org.onap.cps.ncmp.api.impl.events.cmsubscription.DmiCmNotificationSubscriptionCacheHandler -import org.onap.cps.ncmp.api.impl.events.cmsubscription.mapper.CmNotificationSubscriptionDmiInEventMapper -import org.onap.cps.ncmp.api.impl.events.cmsubscription.mapper.CmNotificationSubscriptionNcmpOutEventMapper import org.onap.cps.ncmp.api.impl.events.cmsubscription.model.CmNotificationSubscriptionStatus import org.onap.cps.ncmp.api.impl.events.cmsubscription.model.DmiCmNotificationSubscriptionDetails +import org.onap.cps.ncmp.api.impl.events.cmsubscription.model.DmiCmNotificationSubscriptionPredicate +import org.onap.cps.ncmp.api.impl.operations.DatastoreType import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.client_to_ncmp.CmNotificationSubscriptionNcmpInEvent import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.ncmp_to_dmi.CmNotificationSubscriptionDmiInEvent import org.onap.cps.ncmp.events.cmsubscription_merge1_0_0.ncmp_to_client.CmNotificationSubscriptionNcmpOutEvent @@ -50,13 +50,16 @@ class CmNotificationSubscriptionHandlerServiceImplSpec extends Specification{ mockCmNotificationSubscriptionEventsHandler, mockDmiCmNotificationSubscriptionCacheHandler) def testSubscriptionDetailsMap = ["dmi-1":new DmiCmNotificationSubscriptionDetails([], CmNotificationSubscriptionStatus.PENDING)] - def testListOfDeltaPredicates = [] def 'Consume valid and unique CmNotificationSubscriptionNcmpInEvent create message'() { given: 'a cmNotificationSubscriptionNcmp in event with unique subscription id' def jsonData = TestUtils.getResourceFileContent('cmSubscription/cmNotificationSubscriptionNcmpInEvent.json') def testEventConsumed = jsonObjectMapper.convertJsonString(jsonData, CmNotificationSubscriptionNcmpInEvent.class) + def testListOfDeltaPredicates = [new DmiCmNotificationSubscriptionPredicate(['ch1'].toSet(), DatastoreType.PASSTHROUGH_OPERATIONAL, ['/a/b'].toSet())] mockCmNotificationSubscriptionPersistenceService.isUniqueSubscriptionId("test-id") >> true + and: 'relevant details is extracted from the event' + def subscriptionId = testEventConsumed.getData().getSubscriptionId() + def predicates = testEventConsumed.getData().getPredicates() and: 'the cache handler returns for relevant subscription id' 1 * mockDmiCmNotificationSubscriptionCacheHandler.get("test-id") >> testSubscriptionDetailsMap and: 'the delta predicates is returned' @@ -66,7 +69,7 @@ class CmNotificationSubscriptionHandlerServiceImplSpec extends Specification{ 1 * mockCmNotificationSubscriptionMappersHandler .toCmNotificationSubscriptionDmiInEvent(testListOfDeltaPredicates) >> testDmiInEvent when: 'the valid and unique event is consumed' - objectUnderTest.processSubscriptionCreateRequest(testEventConsumed) + objectUnderTest.processSubscriptionCreateRequest(subscriptionId, predicates) then: 'the subscription cache handler is called once' 1 * mockDmiCmNotificationSubscriptionCacheHandler.add('test-id',_) and: 'the events handler method to publish DMI event is called correct number of times with the correct parameters' @@ -77,21 +80,68 @@ class CmNotificationSubscriptionHandlerServiceImplSpec extends Specification{ "test-id", "subscriptionCreateResponse", null, true) } + def 'Consume valid and Overlapping Cm Notification Subscription NcmpIn Event'() { + given: 'a cmNotificationSubscriptionNcmp in event with unique subscription id' + def jsonData = TestUtils.getResourceFileContent('cmSubscription/cmNotificationSubscriptionNcmpInEvent.json') + def testEventConsumed = jsonObjectMapper.convertJsonString(jsonData, CmNotificationSubscriptionNcmpInEvent.class) + def noDeltaPredicates = [] + mockCmNotificationSubscriptionPersistenceService.isUniqueSubscriptionId("test-id") >> true + and: 'the cache handler returns for relevant subscription id' + 1 * mockDmiCmNotificationSubscriptionCacheHandler.get("test-id") >> testSubscriptionDetailsMap + and: 'the delta predicates is returned' + 1 * mockCmNotificationSubscriptionDelta.getDelta(_) >> noDeltaPredicates + when: 'the valid and unique event is consumed' + objectUnderTest.processSubscriptionCreateRequest('test-id', noDeltaPredicates) + then: 'the subscription cache handler is called once' + 1 * mockDmiCmNotificationSubscriptionCacheHandler.add('test-id', _) + and: 'the subscription details are updated in the cache' + 1 * mockDmiCmNotificationSubscriptionCacheHandler.updateDmiCmNotificationSubscriptionStatusPerDmi('test-id', _, CmNotificationSubscriptionStatus.ACCEPTED) + and: 'we schedule to send the response after configured time from the cache' + 1 * mockCmNotificationSubscriptionEventsHandler.publishCmNotificationSubscriptionNcmpOutEvent( + "test-id", "subscriptionCreateResponse", null, true) + } + def 'Consume valid and but non-unique CmNotificationSubscription create message'() { given: 'a cmNotificationSubscriptionNcmp in event' def jsonData = TestUtils.getResourceFileContent('cmSubscription/cmNotificationSubscriptionNcmpInEvent.json') def testEventConsumed = jsonObjectMapper.convertJsonString(jsonData, CmNotificationSubscriptionNcmpInEvent.class) mockCmNotificationSubscriptionPersistenceService.isUniqueSubscriptionId('test-id') >> false + and: 'relevant details is extracted from the event' + def subscriptionId = testEventConsumed.getData().getSubscriptionId() + def predicates = testEventConsumed.getData().getPredicates() and: 'the NCMP out in event mapper returns an event for rejected request' def testNcmpOutEvent = new CmNotificationSubscriptionNcmpOutEvent() 1 * mockCmNotificationSubscriptionMappersHandler.toCmNotificationSubscriptionNcmpOutEventForRejectedRequest( "test-id",_) >> testNcmpOutEvent when: 'the valid but non-unique event is consumed' - objectUnderTest.processSubscriptionCreateRequest(testEventConsumed) + objectUnderTest.processSubscriptionCreateRequest(subscriptionId, predicates) then: 'the events handler method to publish DMI event is never called' 0 * mockCmNotificationSubscriptionEventsHandler.publishCmNotificationSubscriptionDmiInEvent(_,_,_,_) and: 'the events handler method to publish NCMP out event is called once' 1 * mockCmNotificationSubscriptionEventsHandler.publishCmNotificationSubscriptionNcmpOutEvent( 'test-id', 'subscriptionCreateResponse', testNcmpOutEvent, false) } + + def 'Consume valid CmNotificationSubscriptionNcmpInEvent delete message'() { + given: 'a cmNotificationSubscriptionNcmp in event for delete' + def jsonData = TestUtils.getResourceFileContent('cmSubscription/cmNotificationSubscriptionNcmpInEvent.json') + def testEventConsumed = jsonObjectMapper.convertJsonString(jsonData, CmNotificationSubscriptionNcmpInEvent.class) + and: 'relevant details is extracted from the event' + def subscriptionId = testEventConsumed.getData().getSubscriptionId() + def predicates = testEventConsumed.getData().getPredicates() + and: 'the cache handler returns for relevant subscription id' + 1 * mockDmiCmNotificationSubscriptionCacheHandler.get('test-id') >> testSubscriptionDetailsMap + when: 'the valid and unique event is consumed' + objectUnderTest.processSubscriptionDeleteRequest(subscriptionId, predicates) + then: 'the subscription cache handler is called once' + 1 * mockDmiCmNotificationSubscriptionCacheHandler.add('test-id', predicates) + and: 'the mapper handler to get DMI in event is called once' + 1 * mockCmNotificationSubscriptionMappersHandler.toCmNotificationSubscriptionDmiInEvent(_) + and: 'the events handler method to publish DMI event is called correct number of times with the correct parameters' + testSubscriptionDetailsMap.size() * mockCmNotificationSubscriptionEventsHandler.publishCmNotificationSubscriptionDmiInEvent( + 'test-id', 'dmi-1', 'subscriptionDeleteRequest', _) + and: 'we schedule to send the response after configured time from the cache' + 1 * mockCmNotificationSubscriptionEventsHandler.publishCmNotificationSubscriptionNcmpOutEvent( + 'test-id', 'subscriptionDeleteResponse', null, true) + } } diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/service/CmNotificationSubscriptionPersistenceServiceImplSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/service/CmNotificationSubscriptionPersistenceServiceImplSpec.groovy index b51ecb0cf..281ec4f7e 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/service/CmNotificationSubscriptionPersistenceServiceImplSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/service/CmNotificationSubscriptionPersistenceServiceImplSpec.groovy @@ -1,6 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (c) 2024 Nordix Foundation. + * Modifications Copyright (C) 2024 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,9 +24,8 @@ package org.onap.cps.ncmp.api.impl.events.cmsubscription.service import org.onap.cps.utils.ContentType import static org.onap.cps.ncmp.api.impl.events.cmsubscription.service.CmNotificationSubscriptionPersistenceServiceImpl.CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_WITH_ID; -import static org.onap.cps.ncmp.api.impl.events.cmsubscription.service.CmNotificationSubscriptionPersistenceServiceImpl.CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_WITH_DATASTORE_AND_CMHANDLE; +import static org.onap.cps.ncmp.api.impl.events.cmsubscription.service.CmNotificationSubscriptionPersistenceServiceImpl.CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_FILTERS_WITH_DATASTORE_AND_CMHANDLE; import static org.onap.cps.ncmp.api.impl.events.cmsubscription.service.CmNotificationSubscriptionPersistenceServiceImpl.CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_WITH_DATASTORE_CMHANDLE_AND_XPATH; - import org.onap.cps.api.CpsDataService import org.onap.cps.api.CpsQueryService import org.onap.cps.ncmp.api.impl.operations.DatastoreType @@ -48,7 +48,7 @@ class CmNotificationSubscriptionPersistenceServiceImplSpec extends Specification def cpsPathQuery = "/datastores/datastore[@name='ncmp-datastore:passthrough-running']/cm-handles/cm-handle[@id='ch-1']/filters/filter[@xpath='/cps/path']"; and: 'datanodes optionally returned' 1 * mockCpsQueryService.queryDataNodes('NCMP-Admin', 'cm-data-subscriptions', - cpsPathQuery, FetchDescendantsOption.OMIT_DESCENDANTS) >> dataNode + cpsPathQuery, FetchDescendantsOption.OMIT_DESCENDANTS) >> dataNode when: 'we check for an ongoing cm subscription' def response = objectUnderTest.isOngoingCmNotificationSubscription(DatastoreType.PASSTHROUGH_RUNNING, 'ch-1', '/cps/path') then: 'we get expected response' @@ -64,7 +64,7 @@ class CmNotificationSubscriptionPersistenceServiceImplSpec extends Specification def cpsPathQuery = CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_WITH_ID.formatted('some-sub') and: 'relevant datanodes are returned' 1 * mockCpsQueryService.queryDataNodes('NCMP-Admin', 'cm-data-subscriptions', cpsPathQuery, FetchDescendantsOption.OMIT_DESCENDANTS) >> - dataNodes + dataNodes when: 'a subscription ID is tested for uniqueness' def result = objectUnderTest.isUniqueSubscriptionId('some-sub') then: 'result is as expected' @@ -79,7 +79,7 @@ class CmNotificationSubscriptionPersistenceServiceImplSpec extends Specification given: 'a valid cm subscription path query' def cpsPathQuery =CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_WITH_DATASTORE_CMHANDLE_AND_XPATH.formatted('ncmp-datastore:passthrough-running', 'ch-1', '/x/y') and: 'a dataNode exists for the given cps path query' - mockCpsQueryService.queryDataNodes('NCMP-Admin', 'cm-data-subscriptions', + mockCpsQueryService.queryDataNodes('NCMP-Admin', 'cm-data-subscriptions', cpsPathQuery, FetchDescendantsOption.OMIT_DESCENDANTS) >> [new DataNode(xpath: cpsPathQuery, leaves: ['xpath': '/x/y','subscriptionIds': ['sub-1']])] when: 'the method to add/update cm notification subscription is called' objectUnderTest.addCmNotificationSubscription(DatastoreType.PASSTHROUGH_RUNNING, 'ch-1','/x/y', 'newSubId') @@ -88,18 +88,18 @@ class CmNotificationSubscriptionPersistenceServiceImplSpec extends Specification 'NCMP-Admin', 'cm-data-subscriptions', '/datastores/datastore[@name=\'ncmp-datastore:passthrough-running\']/cm-handles/cm-handle[@id=\'ch-1\']/filters', - objectUnderTest.getSubscriptionDetailsAsJson('/x/y', ['sub-1','newSubId']), _) + objectUnderTest.getSubscriptionDetailsAsJson('/x/y', ['sub-1','newSubId']), _,ContentType.JSON) } def 'Add new cm notification subscription for #datastoreType'() { given: 'a valid cm subscription path query' def cmSubscriptionCpsPathQuery = CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_WITH_DATASTORE_CMHANDLE_AND_XPATH.formatted(datastoreName, 'ch-1', '/x/y') - def cmHandleForSubscriptionPathQuery = CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_WITH_DATASTORE_AND_CMHANDLE.formatted(datastoreName, 'ch-1') + def cmHandleForSubscriptionPathQuery = CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_FILTERS_WITH_DATASTORE_AND_CMHANDLE.formatted(datastoreName, 'ch-1') and: 'a parent node xpath for the cm subscription path above' def parentNodeXpath = '/datastores/datastore[@name=\'%s\']/cm-handles' and: 'a datanode does not exist for cm subscription path query' mockCpsQueryService.queryDataNodes('NCMP-Admin', 'cm-data-subscriptions', - cmSubscriptionCpsPathQuery, + cmSubscriptionCpsPathQuery, FetchDescendantsOption.OMIT_DESCENDANTS) >> [] and: 'a datanode does not exist for the given cm handle subscription path query' mockCpsQueryService.queryDataNodes('NCMP-Admin', 'cm-data-subscriptions', @@ -124,7 +124,7 @@ class CmNotificationSubscriptionPersistenceServiceImplSpec extends Specification def 'Add new cm notification subscription when xpath does not exist for existing subscription cm handle'() { given: 'a valid cm subscription path query' def cmSubscriptionCpsPathQuery = CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_WITH_DATASTORE_CMHANDLE_AND_XPATH.formatted(datastoreName, 'ch-1', '/x/y') - def cmHandleForSubscriptionPathQuery = CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_WITH_DATASTORE_AND_CMHANDLE.formatted(datastoreName, 'ch-1') + def cmHandleForSubscriptionPathQuery = CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_FILTERS_WITH_DATASTORE_AND_CMHANDLE.formatted(datastoreName, 'ch-1') and: 'a parent node xpath for given cm handle for subscription path above' def parentNodeXpath = '/datastores/datastore[@name=\'%s\']/cm-handles/cm-handle[@id=\'%s\']/filters' and: 'a datanode does not exist for cm subscription path query' @@ -157,19 +157,36 @@ class CmNotificationSubscriptionPersistenceServiceImplSpec extends Specification then: 'the list of subscribers is updated' 1 * mockCpsDataService.updateNodeLeaves('NCMP-Admin', 'cm-data-subscriptions', '/datastores/datastore[@name=\'ncmp-datastore:passthrough-running\']/cm-handles/cm-handle[@id=\'ch-1\']/filters', - objectUnderTest.getSubscriptionDetailsAsJson('/x/y', ['sub-2']), _) + objectUnderTest.getSubscriptionDetailsAsJson('/x/y', ['sub-2']), _, ContentType.JSON) } - def 'Removing last ongoing subscription for datastore, cmhandle and xpath'(){ + def 'Removing last ongoing subscription for datastore and cmhandle and xpath'(){ given: 'a subscription exists when queried but has only 1 subscriber' - def cpsPathQuery = CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_WITH_DATASTORE_CMHANDLE_AND_XPATH.formatted('ncmp-datastore:passthrough-running', 'ch-1', '/x/y') - mockCpsQueryService.queryDataNodes('NCMP-Admin', 'cm-data-subscriptions', - cpsPathQuery, FetchDescendantsOption.OMIT_DESCENDANTS) >> [new DataNode(xpath: cpsPathQuery, leaves: ['xpath': '/x/y','subscriptionIds': ['sub-1']])] + mockCpsQueryService.queryDataNodes( + 'NCMP-Admin', + 'cm-data-subscriptions', + CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_WITH_DATASTORE_CMHANDLE_AND_XPATH.formatted('ncmp-datastore:passthrough-running', 'ch-1', '/x/y'), + FetchDescendantsOption.OMIT_DESCENDANTS) >> [new DataNode(leaves: ['xpath': '/x/y','subscriptionIds': ['sub-1']])] + and: 'the #scenario' + mockCpsQueryService.queryDataNodes( + 'NCMP-Admin', + 'cm-data-subscriptions', + CPS_PATH_QUERY_FOR_CM_SUBSCRIPTION_FILTERS_WITH_DATASTORE_AND_CMHANDLE.formatted('ncmp-datastore:passthrough-running', 'ch-1'), + FetchDescendantsOption.DIRECT_CHILDREN_ONLY) >> [new DataNode(childDataNodes: listOfChildNodes)] when: 'that last ongoing subscription is removed' objectUnderTest.removeCmNotificationSubscription(DatastoreType.PASSTHROUGH_RUNNING, 'ch-1', '/x/y', 'sub-1') then: 'the subscription with empty subscriber list is removed' 1 * mockCpsDataService.deleteDataNode('NCMP-Admin', 'cm-data-subscriptions', '/datastores/datastore[@name=\'ncmp-datastore:passthrough-running\']/cm-handles/cm-handle[@id=\'ch-1\']/filters/filter[@xpath=\'/x/y\']', _) + and: 'method call to delete the cm handle is called the correct number of times' + numberOfCallsToDeleteCmHandle * mockCpsDataService.deleteDataNode('NCMP-Admin', 'cm-data-subscriptions', + '/datastores/datastore[@name=\'ncmp-datastore:passthrough-running\']/cm-handles/cm-handle[@id=\'ch-1\']', + _) + where: + scenario | listOfChildNodes || numberOfCallsToDeleteCmHandle + 'cm handle in same datastore is used for other subscriptions' | [new DataNode()] || 0 + 'cm handle in same datastore is NOT used for other subscriptions' | [] || 1 } + }
\ No newline at end of file diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/inventory/InventoryPersistenceImplSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/inventory/InventoryPersistenceImplSpec.groovy index 9907e9ab2..66fd7d88e 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/inventory/InventoryPersistenceImplSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/inventory/InventoryPersistenceImplSpec.groovy @@ -37,7 +37,6 @@ import org.onap.cps.api.CpsModuleService import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle import org.onap.cps.spi.CascadeDeleteAllowed import org.onap.cps.spi.FetchDescendantsOption -import org.onap.cps.ncmp.api.impl.exception.NoAlternateIdParentFoundException import org.onap.cps.spi.exceptions.DataNodeNotFoundException import org.onap.cps.spi.model.DataNode import org.onap.cps.spi.model.ModuleDefinition @@ -303,41 +302,6 @@ class InventoryPersistenceImplSpec extends Specification { assert objectUnderTest.getCmHandleDataNodeByAlternateId('alternate id') == new DataNode() } - def 'Find cm handle parent data node using alternate ids'() { - given: 'cm handle in the registry with alternateId /a/b' - def matchingCpsPath = "/dmi-registry/cm-handles[@alternate-id='/a/b']" - mockCmHandleQueries.queryNcmpRegistryByCpsPath(matchingCpsPath, OMIT_DESCENDANTS) >> [new DataNode()] - and: 'no other cm handle' - mockCmHandleQueries.queryNcmpRegistryByCpsPath(*_) >> [] - expect: 'querying for alternate id a matching result found' - assert objectUnderTest.getCmHandleDataNodeByLongestMatchAlternateId(alternateId, '/') != null - where: 'the following parameters are used' - scenario | alternateId - 'exact match' | '/a/b' - 'exact match with trailing separator' | '/a/b/' - 'child match' | '/a/b/c' - } - - def 'Find cm handle parent data node using alternate ids mismatches'() { - given: 'cm handle in the registry with alternateId' - def matchingCpsPath = "/dmi-registry/cm-handles[@alternate-id='${cpsPath}]" - mockCmHandleQueries.queryNcmpRegistryByCpsPath(matchingCpsPath, OMIT_DESCENDANTS) >> [new DataNode()] - and: 'no other cm handle' - mockCmHandleQueries.queryNcmpRegistryByCpsPath(*_) >> [] - when: 'attempt to find alternateId' - objectUnderTest.getCmHandleDataNodeByLongestMatchAlternateId(alternateId, '/') - then: 'no alternate id found exception thrown' - def thrown = thrown(NoAlternateIdParentFoundException) - and: 'the exception has the relevant details from the error response' - assert thrown.message == 'No matching (parent) cm handle found using alternate ids' - assert thrown.details == 'cannot find a datanode with alternate id ' + alternateId - where: 'the following parameters are used' - scenario | alternateId | cpsPath - 'no match for parent only' | '/a' | '/a/b' - 'no match at all' | '/x/y/z' | '/a/b' - 'no match with trailing separator' | '/c/d/' | '/c/d' - } - def 'Attempt to get non existing cm handle data node by alternate id'() { given: 'query service is invoked and returns empty collection of data nodes' mockCmHandleQueries.queryNcmpRegistryByCpsPath(*_) >> [] diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DatastoreTypeSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DatastoreTypeSpec.groovy new file mode 100644 index 000000000..7e364c97c --- /dev/null +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DatastoreTypeSpec.groovy @@ -0,0 +1,46 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps.ncmp.api.impl.operations + +import org.onap.cps.ncmp.api.impl.exception.InvalidDatastoreException +import spock.lang.Specification + +class DatastoreTypeSpec extends Specification { + + def 'Converting string to enum.'() { + expect: 'converting string to enum results in the correct enum value' + DatastoreType.fromDatastoreName(datastoreName) == expectedEnum + where: 'the following datastore names are used' + datastoreName || expectedEnum + 'ncmp-datastore:operational' || DatastoreType.OPERATIONAL + 'ncmp-datastore:passthrough-running' || DatastoreType.PASSTHROUGH_RUNNING + 'ncmp-datastore:passthrough-operational' || DatastoreType.PASSTHROUGH_OPERATIONAL + } + + def 'Converting unknown name string to enum.'() { + when: 'attempt converting unknown datastore name' + DatastoreType.fromDatastoreName('unknown') + then: 'an invalid datastore exception is thrown' + def thrown = thrown(InvalidDatastoreException) + assert thrown.message.contains('unknown is an invalid datastore') + } + +} diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiDataOperationsSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiDataOperationsSpec.groovy index eb6c7a0f4..b286e9fb1 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiDataOperationsSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiDataOperationsSpec.groovy @@ -21,16 +21,25 @@ package org.onap.cps.ncmp.api.impl.operations +import static org.onap.cps.ncmp.api.impl.events.mapper.CloudEventMapper.toTargetEvent +import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.PASSTHROUGH_OPERATIONAL +import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.PASSTHROUGH_RUNNING +import static org.onap.cps.ncmp.api.impl.operations.OperationType.CREATE +import static org.onap.cps.ncmp.api.impl.operations.OperationType.READ +import static org.onap.cps.ncmp.api.impl.operations.OperationType.UPDATE +import static org.onap.cps.ncmp.api.impl.operations.RequiredDmiService.DATA +import static org.onap.cps.ncmp.api.NcmpResponseStatus.UNKNOWN_ERROR + import com.fasterxml.jackson.databind.ObjectMapper import org.onap.cps.events.EventsPublisher -import org.onap.cps.ncmp.api.impl.config.NcmpConfiguration -import org.onap.cps.ncmp.api.impl.exception.HttpClientRequestException -import org.onap.cps.ncmp.api.impl.utils.DmiServiceUrlBuilder +import org.onap.cps.ncmp.api.impl.config.DmiProperties +import org.onap.cps.ncmp.api.impl.exception.DmiClientRequestException import org.onap.cps.ncmp.api.impl.utils.context.CpsApplicationContext import org.onap.cps.ncmp.api.models.DataOperationRequest import org.onap.cps.ncmp.api.models.CmResourceAddress import org.onap.cps.ncmp.events.async1_0_0.DataOperationEvent import org.onap.cps.ncmp.utils.TestUtils +import org.onap.cps.ncmp.api.impl.inventory.CmHandleState import org.onap.cps.utils.JsonObjectMapper import org.spockframework.spring.SpringBean import org.springframework.beans.factory.annotation.Autowired @@ -39,28 +48,17 @@ import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.test.context.ContextConfiguration import spock.lang.Shared - -import java.util.concurrent.TimeoutException - -import static org.onap.cps.ncmp.api.NcmpResponseStatus.DMI_SERVICE_NOT_RESPONDING -import static org.onap.cps.ncmp.api.NcmpResponseStatus.UNABLE_TO_READ_RESOURCE_DATA -import static org.onap.cps.ncmp.api.impl.events.mapper.CloudEventMapper.toTargetEvent -import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.PASSTHROUGH_OPERATIONAL -import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.PASSTHROUGH_RUNNING -import static org.onap.cps.ncmp.api.impl.operations.OperationType.CREATE -import static org.onap.cps.ncmp.api.impl.operations.OperationType.READ -import static org.onap.cps.ncmp.api.impl.operations.OperationType.UPDATE +import reactor.core.publisher.Mono @SpringBootTest -@ContextConfiguration(classes = [EventsPublisher, CpsApplicationContext, NcmpConfiguration.DmiProperties, DmiDataOperations]) +@ContextConfiguration(classes = [EventsPublisher, CpsApplicationContext, DmiProperties, DmiDataOperations]) class DmiDataOperationsSpec extends DmiOperationsBaseSpec { - @SpringBean - DmiServiceUrlBuilder dmiServiceUrlBuilder = Mock() def dmiServiceBaseUrl = "${dmiServiceName}/dmi/v1/ch/${cmHandleId}/data/ds/ncmp-datastore:" def NO_TOPIC = null def NO_REQUEST_ID = null def NO_AUTH_HEADER = null + @Shared def OPTIONS_PARAM = '(a=1,b=2)' @@ -77,23 +75,24 @@ class DmiDataOperationsSpec extends DmiOperationsBaseSpec { given: 'a cm handle for #cmHandleId' mockYangModelCmHandleRetrieval(dmiProperties) and: 'a positive response from DMI service when it is called with the expected parameters' - def responseFromDmi = new ResponseEntity<Object>(HttpStatus.OK) - def expectedUrl = dmiServiceBaseUrl + "${expectedDatastoreInUrl}?resourceIdentifier=${resourceIdentifier}${expectedOptionsInUrl}" - mockDmiRestClient.postOperationWithJsonData(expectedUrl, expectedJson, READ, NO_AUTH_HEADER) >> responseFromDmi - dmiServiceUrlBuilder.getDmiDatastoreUrl(_, _) >> expectedUrl + def responseFromDmi = Mono.just(new ResponseEntity<Object>('{some-key:some-value}', HttpStatus.OK)) + def expectedUrl = "${dmiServiceBaseUrl}${expectedDatastoreInUrl}?resourceIdentifier=${resourceIdentifier}${expectedOptionsInUrl}" + def expectedJson = '{"operation":"read","cmHandleProperties":' + expectedProperties + ',"moduleSetTag":""}' + mockDmiRestClient.postOperationWithJsonDataAsync(DATA, expectedUrl, expectedJson, READ, NO_AUTH_HEADER) >> responseFromDmi when: 'get resource data is invoked' def cmResourceAddress = new CmResourceAddress(dataStore.datastoreName, cmHandleId, resourceIdentifier) - def result = objectUnderTest.getResourceDataFromDmi(cmResourceAddress, options, NO_TOPIC, NO_REQUEST_ID, NO_AUTH_HEADER) + def result = objectUnderTest.getResourceDataFromDmi(cmResourceAddress, options, NO_TOPIC, NO_REQUEST_ID, NO_AUTH_HEADER).block() then: 'the result is the response from the DMI service' - assert result == responseFromDmi + assert result.body == '{some-key:some-value}' + assert result.statusCode.'2xxSuccessful' where: 'the following parameters are used' - scenario | dmiProperties | dataStore | options || expectedJson | expectedDatastoreInUrl | expectedOptionsInUrl - 'without properties' | [] | PASSTHROUGH_OPERATIONAL | OPTIONS_PARAM || '{"operation":"read","cmHandleProperties":{}}' | 'passthrough-operational' | '&options=(a=1,b=2)' - 'with properties' | [yangModelCmHandleProperty] | PASSTHROUGH_OPERATIONAL | OPTIONS_PARAM || '{"operation":"read","cmHandleProperties":{"prop1":"val1"}}' | 'passthrough-operational' | '&options=(a=1,b=2)' - 'null options' | [yangModelCmHandleProperty] | PASSTHROUGH_OPERATIONAL | null || '{"operation":"read","cmHandleProperties":{"prop1":"val1"}}' | 'passthrough-operational' | '' - 'empty options' | [yangModelCmHandleProperty] | PASSTHROUGH_OPERATIONAL | '' || '{"operation":"read","cmHandleProperties":{"prop1":"val1"}}' | 'passthrough-operational' | '' - 'datastore running without properties' | [] | PASSTHROUGH_RUNNING | OPTIONS_PARAM || '{"operation":"read","cmHandleProperties":{}}' | 'passthrough-running' | '&options=(a=1,b=2)' - 'datastore running with properties' | [yangModelCmHandleProperty] | PASSTHROUGH_RUNNING | OPTIONS_PARAM || '{"operation":"read","cmHandleProperties":{"prop1":"val1"}}' | 'passthrough-running' | '&options=(a=1,b=2)' + scenario | dmiProperties | dataStore | options || expectedProperties | expectedDatastoreInUrl | expectedOptionsInUrl + 'without properties' | [] | PASSTHROUGH_OPERATIONAL | OPTIONS_PARAM || '{}' | 'passthrough-operational' | '&options=(a%3D1,b%3D2)' + 'with properties' | [yangModelCmHandleProperty] | PASSTHROUGH_OPERATIONAL | OPTIONS_PARAM || '{"prop1":"val1"}' | 'passthrough-operational' | '&options=(a%3D1,b%3D2)' + 'null options' | [yangModelCmHandleProperty] | PASSTHROUGH_OPERATIONAL | null || '{"prop1":"val1"}' | 'passthrough-operational' | '' + 'empty options' | [yangModelCmHandleProperty] | PASSTHROUGH_OPERATIONAL | '' || '{"prop1":"val1"}' | 'passthrough-operational' | '' + 'datastore running without properties' | [] | PASSTHROUGH_RUNNING | OPTIONS_PARAM || '{}' | 'passthrough-running' | '&options=(a%3D1,b%3D2)' + 'datastore running with properties' | [yangModelCmHandleProperty] | PASSTHROUGH_RUNNING | OPTIONS_PARAM || '{"prop1":"val1"}' | 'passthrough-running' | '&options=(a%3D1,b%3D2)' } def 'Execute (async) data operation from DMI service.'() { @@ -103,47 +102,46 @@ class DmiDataOperationsSpec extends DmiOperationsBaseSpec { def dataOperationRequest = spiedJsonObjectMapper.convertJsonString(dataOperationBatchRequestJsonData, DataOperationRequest.class) dataOperationRequest.dataOperationDefinitions[0].cmHandleIds = [cmHandleId] and: 'a positive response from DMI service when it is called with valid request parameters' - def responseFromDmi = new ResponseEntity<Object>(HttpStatus.ACCEPTED) - def expectedDmiBatchResourceDataUrl = "ncmp/v1/data/topic=my-topic-name" - def expectedBatchRequestAsJson = '{"operations":[{"operation":"read","operationId":"operational-14","datastore":"ncmp-datastore:passthrough-operational","options":"some option","resourceIdentifier":"some resource identifier","cmHandles":[{"id":"some-cm-handle","cmHandleProperties":{"prop1":"val1"}}]}]}' - mockDmiRestClient.postOperationWithJsonData(expectedDmiBatchResourceDataUrl, _, READ.operationName, NO_AUTH_HEADER) >> responseFromDmi - dmiServiceUrlBuilder.getDataOperationRequestUrl(_, _) >> expectedDmiBatchResourceDataUrl - when: 'get resource data for group of cm handles are invoked' + def responseFromDmi = Mono.just(new ResponseEntity<Object>(HttpStatus.ACCEPTED)) + def expectedDmiBatchResourceDataUrl = "someServiceName/dmi/v1/data?requestId=requestId&topic=my-topic-name" + def expectedBatchRequestAsJson = '{"operations":[{"operation":"read","operationId":"operational-14","datastore":"ncmp-datastore:passthrough-operational","options":"some option","resourceIdentifier":"some resource identifier","cmHandles":[{"id":"some-cm-handle","moduleSetTag":"","cmHandleProperties":{"prop1":"val1"}}]}]}' + mockDmiRestClient.postOperationWithJsonDataAsync(DATA, expectedDmiBatchResourceDataUrl, _, READ, NO_AUTH_HEADER) >> responseFromDmi + when: 'get resource data for group of cm handles is invoked' objectUnderTest.requestResourceDataFromDmi('my-topic-name', dataOperationRequest, 'requestId', NO_AUTH_HEADER) - then: 'the post operation was called and ncmp generated dmi request body json args' - 1 * mockDmiRestClient.postOperationWithJsonData(expectedDmiBatchResourceDataUrl, expectedBatchRequestAsJson, READ, NO_AUTH_HEADER) + then: 'the post operation was called with the expected URL and JSON request body' + 1 * mockDmiRestClient.postOperationWithJsonDataAsync(DATA, expectedDmiBatchResourceDataUrl, expectedBatchRequestAsJson, READ, NO_AUTH_HEADER) } - def 'Execute (async) data operation from DMI service for #scenario.'() { - given: 'data operation request body and dmi resource url' - def dmiDataOperation = DmiDataOperation.builder().operationId('some-operation-id').build() - dmiDataOperation.getCmHandles().add(CmHandle.builder().id('some-cm-handle-id').build()) - def dmiDataOperationResourceDataUrl = "http://dmi-service-name:dmi-port/dmi/v1/data?topic=my-topic-name&requestId=some-request-id" + def 'Execute (async) data operation from DMI service with Exception.'() { + given: 'collection of yang model cm Handles and data operation request' + mockYangModelCmHandleCollectionRetrieval([yangModelCmHandleProperty]) + def dataOperationBatchRequestJsonData = TestUtils.getResourceFileContent('dataOperationRequest.json') + def dataOperationRequest = spiedJsonObjectMapper.convertJsonString(dataOperationBatchRequestJsonData, DataOperationRequest.class) + dataOperationRequest.dataOperationDefinitions[0].cmHandleIds = [cmHandleId] + and: 'the published cloud event will be captured' def actualDataOperationCloudEvent = null - when: 'exception occurs after sending request to dmi service' - objectUnderTest.handleTaskCompletionException(new Throwable(exception), dmiDataOperationResourceDataUrl, List.of(dmiDataOperation)) - then: 'a cloud event is published' - eventsPublisher.publishCloudEvent('my-topic-name', 'some-request-id', _) >> { args -> actualDataOperationCloudEvent = args[2] } - and: 'the event contains the expected error details' + eventsPublisher.publishCloudEvent('my-topic-name', 'my-request-id', _) >> { args -> actualDataOperationCloudEvent = args[2] } + and: 'a DMI client request exception is thrown when DMI service is called' + mockDmiRestClient.postOperationWithJsonDataAsync(*_) >> { Mono.error(new DmiClientRequestException(123, '', '', UNKNOWN_ERROR)) } + when: 'attempt to get resource data for group of cm handles is invoked' + objectUnderTest.requestResourceDataFromDmi('my-topic-name', dataOperationRequest, 'my-request-id', NO_AUTH_HEADER) + then: 'the event contains the expected error details' def eventDataValue = extractDataValue(actualDataOperationCloudEvent) - assert eventDataValue.operationId == dmiDataOperation.operationId - assert eventDataValue.ids == dmiDataOperation.cmHandles.id - assert eventDataValue.statusCode == responseCode.code - assert eventDataValue.statusMessage == responseCode.message - where: 'the following exceptions are occurred' - scenario | exception || responseCode - 'http client request exception' | new HttpClientRequestException('error-message', 'error-details', HttpStatus.SERVICE_UNAVAILABLE.value()) || UNABLE_TO_READ_RESOURCE_DATA - 'timeout exception' | new TimeoutException() || DMI_SERVICE_NOT_RESPONDING + assert eventDataValue.statusCode == '108' + assert eventDataValue.statusMessage == UNKNOWN_ERROR.message + and: 'the event contains the correct operation details' + assert eventDataValue.operationId == dataOperationRequest.dataOperationDefinitions[0].operationId + assert eventDataValue.ids == dataOperationRequest.dataOperationDefinitions[0].cmHandleIds } def 'call get all resource data.'() { - given: 'the system returns a cm handle with a sample property' - mockYangModelCmHandleRetrieval([yangModelCmHandleProperty]) + given: 'the system returns a cm handle with a sample property and sample module set tag' + mockYangModelCmHandleRetrieval([yangModelCmHandleProperty], 'my-module-set-tag') and: 'a positive response from DMI service when it is called with the expected parameters' def responseFromDmi = new ResponseEntity<Object>(HttpStatus.OK) def expectedUrl = dmiServiceBaseUrl + "passthrough-operational?resourceIdentifier=/" - mockDmiRestClient.postOperationWithJsonData(expectedUrl, '{"operation":"read","cmHandleProperties":{"prop1":"val1"}}', READ, null) >> responseFromDmi - dmiServiceUrlBuilder.getDmiDatastoreUrl(_, _) >> expectedUrl + def expectedJson = '{"operation":"read","cmHandleProperties":{"prop1":"val1"},"moduleSetTag":"my-module-set-tag"}' + mockDmiRestClient.postOperationWithJsonData(DATA, expectedUrl, expectedJson, READ, null) >> responseFromDmi when: 'get resource data is invoked' def result = objectUnderTest.getResourceDataFromDmi( PASSTHROUGH_OPERATIONAL.datastoreName, cmHandleId, NO_REQUEST_ID) then: 'the result is the response from the DMI service' @@ -154,11 +152,10 @@ class DmiDataOperationsSpec extends DmiOperationsBaseSpec { given: 'a cm handle for #cmHandleId' mockYangModelCmHandleRetrieval([yangModelCmHandleProperty]) and: 'a positive response from DMI service when it is called with the expected parameters' - def expectedUrl = dmiServiceBaseUrl + "passthrough-running?resourceIdentifier=${resourceIdentifier}" - def expectedJson = '{"operation":"' + expectedOperationInUrl + '","dataType":"some data type","data":"requestData","cmHandleProperties":{"prop1":"val1"}}' + def expectedUrl = "${dmiServiceBaseUrl}passthrough-running?resourceIdentifier=${resourceIdentifier}" + def expectedJson = '{"operation":"' + expectedOperationInUrl + '","dataType":"some data type","data":"requestData","cmHandleProperties":{"prop1":"val1"},"moduleSetTag":""}' def responseFromDmi = new ResponseEntity<Object>(HttpStatus.OK) - dmiServiceUrlBuilder.getDmiDatastoreUrl(_, _) >> expectedUrl - mockDmiRestClient.postOperationWithJsonData(expectedUrl, expectedJson, operation, NO_AUTH_HEADER) >> responseFromDmi + mockDmiRestClient.postOperationWithJsonData(DATA, expectedUrl, expectedJson, operation, NO_AUTH_HEADER) >> responseFromDmi when: 'write resource method is invoked' def result = objectUnderTest.writeResourceDataPassThroughRunningFromDmi(cmHandleId, 'parent/child', operation, 'requestData', 'some data type', NO_AUTH_HEADER) then: 'the result is the response from the DMI service' @@ -169,7 +166,29 @@ class DmiDataOperationsSpec extends DmiOperationsBaseSpec { UPDATE || 'update' } + def 'State Ready validation'() { + given: ' a yang model cm handle' + populateYangModelCmHandle([] ,'') + when: 'Validating State of #cmHandleState' + def caughtException = null + try { + objectUnderTest.validateIfCmHandleStateReady(yangModelCmHandle, cmHandleState) + } catch (Exception e) { + caughtException = e + } + then: 'only when not ready a exception is thrown' + if (expecteException) { + assert caughtException.details.contains('not in READY state') + } else { + assert caughtException == null + } + where: ' the following states are used' + cmHandleState || expecteException + CmHandleState.READY || false + CmHandleState.ADVISED || true + } + def extractDataValue(actualDataOperationCloudEvent) { - return toTargetEvent(actualDataOperationCloudEvent, DataOperationEvent.class).data.responses[0] + return toTargetEvent(actualDataOperationCloudEvent, DataOperationEvent).data.responses[0] } } diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiModelOperationsSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiModelOperationsSpec.groovy index 9aab46747..db7f26f5f 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiModelOperationsSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiModelOperationsSpec.groovy @@ -21,9 +21,12 @@ package org.onap.cps.ncmp.api.impl.operations +import static org.onap.cps.ncmp.api.impl.operations.OperationType.READ +import static org.onap.cps.ncmp.api.impl.operations.RequiredDmiService.MODEL + import com.fasterxml.jackson.core.JsonProcessingException import com.fasterxml.jackson.databind.ObjectMapper -import org.onap.cps.ncmp.api.impl.config.NcmpConfiguration +import org.onap.cps.ncmp.api.impl.config.DmiProperties import org.onap.cps.spi.model.ModuleReference import org.onap.cps.utils.JsonObjectMapper import org.spockframework.spring.SpringBean @@ -34,10 +37,8 @@ import org.springframework.http.ResponseEntity import org.springframework.test.context.ContextConfiguration import spock.lang.Shared -import static org.onap.cps.ncmp.api.impl.operations.OperationType.READ - @SpringBootTest -@ContextConfiguration(classes = [NcmpConfiguration.DmiProperties, DmiModelOperations]) +@ContextConfiguration(classes = [DmiProperties, DmiModelOperations]) class DmiModelOperationsSpec extends DmiOperationsBaseSpec { @Shared @@ -58,8 +59,7 @@ class DmiModelOperationsSpec extends DmiOperationsBaseSpec { def moduleReferencesAsLisOfMaps = [[moduleName: 'mod1', revision: 'A'], [moduleName: 'mod2', revision: 'X']] def expectedUrl = "${dmiServiceName}/dmi/v1/ch/${cmHandleId}/modules" def responseFromDmi = new ResponseEntity([schemas: moduleReferencesAsLisOfMaps], HttpStatus.OK) - mockDmiRestClient.postOperationWithJsonData(expectedUrl, '{"cmHandleProperties":{},"moduleSetTag":""}', READ, NO_AUTH_HEADER) - >> responseFromDmi + mockDmiRestClient.postOperationWithJsonData(MODEL, expectedUrl, '{"cmHandleProperties":{},"moduleSetTag":""}', READ, NO_AUTH_HEADER) >> responseFromDmi when: 'get module references is called' def result = objectUnderTest.getModuleReferences(yangModelCmHandle) then: 'the result consists of expected module references' @@ -90,7 +90,7 @@ class DmiModelOperationsSpec extends DmiOperationsBaseSpec { mockYangModelCmHandleRetrieval(dmiProperties) and: 'a positive response from DMI service when it is called with tha expected parameters' def responseFromDmi = new ResponseEntity<String>(HttpStatus.OK) - mockDmiRestClient.postOperationWithJsonData("${dmiServiceName}/dmi/v1/ch/${cmHandleId}/modules", + mockDmiRestClient.postOperationWithJsonData(MODEL, "${dmiServiceName}/dmi/v1/ch/${cmHandleId}/modules", '{"cmHandleProperties":' + expectedAdditionalPropertiesInRequest + ',"moduleSetTag":""}', READ, NO_AUTH_HEADER) >> responseFromDmi when: 'a get module references is called' def result = objectUnderTest.getModuleReferences(yangModelCmHandle) @@ -109,7 +109,7 @@ class DmiModelOperationsSpec extends DmiOperationsBaseSpec { def responseFromDmi = new ResponseEntity([[moduleName: 'mod1', revision: 'A', yangSource: 'some yang source'], [moduleName: 'mod2', revision: 'C', yangSource: 'other yang source']], HttpStatus.OK) def expectedModuleReferencesInRequest = '{"name":"mod1","revision":"A"},{"name":"mod2","revision":"X"}' - mockDmiRestClient.postOperationWithJsonData("${dmiServiceName}/dmi/v1/ch/${cmHandleId}/moduleResources", + mockDmiRestClient.postOperationWithJsonData(MODEL, "${dmiServiceName}/dmi/v1/ch/${cmHandleId}/moduleResources", '{"data":{"modules":[' + expectedModuleReferencesInRequest + ']},"cmHandleProperties":{}}', READ, NO_AUTH_HEADER) >> responseFromDmi when: 'get new yang resources from DMI service' def result = objectUnderTest.getNewYangResourcesFromDmi(yangModelCmHandle, newModuleReferences) @@ -141,7 +141,7 @@ class DmiModelOperationsSpec extends DmiOperationsBaseSpec { mockYangModelCmHandleRetrieval(dmiProperties) and: 'a positive response from DMI service when it is called with the expected moduleSetTag, modules and properties' def responseFromDmi = new ResponseEntity<>([[moduleName: 'mod1', revision: 'A', yangSource: 'some yang source']], HttpStatus.OK) - mockDmiRestClient.postOperationWithJsonData("${dmiServiceName}/dmi/v1/ch/${cmHandleId}/moduleResources", + mockDmiRestClient.postOperationWithJsonData(MODEL, "${dmiServiceName}/dmi/v1/ch/${cmHandleId}/moduleResources", '{"data":{"modules":[{"name":"mod1","revision":"A"},{"name":"mod2","revision":"X"}]},"cmHandleProperties":' + expectedAdditionalPropertiesInRequest + '}', READ, NO_AUTH_HEADER) >> responseFromDmi when: 'get new yang resources from DMI service' @@ -159,9 +159,9 @@ class DmiModelOperationsSpec extends DmiOperationsBaseSpec { mockYangModelCmHandleRetrieval([], moduleSetTag) and: 'a positive response from DMI service when it is called with the expected moduleSetTag' def responseFromDmi = new ResponseEntity<>([[moduleName: 'mod1', revision: 'A', yangSource: 'some yang source']], HttpStatus.OK) - mockDmiRestClient.postOperationWithJsonData("${dmiServiceName}/dmi/v1/ch/${cmHandleId}/moduleResources", - '{' + expectedModuleSetTagInRequest + '"data":{"modules":[{"name":"mod1","revision":"A"},{"name":"mod2","revision":"X"}]},"cmHandleProperties":{}}', - READ, NO_AUTH_HEADER) >> responseFromDmi + mockDmiRestClient.postOperationWithJsonData(MODEL, "${dmiServiceName}/dmi/v1/ch/${cmHandleId}/moduleResources", + '{' + expectedModuleSetTagInRequest + '"data":{"modules":[{"name":"mod1","revision":"A"},{"name":"mod2","revision":"X"}]},"cmHandleProperties":{}}', + READ, NO_AUTH_HEADER) >> responseFromDmi when: 'get new yang resources from DMI service' def result = objectUnderTest.getNewYangResourcesFromDmi(yangModelCmHandle, newModuleReferences) then: 'the result is the response from DMI service' diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiOperationsBaseSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiOperationsBaseSpec.groovy index 72a0f2f11..136ff7832 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiOperationsBaseSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiOperationsBaseSpec.groovy @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2021-2023 Nordix Foundation + * Copyright (C) 2021-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. @@ -22,13 +22,10 @@ package org.onap.cps.ncmp.api.impl.operations import com.fasterxml.jackson.databind.ObjectMapper import org.onap.cps.ncmp.api.impl.client.DmiRestClient -import org.onap.cps.ncmp.api.impl.config.NcmpConfiguration import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle -import org.onap.cps.ncmp.api.impl.utils.DmiServiceUrlBuilder import org.onap.cps.ncmp.api.impl.inventory.CmHandleState import org.onap.cps.ncmp.api.impl.inventory.CompositeState import org.onap.cps.ncmp.api.impl.inventory.InventoryPersistence -import org.onap.cps.spi.utils.CpsValidator import org.spockframework.spring.SpringBean import spock.lang.Shared import spock.lang.Specification @@ -44,16 +41,11 @@ abstract class DmiOperationsBaseSpec extends Specification { @SpringBean InventoryPersistence mockInventoryPersistence = Mock() - def mockCpsValidator = Mock(CpsValidator) - @SpringBean ObjectMapper spyObjectMapper = Spy() - @SpringBean - DmiServiceUrlBuilder dmiServiceUrlBuilder = new DmiServiceUrlBuilder(new NcmpConfiguration.DmiProperties(), mockCpsValidator) - def yangModelCmHandle = new YangModelCmHandle() - def static dmiServiceName = 'some service name' + def static dmiServiceName = 'someServiceName' def static cmHandleId = 'some-cm-handle' def static resourceIdentifier = 'parent/child' @@ -68,7 +60,7 @@ abstract class DmiOperationsBaseSpec extends Specification { } def mockYangModelCmHandleCollectionRetrieval(dmiProperties) { - populateYangModelCmHandle(dmiProperties, "") + populateYangModelCmHandle(dmiProperties, '') mockInventoryPersistence.getYangModelCmHandles(_) >> [yangModelCmHandle] } diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/OperationTypeSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/OperationTypeSpec.groovy new file mode 100644 index 000000000..d31b8d4fd --- /dev/null +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/OperationTypeSpec.groovy @@ -0,0 +1,48 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps.ncmp.api.impl.operations + +import org.onap.cps.ncmp.api.impl.exception.InvalidOperationException +import spock.lang.Specification + +class OperationTypeSpec extends Specification { + + def 'Converting string to enum.'() { + expect: 'converting string to enum results in the correct enum value' + OperationType.fromOperationName(operationName) == expectedEnum + where: 'the following datastore names are used' + operationName || expectedEnum + 'read' || OperationType.READ + 'create' || OperationType.CREATE + 'update' || OperationType.UPDATE + 'patch' || OperationType.PATCH + 'delete' || OperationType.DELETE + } + + def 'Converting unknown name string to enum.'() { + when: 'attempt converting unknown datastore name' + OperationType.fromOperationName('unknown') + then: 'an invalid operation exception is thrown' + def thrown = thrown(InvalidOperationException) + assert thrown.message.contains('unknown is an invalid operation') + } + +} diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/DmiServiceUrlBuilderSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/DmiServiceUrlBuilderSpec.groovy index fbf2c3d78..69d08e3de 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/DmiServiceUrlBuilderSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/DmiServiceUrlBuilderSpec.groovy @@ -20,76 +20,67 @@ package org.onap.cps.ncmp.api.impl.utils -import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.PASSTHROUGH_RUNNING - -import org.onap.cps.ncmp.api.impl.operations.RequiredDmiService -import org.onap.cps.spi.utils.CpsValidator -import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle -import org.onap.cps.ncmp.api.impl.config.NcmpConfiguration -import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle import spock.lang.Specification class DmiServiceUrlBuilderSpec extends Specification { - static YangModelCmHandle yangModelCmHandle = YangModelCmHandle.toYangModelCmHandle('dmiServiceName', - 'dmiDataServiceName', 'dmiModuleServiceName', new NcmpServiceCmHandle(cmHandleId: 'some-cm-handle-id'),'my-module-set-tag', 'my-alternate-id', 'my-data-producer-identifier') - - NcmpConfiguration.DmiProperties dmiProperties = new NcmpConfiguration.DmiProperties() - - def mockCpsValidator = Mock(CpsValidator) + def objectUnderTest = new DmiServiceUrlBuilder() - def objectUnderTest = new DmiServiceUrlBuilder(dmiProperties, mockCpsValidator) - - def setup() { - dmiProperties.dmiBasePath = 'dmi' + def 'Build URI with (variable) path segments and parameters.'() { + given: 'the URI details are given to the builder' + objectUnderTest.pathSegment(segment1) + objectUnderTest.variablePathSegment('myVariableSegment','someValue') + objectUnderTest.pathSegment(segment2) + objectUnderTest.queryParameter('param1', paramValue1) + objectUnderTest.queryParameter('param2', paramValue2) + objectUnderTest.queryParameter('param3', null) + objectUnderTest.queryParameter('param4', '') + when: 'the URI (string) is build' + def result = objectUnderTest.build('myDmiServer', 'myBasePath') + then: 'the URI is correct (segments are in correct order) ' + assert result == expectedUri + where: 'following URI details are used' + segment1 | segment2 | paramValue1 | paramValue2 || expectedUri + 'segment1' | 'segment2' | '123' | 'abc' || 'myDmiServer/myBasePath/v1/segment1/someValue/segment2?param1=123¶m2=abc' + 'segment2' | 'segment1' | 'abc' | '123' || 'myDmiServer/myBasePath/v1/segment2/someValue/segment1?param1=abc¶m2=123' } - def 'Create the dmi service url with #scenario.'() { - given: 'uri variables' - def uriVars = objectUnderTest.populateUriVariables(PASSTHROUGH_RUNNING.datastoreName, yangModelCmHandle.resolveDmiServiceName(RequiredDmiService.DATA), 'cmHandle') - and: 'query params' - def uriQueries = objectUnderTest.populateQueryParams(resourceId, 'optionsParamInQuery', topic) - when: 'a dmi datastore service url is generated' - def dmiServiceUrl = objectUnderTest.getDmiDatastoreUrl(uriQueries, uriVars) - then: 'service url is generated as expected' - assert dmiServiceUrl == expectedDmiServiceUrl - where: 'the following parameters are used' - scenario | topic | resourceId || expectedDmiServiceUrl - 'With valid resourceId' | 'topicParamInQuery' | 'resourceId' || 'dmiServiceName/dmi/v1/ch/cmHandle/data/ds/ncmp-datastore:passthrough-running?resourceIdentifier=resourceId&options=optionsParamInQuery&topic=topicParamInQuery' - 'With Empty resourceId' | 'topicParamInQuery' | '' || 'dmiServiceName/dmi/v1/ch/cmHandle/data/ds/ncmp-datastore:passthrough-running?options=optionsParamInQuery&topic=topicParamInQuery' - 'With Empty dmi base path' | 'topicParamInQuery' | 'resourceId' || 'dmiServiceName/dmi/v1/ch/cmHandle/data/ds/ncmp-datastore:passthrough-running?resourceIdentifier=resourceId&options=optionsParamInQuery&topic=topicParamInQuery' - 'With Empty topicParamInQuery' | '' | 'resourceId' || 'dmiServiceName/dmi/v1/ch/cmHandle/data/ds/ncmp-datastore:passthrough-running?resourceIdentifier=resourceId&options=optionsParamInQuery' + def 'Build URI with special characters in path segments.'() { + given: 'the path segments are given to the builder' + objectUnderTest.pathSegment(segment) + objectUnderTest.variablePathSegment('myVariableSegment', variableSegmentValue) + when: 'the URI (string) is build' + def result = objectUnderTest.build('myDmiServer', 'myBasePath') + then: 'Only teh characters that cause issues in path segments issues are encoded' + assert result == expectedUri + where: 'following variable path segments are used' + segment | variableSegmentValue || expectedUri + 'some/special?characters=are\\encoded' | 'my/variable/segment' || 'myDmiServer/myBasePath/v1/some%2Fspecial%3Fcharacters=are%5Cencoded/my%2Fvariable%2Fsegment' + 'but=some&are:not-!' | 'my&variable:segment' || 'myDmiServer/myBasePath/v1/but=some&are:not-!/my&variable:segment' } - def 'Populate dmi data store url #scenario.'() { - given: 'uri variables are created' - dmiProperties.dmiBasePath = dmiBasePath - def uriVars = objectUnderTest.populateUriVariables(PASSTHROUGH_RUNNING.datastoreName, yangModelCmHandle.resolveDmiServiceName(RequiredDmiService.DATA), 'cmHandle') - and: 'null query params' - def uriQueries = objectUnderTest.populateQueryParams(null, null, null) - when: 'a dmi datastore service url is generated' - def dmiServiceUrl = objectUnderTest.getDmiDatastoreUrl(uriQueries, uriVars) - then: 'the created dmi service url matches the expected' - assert dmiServiceUrl == expectedDmiServiceUrl - where: 'the following parameters are used' - scenario | decription | dmiBasePath || expectedDmiServiceUrl - 'with base path / ' | 'Invalid base path as it starts with /' | '/dmi' || 'dmiServiceName//dmi/v1/ch/cmHandle/data/ds/ncmp-datastore:passthrough-running' - 'without base path / ' | 'Valid path as it does not starts with /' | 'dmi' || 'dmiServiceName/dmi/v1/ch/cmHandle/data/ds/ncmp-datastore:passthrough-running' + def 'Build URI with special characters in query parameters.'() { + given: 'the query parameter is given to the builder' + objectUnderTest.queryParameter(paramName, value) + when: 'the URI (string) is build' + def result = objectUnderTest.build('myDmiServer', 'myBasePath') + then: 'Only the characters (in the name and value) that cause issues in query parameters are encoded' + assert result == expectedUri + where: 'the following query parameters are used' + paramName | value || expectedUri + 'my¶m' | 'some?special&characters=are\\encoded' || 'myDmiServer/myBasePath/v1?my%26param=some?special%26characters%3Dare%5Cencoded' + 'my-param' | 'but/some:are-not-!' || 'myDmiServer/myBasePath/v1?my-param=but/some:are-not-!' } - def 'Bath request Url creation.'() { - given: 'the required path parameters' - def batchRequestUriVariables = [dmiServiceName: 'some-service', dmiBasePath: 'testBase', cmHandleId: '123'] - and: 'the relevant query parameters' - def batchRequestQueryParams = objectUnderTest.getDataOperationRequestQueryParams('some topic', 'some id') - when: 'a URL is created' - def result = objectUnderTest.getDataOperationRequestUrl(batchRequestQueryParams, batchRequestUriVariables) - then: 'it is formed correctly' - assert result.toString() == 'some-service/testBase/v1/data?topic=some topic&requestId=some id' + def 'Build URI with empty query parameters.'() { + when: 'the query parameter is given to the builder' + objectUnderTest.queryParameter('param', value) + and: 'the URI (string) is build' + def result = objectUnderTest.build('myDmiServer', 'myBasePath') + then: 'no parameter gets added' + assert result == 'myDmiServer/myBasePath/v1' + where: 'the following parameter values are used' + value << [ null, '', ' ' ] } - def 'Populate batch uri variables.'() { - expect: 'Populate batch uri variables returns a map with given service name and base path from setup' - assert objectUnderTest.populateDataOperationRequestUriVariables('some service') == [dmiServiceName: 'some service', dmiBasePath: 'dmi' ] - } } diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/EventDateTimeFormatterSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/EventDateTimeFormatterSpec.groovy new file mode 100644 index 000000000..c72eb9e4c --- /dev/null +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/EventDateTimeFormatterSpec.groovy @@ -0,0 +1,45 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps.ncmp.api.impl.utils + +import spock.lang.Specification +import java.time.Year + +class EventDateTimeFormatterSpec extends Specification { + + def 'Get ISO formatted date and time.' () { + expect: 'iso formatted date and time starts with current year' + assert EventDateTimeFormatter.getCurrentIsoFormattedDateTime().startsWith(String.valueOf(Year.now())) + } + + def 'Convert date time from string to OffsetDateTime type.'() { + when: 'date time as a string is converted to OffsetDateTime type' + def result = EventDateTimeFormatter.toIsoOffsetDateTime('2024-05-28T18:28:02.869+0100') + then: 'the result convert back back to a string is the same as the original timestamp (except the format of timezone offset)' + assert result.toString() == '2024-05-28T18:28:02.869+01:00' + } + + def 'Convert blank string.' () { + expect: 'converting a blank string result in null' + assert EventDateTimeFormatter.toIsoOffsetDateTime(' ') == null + } + +} diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/RestQueryParametersValidatorSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/RestQueryParametersValidatorSpec.groovy index dc471e64f..54befb446 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/RestQueryParametersValidatorSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/RestQueryParametersValidatorSpec.groovy @@ -27,7 +27,6 @@ import spock.lang.Specification class RestQueryParametersValidatorSpec extends Specification { - def 'CM Handle Query validation: empty query.'() { given: 'a cm handle query' def cmHandleQueryParameters = new CmHandleQueryServiceParameters() @@ -62,13 +61,13 @@ class RestQueryParametersValidatorSpec extends Specification { then: 'a data validation exception is thrown' def thrown = thrown(DataValidationException) and: 'the exception details contain the correct significant term ' - thrown.details.contains(expectedWordInDetails) + assert thrown.details.contains(expectedWordInDetails) where: scenario | conditionName | conditionParameters || expectedWordInDetails 'unknown condition name' | 'unknownCondition' | [['key': 'value']] || 'conditionName' 'no condition name' | '' | [['key': 'value']] || 'conditionName' + 'empty conditions' | 'validConditionName' | [] || 'conditionsParameters' 'empty properties' | 'validConditionName' | [[:]] || 'conditionsParameter' - 'empty conditions' | 'validConditionName' | [[:]] || 'conditionsParameter' 'too many properties' | 'validConditionName' | [[key1: 'value1', key2: 'value2']] || 'conditionsParameter' 'empty key' | 'validConditionName' | [['': 'wrong']] || 'conditionsParameter' } diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/context/CpsApplicationContextSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/context/CpsApplicationContextSpec.groovy index b7fa44925..ee117160c 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/context/CpsApplicationContextSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/context/CpsApplicationContextSpec.groovy @@ -1,3 +1,23 @@ +/* + * ============LICENSE_START======================================================== + * Copyright (c) 2023-2024 Nordix Foundation. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + package org.onap.cps.ncmp.api.impl.utils.context import com.fasterxml.jackson.databind.ObjectMapper diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/data/operation/ResourceDataOperationRequestUtilsSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/data/operation/ResourceDataOperationRequestUtilsSpec.groovy index 8df27bb62..653068592 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/data/operation/ResourceDataOperationRequestUtilsSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/data/operation/ResourceDataOperationRequestUtilsSpec.groovy @@ -20,16 +20,20 @@ package org.onap.cps.ncmp.api.impl.utils.data.operation +import static org.onap.cps.ncmp.api.impl.events.mapper.CloudEventMapper.toTargetEvent +import static org.onap.cps.ncmp.api.impl.inventory.CmHandleState.ADVISED +import static org.onap.cps.ncmp.api.impl.inventory.CmHandleState.READY + import com.fasterxml.jackson.databind.ObjectMapper import io.cloudevents.CloudEvent import io.cloudevents.kafka.CloudEventDeserializer import io.cloudevents.kafka.impl.KafkaHeaders import org.apache.kafka.clients.consumer.KafkaConsumer import org.onap.cps.events.EventsPublisher -import org.onap.cps.ncmp.api.NcmpResponseStatus +import org.onap.cps.ncmp.api.impl.operations.DmiDataOperation +import org.onap.cps.ncmp.api.impl.operations.OperationType import org.onap.cps.ncmp.api.impl.utils.context.CpsApplicationContext import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle -import org.onap.cps.ncmp.api.impl.inventory.CmHandleState import org.onap.cps.ncmp.api.impl.inventory.CompositeStateBuilder import org.onap.cps.ncmp.api.kafka.MessagingBaseSpec import org.onap.cps.ncmp.api.models.DataOperationRequest @@ -37,15 +41,11 @@ import org.onap.cps.ncmp.events.async1_0_0.DataOperationEvent import org.onap.cps.ncmp.utils.TestUtils import org.onap.cps.utils.JsonObjectMapper import org.spockframework.spring.SpringBean -import org.springframework.beans.factory.annotation.Autowired import org.springframework.test.context.ContextConfiguration - +import org.springframework.util.LinkedMultiValueMap import java.time.Duration -import java.util.concurrent.TimeoutException - -import static org.onap.cps.ncmp.api.impl.events.mapper.CloudEventMapper.toTargetEvent -@ContextConfiguration(classes = [EventsPublisher, CpsApplicationContext, ObjectMapper]) +@ContextConfiguration(classes = [EventsPublisher, CpsApplicationContext]) class ResourceDataOperationRequestUtilsSpec extends MessagingBaseSpec { def static clientTopic = 'my-topic-name' @@ -57,9 +57,6 @@ class ResourceDataOperationRequestUtilsSpec extends MessagingBaseSpec { @SpringBean EventsPublisher eventPublisher = new EventsPublisher<CloudEvent>(legacyEventKafkaTemplate, cloudEventKafkaTemplate) - @Autowired - ObjectMapper objectMapper - def 'Process per data operation request with #serviceName.'() { given: 'data operation request with 3 operations' def dataOperationRequestJsonData = TestUtils.getResourceFileContent('dataOperationRequest.json') @@ -90,6 +87,23 @@ class ResourceDataOperationRequestUtilsSpec extends MessagingBaseSpec { 'dmi2' | 2 || 'operational-15' | 'ncmp-datastore:passthrough-operational' | ['ch4-dmi2'] } + def 'Process one data operation request with #serviceName and Module Set Tag set.'() { + given: 'data operation request' + def dataOperationRequestJsonData = TestUtils.getResourceFileContent('dataOperationRequest.json') + def dataOperationRequest = jsonObjectMapper.convertJsonString(dataOperationRequestJsonData, DataOperationRequest.class) + and: '1 known cm handles: ch1-dmi1' + def yangModelCmHandles = getYangModelCmHandlesForOneCmHandle() + when: 'data operation request is processed' + def operationsOutPerDmiServiceName = ResourceDataOperationRequestUtils.processPerDefinitionInDataOperationsRequest(clientTopic,'request-id', dataOperationRequest, yangModelCmHandles) + and: 'converted to a json node' + def dmiDataOperationRequestBody = operationsOutPerDmiServiceName['dmi1'] + def cmHandlesInRequestBody = dmiDataOperationRequestBody[0].cmHandles + then: 'it contains the correct operation details' + assert cmHandlesInRequestBody.size() == 1 + assert cmHandlesInRequestBody[0].id == 'ch1-dmi1' + assert cmHandlesInRequestBody[0].moduleSetTag == 'module-set-tag1' + } + def 'Process per data operation request with non-ready, non-existing cm handle and publish event to client specified topic'() { given: 'consumer subscribing to client topic' def cloudEventKafkaConsumer = new KafkaConsumer<>(eventConsumerConfigProperties('test-1', CloudEventDeserializer)) @@ -118,34 +132,10 @@ class ResourceDataOperationRequestUtilsSpec extends MessagingBaseSpec { jsonObjectMapper.asJsonString(dataOperationResponseEvent.data.responses) == dataOperationResponseEventJson } - def 'Publish error response for entire data operations request when async task fails'() { - given: 'consumer subscribing to client topic' - def cloudEventKafkaConsumer = new KafkaConsumer<>(eventConsumerConfigProperties(consumerGroupId, CloudEventDeserializer)) - cloudEventKafkaConsumer.subscribe([clientTopic]) - and: 'data operation request having non-ready and non-existing cm handle ids' - def dataOperationRequestJsonData = TestUtils.getResourceFileContent('dataOperationRequest.json') - def dataOperationRequest = jsonObjectMapper.convertJsonString(dataOperationRequestJsonData, DataOperationRequest.class) - when: 'an error occurs for the entire data operations request' - ResourceDataOperationRequestUtils.handleAsyncTaskCompletionForDataOperationsRequest(clientTopic, 'request-id', dataOperationRequest, exceptionThrown) - and: 'subscribed client specified topic is polled and first record is selected' - def consumerRecordOut = cloudEventKafkaConsumer.poll(Duration.ofMillis(1500)).last() - def dataOperationResponseEvent = toTargetEvent(consumerRecordOut.value(), DataOperationEvent.class) - then: 'data operation response event response size is 3' - dataOperationResponseEvent.data.responses.size() == 3 - and: 'all 3 have the expected error code' - dataOperationResponseEvent.data.responses.each { - assert it.statusCode == errorReportedToClientTopic.code - } - where: - scenario | exceptionThrown | consumerGroupId || errorReportedToClientTopic - 'task timed out' | new TimeoutException() | 'test-2' || NcmpResponseStatus.DMI_SERVICE_NOT_RESPONDING - 'unspecified error' | new RuntimeException() | 'test-3' || NcmpResponseStatus.UNKNOWN_ERROR - } - static def getYangModelCmHandles() { def dmiProperties = [new YangModelCmHandle.Property('prop', 'some DMI property')] - def readyState = new CompositeStateBuilder().withCmHandleState(CmHandleState.READY).withLastUpdatedTimeNow().build() - def advisedState = new CompositeStateBuilder().withCmHandleState(CmHandleState.ADVISED).withLastUpdatedTimeNow().build() + def readyState = new CompositeStateBuilder().withCmHandleState(READY).withLastUpdatedTimeNow().build() + def advisedState = new CompositeStateBuilder().withCmHandleState(ADVISED).withLastUpdatedTimeNow().build() return [new YangModelCmHandle(id: 'ch1-dmi1', dmiServiceName: 'dmi1', dmiProperties: dmiProperties, compositeState: readyState), new YangModelCmHandle(id: 'ch2-dmi1', dmiServiceName: 'dmi1', dmiProperties: dmiProperties, compositeState: readyState), new YangModelCmHandle(id: 'ch6-dmi1', dmiServiceName: 'dmi1', dmiProperties: dmiProperties, compositeState: readyState), @@ -156,4 +146,19 @@ class ResourceDataOperationRequestUtilsSpec extends MessagingBaseSpec { new YangModelCmHandle(id: 'non-ready-cm-handle', dmiServiceName: 'dmi2', dmiProperties: dmiProperties, compositeState: advisedState) ] } + + static def getYangModelCmHandlesForOneCmHandle() { + def dmiProperties = [new YangModelCmHandle.Property('prop', 'some DMI property')] + def readyState = new CompositeStateBuilder().withCmHandleState(READY).withLastUpdatedTimeNow().build() + return [new YangModelCmHandle(id: 'ch1-dmi1', dmiServiceName: 'dmi1', moduleSetTag: 'module-set-tag1', dmiProperties: dmiProperties, compositeState: readyState)] + } + + def mockAndPopulateErrorMap(errorReportedToClientTopic) { + def dmiDataOperation = DmiDataOperation.builder().operation(OperationType.fromOperationName('read')) + .operationId('some-op-id').datastore('ncmp-datastore:passthrough-operational') + .options('some-option').resourceIdentifier('some-resource-identifier').build() + def cmHandleIdsPerResponseCodesPerOperation = new LinkedMultiValueMap<>() + cmHandleIdsPerResponseCodesPerOperation.add(dmiDataOperation, Map.of(errorReportedToClientTopic, ['some-cm-handle-id'])) + return cmHandleIdsPerResponseCodesPerOperation + } } diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/init/AbstractModelLoaderSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/init/AbstractModelLoaderSpec.groovy index b0be29d93..162a9831c 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/init/AbstractModelLoaderSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/init/AbstractModelLoaderSpec.groovy @@ -32,7 +32,7 @@ import org.onap.cps.spi.CascadeDeleteAllowed import org.onap.cps.spi.exceptions.AlreadyDefinedException import org.springframework.boot.SpringApplication import org.slf4j.LoggerFactory -import org.springframework.boot.context.event.ApplicationReadyEvent +import org.springframework.boot.context.event.ApplicationStartedEvent import org.springframework.context.annotation.AnnotationConfigApplicationContext import spock.lang.Specification @@ -64,18 +64,18 @@ class AbstractModelLoaderSpec extends Specification { applicationContext.close() } - def 'Application ready event'() { - when: 'Application (ready) event is triggered' - objectUnderTest.onApplicationEvent(Mock(ApplicationReadyEvent)) + def 'Application started event'() { + when: 'Application (started) event is triggered' + objectUnderTest.onApplicationEvent(Mock(ApplicationStartedEvent)) then: 'the onboard/upgrade method is executed' 1 * objectUnderTest.onboardOrUpgradeModel() } - def 'Application ready event with start up exception'() { + def 'Application started event with start up exception'() { given: 'a start up exception is thrown doing model onboarding' objectUnderTest.onboardOrUpgradeModel() >> { throw new NcmpStartUpException('test message','details are not logged') } - when: 'Application (ready) event is triggered' - objectUnderTest.onApplicationEvent(new ApplicationReadyEvent(new SpringApplication(), null, applicationContext, null)) + when: 'Application (started) event is triggered' + objectUnderTest.onApplicationEvent(new ApplicationStartedEvent(new SpringApplication(), null, applicationContext, null)) then: 'the exception message is logged' def logs = loggingListAppender.list.toString() assert logs.contains('test message') diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/init/CmDataSubscriptionModelLoaderSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/init/CmDataSubscriptionModelLoaderSpec.groovy index f3b405b11..3d490c861 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/init/CmDataSubscriptionModelLoaderSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/init/CmDataSubscriptionModelLoaderSpec.groovy @@ -31,7 +31,7 @@ import org.onap.cps.ncmp.api.impl.exception.NcmpStartUpException import org.onap.cps.spi.exceptions.AlreadyDefinedException import org.onap.cps.spi.model.Dataspace import org.slf4j.LoggerFactory -import org.springframework.boot.context.event.ApplicationReadyEvent +import org.springframework.boot.context.event.ApplicationStartedEvent import org.springframework.context.annotation.AnnotationConfigApplicationContext import spock.lang.Specification @@ -65,11 +65,11 @@ class CmDataSubscriptionModelLoaderSpec extends Specification { applicationContext.close() } - def 'Onboard subscription model via application ready event.'() { + def 'Onboard subscription model via application started event.'() { given: 'dataspace is ready for use' mockCpsDataspaceService.getDataspace(NCMP_DATASPACE_NAME) >> new Dataspace('') when: 'the application is ready' - objectUnderTest.onApplicationEvent(Mock(ApplicationReadyEvent)) + objectUnderTest.onApplicationEvent(Mock(ApplicationStartedEvent)) then: 'the module service to create schema set is called once' 1 * mockCpsModuleService.createSchemaSet(NCMP_DATASPACE_NAME, 'cm-data-subscriptions', expectedYangResourcesToContentMap) and: 'the admin service to create an anchor set is called once' diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/init/InventoryModelLoaderSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/init/InventoryModelLoaderSpec.groovy index cd659bb52..d43288688 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/init/InventoryModelLoaderSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/init/InventoryModelLoaderSpec.groovy @@ -20,23 +20,22 @@ package org.onap.cps.ncmp.init -import org.onap.cps.api.CpsAnchorService - -import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DATASPACE_NAME -import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DMI_REGISTRY_ANCHOR - import ch.qos.logback.classic.Level import ch.qos.logback.classic.Logger import ch.qos.logback.core.read.ListAppender -import org.onap.cps.api.CpsDataspaceService +import org.onap.cps.api.CpsAnchorService import org.onap.cps.api.CpsDataService +import org.onap.cps.api.CpsDataspaceService import org.onap.cps.api.CpsModuleService import org.onap.cps.spi.model.Dataspace import org.slf4j.LoggerFactory -import org.springframework.boot.context.event.ApplicationReadyEvent +import org.springframework.boot.context.event.ApplicationStartedEvent import org.springframework.context.annotation.AnnotationConfigApplicationContext import spock.lang.Specification +import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DATASPACE_NAME +import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DMI_REGISTRY_ANCHOR + class InventoryModelLoaderSpec extends Specification { def mockCpsAdminService = Mock(CpsDataspaceService) @@ -68,8 +67,8 @@ class InventoryModelLoaderSpec extends Specification { def 'Onboard subscription model via application ready event.'() { given: 'dataspace is ready for use' mockCpsAdminService.getDataspace(NCMP_DATASPACE_NAME) >> new Dataspace('') - when: 'the application is ready' - objectUnderTest.onApplicationEvent(Mock(ApplicationReadyEvent)) + when: 'the application is started' + objectUnderTest.onApplicationEvent(Mock(ApplicationStartedEvent)) then: 'the module service is used to create the new schema set from the correct resource' 1 * mockCpsModuleService.createSchemaSet(NCMP_DATASPACE_NAME, 'dmi-registry-2024-02-23', expectedYangResourceToContentMap) and: 'the admin service is used to update the anchor' diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/utils/AlternateIdMatcherSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/utils/AlternateIdMatcherSpec.groovy new file mode 100644 index 000000000..720a7e7e9 --- /dev/null +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/utils/AlternateIdMatcherSpec.groovy @@ -0,0 +1,66 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2024 Nordix Foundation + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the 'License'); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps.ncmp.utils + +import org.onap.cps.ncmp.api.impl.inventory.InventoryPersistence +import org.onap.cps.ncmp.exceptions.NoAlternateIdMatchFoundException +import org.onap.cps.spi.exceptions.DataNodeNotFoundException +import org.onap.cps.spi.model.DataNode +import spock.lang.Specification + +class AlternateIdMatcherSpec extends Specification { + + def mockInventoryPersistence = Mock(InventoryPersistence) + def objectUnderTest = new AlternateIdMatcher(mockInventoryPersistence) + + def setup() { + given: 'cm handle in the registry with alternate id /a/b' + mockInventoryPersistence.getCmHandleDataNodeByAlternateId('/a/b') >> new DataNode() + and: 'no other cm handle' + mockInventoryPersistence.getCmHandleDataNodeByAlternateId(_) >> { throw new DataNodeNotFoundException('', '') } + } + + def 'Finding longest alternate id matches.'() { + expect: 'querying for alternate id a matching result found' + assert objectUnderTest.getCmHandleDataNodeByLongestMatchAlternateId(targetAlternateId, '/') != null + where: 'the following parameters are used' + scenario | targetAlternateId + 'exact match' | '/a/b' + 'parent match' | '/a/b/c' + 'grand parent match' | '/a/b/c/d' + 'trailing separator match' | '/a/b/' + } + + def 'Attempt to find longest alternate id match without any matches.'() { + when: 'attempt to find alternateId' + objectUnderTest.getCmHandleDataNodeByLongestMatchAlternateId(targetAlternateId, '/') + then: 'no alternate id match found exception thrown' + def thrown = thrown(NoAlternateIdMatchFoundException) + and: 'the exception has the relevant details from the error response' + assert thrown.message == 'No matching cm handle found using alternate ids' + assert thrown.details == 'cannot find a datanode with alternate id ' + targetAlternateId + where: 'the following parameters are used' + scenario | targetAlternateId + 'no match for parent only' | '/a' + 'no match for other child' | '/a/c' + 'no match at all' | '/x/y' + } +}
\ No newline at end of file diff --git a/cps-ncmp-service/src/test/resources/application.yml b/cps-ncmp-service/src/test/resources/application.yml index 574b49982..e35f47100 100644 --- a/cps-ncmp-service/src/test/resources/application.yml +++ b/cps-ncmp-service/src/test/resources/application.yml @@ -1,5 +1,5 @@ # ============LICENSE_START======================================================= -# Copyright (C) 2021-2023 Nordix Foundation +# Copyright (C) 2021-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. @@ -30,14 +30,19 @@ app: async-m2m: topic: ncmp-async-m2m avc: - subscription-topic: subscription + cm-subscription-ncmp-in: subscription cm-events-topic: cm-events - subscription-forward-topic-prefix: ${NCMP_FORWARD_CM_AVC_SUBSCRIPTION:ncmp-dmi-cm-avc-subscription-} + cm-subscription-dmi-in: ${CM_SUBSCRIPTION_DMI_IN_TOPIC:ncmp-dmi-cm-avc-subscription} ncmp: dmi: httpclient: - connectionTimeoutInSeconds: 180 + data-services: + connectionTimeoutInSeconds: 123 + maximumInMemorySizeInMegabytes: 7 + model-services: + connectionTimeoutInSeconds: 456 + maximumInMemorySizeInMegabytes: 8 auth: username: some-user password: some-password diff --git a/cps-ncmp-service/src/test/resources/cmSubscription/cmNotificationSubscriptionNcmpInEvent.json b/cps-ncmp-service/src/test/resources/cmSubscription/cmNotificationSubscriptionNcmpInEvent.json index 6b665495c..04d37b8bb 100644 --- a/cps-ncmp-service/src/test/resources/cmSubscription/cmNotificationSubscriptionNcmpInEvent.json +++ b/cps-ncmp-service/src/test/resources/cmSubscription/cmNotificationSubscriptionNcmpInEvent.json @@ -6,14 +6,14 @@ "targetFilter": ["ch1","ch2"], "scopeFilter": { "datastore": "ncmp-datastore:passthrough-operational", - "xpath-filter": ["/x1/y1","x2/y2"] + "xpathFilter": ["/x1/y1","x2/y2"] } }, { "targetFilter": ["ch3","ch4"], "scopeFilter": { "datastore": "ncmp-datastore:passthrough-operational", - "xpath-filter": ["/x3/y3","x4/y4"] + "xpathFilter": ["/x3/y3","x4/y4"] } } ] |