diff options
19 files changed, 474 insertions, 491 deletions
diff --git a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyRestExceptionHandler.java b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyRestExceptionHandler.java index d8bc73a3a5..d8c4af53bb 100644 --- a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyRestExceptionHandler.java +++ b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyRestExceptionHandler.java @@ -28,7 +28,6 @@ import org.onap.cps.ncmp.api.data.exceptions.InvalidOperationException; import org.onap.cps.ncmp.api.data.exceptions.OperationNotSupportedException; import org.onap.cps.ncmp.api.impl.exception.DmiClientRequestException; import org.onap.cps.ncmp.api.impl.exception.DmiRequestException; -import org.onap.cps.ncmp.api.impl.exception.InvalidDmiResourceUrlException; import org.onap.cps.ncmp.api.impl.exception.NcmpException; import org.onap.cps.ncmp.api.impl.exception.ServerNcmpException; import org.onap.cps.ncmp.exceptions.InvalidTopicException; @@ -80,7 +79,7 @@ public class NetworkCmProxyRestExceptionHandler { @ExceptionHandler({DmiRequestException.class, DataValidationException.class, InvalidOperationException.class, OperationNotSupportedException.class, HttpMessageNotReadableException.class, InvalidTopicException.class, - InvalidDatastoreException.class, InvalidDmiResourceUrlException.class}) + InvalidDatastoreException.class}) public static ResponseEntity<Object> handleDmiRequestExceptions(final Exception exception) { return buildErrorResponse(HttpStatus.BAD_REQUEST, exception); } 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 39219bd371..ac7728da92 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 @@ -24,20 +24,17 @@ 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.data.models.OperationType; 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.utils.url.builder.UrlTemplateParameters; import org.onap.cps.ncmp.impl.models.RequiredDmiService; import org.onap.cps.utils.JsonObjectMapper; import org.springframework.beans.factory.annotation.Qualifier; @@ -57,7 +54,6 @@ import reactor.core.publisher.Mono; @Slf4j public class DmiRestClient { - private static final String HEALTH_CHECK_URL_EXTENSION = "/actuator/health"; private static final String NOT_SPECIFIED = ""; private static final String NO_AUTHORIZATION = null; @@ -74,7 +70,7 @@ public class DmiRestClient { * Sends a synchronous (blocking) POST operation to the DMI with a JSON body containing module references. * * @param requiredDmiService Determines if the required service is for a data or model operation. - * @param dmiUrl The DMI resource URL. + * @param urlTemplateParameters The DMI resource URL template with variables. * @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. @@ -82,13 +78,14 @@ public class DmiRestClient { * @throws DmiClientRequestException If there is an error during the DMI request. */ public ResponseEntity<Object> synchronousPostOperationWithJsonData(final RequiredDmiService requiredDmiService, - final String dmiUrl, + final UrlTemplateParameters + urlTemplateParameters, final String requestBodyAsJsonString, final OperationType operationType, final String authorization) { final Mono<ResponseEntity<Object>> responseEntityMono = asynchronousPostOperationWithJsonData(requiredDmiService, - dmiUrl, + urlTemplateParameters, requestBodyAsJsonString, operationType, authorization); @@ -99,22 +96,23 @@ public class DmiRestClient { * 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 urlTemplateParameters The URL template with variables for the POST request. * @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>> asynchronousPostOperationWithJsonData( - final RequiredDmiService requiredDmiService, - final String dmiUrl, - final String requestBodyAsJsonString, - final OperationType operationType, - final String authorization) { + public Mono<ResponseEntity<Object>> asynchronousPostOperationWithJsonData(final RequiredDmiService + requiredDmiService, + final UrlTemplateParameters + urlTemplateParameters, + final String requestBodyAsJsonString, + final OperationType operationType, + final String authorization) { final WebClient webClient = getWebClient(requiredDmiService); return webClient.post() - .uri(toUri(dmiUrl)) + .uri(urlTemplateParameters.urlTemplate(), urlTemplateParameters.urlVariables()) .headers(httpHeaders -> configureHttpHeaders(httpHeaders, authorization)) .body(BodyInserters.fromValue(requestBodyAsJsonString)) .retrieve() @@ -123,25 +121,29 @@ public class DmiRestClient { } /** - * Get DMI plugin health status. + * Retrieves the health status of the DMI plugin. + * This method performs an HTTP GET request to the DMI health check endpoint specified by the URL template + * parameters. If the response status code indicates a client error (4xx) or a server error (5xx), it logs a warning + * and returns an empty Mono. In case of an error during the request, it logs the exception and returns a default + * value of "NOT_SPECIFIED". If the response body contains a JSON node with a "status" field, the value of this + * field is returned. * - * @param dmiUrl the base URL of the dmi-plugin - * @return plugin health status ("UP" is all OK, "" (not-specified) in case of any exception) + * @param urlTemplateParameters the URL template parameters for the DMI health check endpoint + * @return a Mono emitting the health status as a String, or "NOT_SPECIFIED" if an error occurs */ - public String getDmiHealthStatus(final String dmiUrl) { - try { - 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.path("status").asText(); - } catch (final Exception e) { - log.warn("Failed to retrieve health status from {}. Error Message: {}", dmiUrl, e.getMessage()); - return NOT_SPECIFIED; - } + public Mono<String> getDmiHealthStatus(final UrlTemplateParameters urlTemplateParameters) { + return healthChecksWebClient.get() + .uri(urlTemplateParameters.urlTemplate(), urlTemplateParameters.urlVariables()) + .headers(httpHeaders -> configureHttpHeaders(httpHeaders, NO_AUTHORIZATION)) + .retrieve() + .bodyToMono(JsonNode.class) + .map(responseHealthStatus -> responseHealthStatus.path("status").asText()) + .onErrorResume(Exception.class, ex -> { + log.warn("Failed to retrieve health status from {}. Status: {}", + urlTemplateParameters.urlTemplate(), ex.getMessage()); + return Mono.empty(); + }) + .defaultIfEmpty(NOT_SPECIFIED); } private WebClient getWebClient(final RequiredDmiService requiredDmiService) { @@ -156,14 +158,6 @@ public class DmiRestClient { } } - 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)) { 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 index 3a861a68b4..be46105d13 100644 --- 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 @@ -47,68 +47,46 @@ import reactor.netty.resources.ConnectionProvider; public class DmiWebClientConfiguration { private final HttpClientConfiguration httpClientConfiguration; - private static final Duration DEFAULT_RESPONSE_TIMEOUT = Duration.ofSeconds(30); /** - * Configures and creates a WebClient bean for DMI data services. + * Configures and creates a web client bean for DMI data services. * + * @param webClientBuilder The builder instance to create the WebClient. * @return a WebClient instance configured for data services. */ @Bean - public WebClient dataServicesWebClient() { - final HttpClientConfiguration.DataServices dataServiceConfig = httpClientConfiguration.getDataServices(); - final ConnectionProvider dataServicesConnectionProvider - = getConnectionProvider(dataServiceConfig.getConnectionProviderName(), - dataServiceConfig.getMaximumConnectionsTotal(), dataServiceConfig.getPendingAcquireMaxCount()); - final HttpClient dataServicesHttpClient = createHttpClient(dataServiceConfig, dataServicesConnectionProvider); - return buildAndGetWebClient(dataServicesHttpClient, dataServiceConfig.getMaximumInMemorySizeInMegabytes()); + public WebClient dataServicesWebClient(final WebClient.Builder webClientBuilder) { + return configureWebClient(webClientBuilder, httpClientConfiguration.getDataServices()); } /** - * Configures and creates a WebClient bean for DMI model services. + * Configures and creates a web client bean for DMI model services. * + * @param webClientBuilder The builder instance to create the WebClient. * @return a WebClient instance configured for model services. */ @Bean - public WebClient modelServicesWebClient() { - final HttpClientConfiguration.ModelServices modelServiceConfig = httpClientConfiguration.getModelServices(); - final ConnectionProvider modelServicesConnectionProvider - = getConnectionProvider(modelServiceConfig.getConnectionProviderName(), - modelServiceConfig.getMaximumConnectionsTotal(), - modelServiceConfig.getPendingAcquireMaxCount()); - final HttpClient modelServicesHttpClient - = createHttpClient(modelServiceConfig, modelServicesConnectionProvider); - return buildAndGetWebClient(modelServicesHttpClient, modelServiceConfig.getMaximumInMemorySizeInMegabytes()); + public WebClient modelServicesWebClient(final WebClient.Builder webClientBuilder) { + return configureWebClient(webClientBuilder, httpClientConfiguration.getModelServices()); } /** - * Configures and creates a WebClient bean for DMI health check services. + * Configures and creates a web client bean for DMI health check services. * + * @param webClientBuilder The builder instance to create the WebClient. * @return a WebClient instance configured for health check services. */ @Bean - public WebClient healthChecksWebClient() { - final HttpClientConfiguration.HealthCheckServices healthCheckServiceConfig - = httpClientConfiguration.getHealthCheckServices(); - final ConnectionProvider healthChecksConnectionProvider - = getConnectionProvider(healthCheckServiceConfig.getConnectionProviderName(), - healthCheckServiceConfig.getMaximumConnectionsTotal(), - healthCheckServiceConfig.getPendingAcquireMaxCount()); - final HttpClient healthChecksHttpClient - = createHttpClient(healthCheckServiceConfig, healthChecksConnectionProvider); - return buildAndGetWebClient(healthChecksHttpClient, - healthCheckServiceConfig.getMaximumInMemorySizeInMegabytes()); + public WebClient healthChecksWebClient(final WebClient.Builder webClientBuilder) { + return configureWebClient(webClientBuilder, httpClientConfiguration.getHealthCheckServices()); } - /** - * Provides a WebClient.Builder bean for creating WebClient instances. - * - * @return a WebClient.Builder instance. - */ - @Bean - public WebClient.Builder webClientBuilder() { - return WebClient.builder(); + private WebClient configureWebClient(final WebClient.Builder webClientBuilder, + final HttpClientConfiguration.ServiceConfig serviceConfig) { + final ConnectionProvider connectionProvider = getConnectionProvider(serviceConfig); + final HttpClient httpClient = createHttpClient(serviceConfig, connectionProvider); + return buildAndGetWebClient(webClientBuilder, httpClient, serviceConfig.getMaximumInMemorySizeInMegabytes()); } private static HttpClient createHttpClient(final HttpClientConfiguration.ServiceConfig serviceConfig, @@ -124,18 +102,16 @@ public class DmiWebClientConfiguration { } @SuppressFBWarnings("BC_UNCONFIRMED_CAST_OF_RETURN_VALUE") - private static ConnectionProvider getConnectionProvider(final String connectionProviderName, - final int maximumConnectionsTotal, - final int pendingAcquireMaxCount) { - return ConnectionProvider.builder(connectionProviderName) - .maxConnections(maximumConnectionsTotal) - .pendingAcquireMaxCount(pendingAcquireMaxCount) + private static ConnectionProvider getConnectionProvider(final HttpClientConfiguration.ServiceConfig serviceConfig) { + return ConnectionProvider.builder(serviceConfig.getConnectionProviderName()) + .maxConnections(serviceConfig.getMaximumConnectionsTotal()) + .pendingAcquireMaxCount(serviceConfig.getPendingAcquireMaxCount()) .build(); } - private WebClient buildAndGetWebClient(final HttpClient httpClient, - final int maximumInMemorySizeInMegabytes) { - return webClientBuilder() + private WebClient buildAndGetWebClient(final WebClient.Builder webClientBuilder, final HttpClient httpClient, + final int maximumInMemorySizeInMegabytes) { + return webClientBuilder .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)) diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/exception/InvalidDmiResourceUrlException.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/exception/InvalidDmiResourceUrlException.java deleted file mode 100644 index 270988b63b..0000000000 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/exception/InvalidDmiResourceUrlException.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * ============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.exception; - -import lombok.Getter; - -@Getter -public class InvalidDmiResourceUrlException extends RuntimeException { - - private static final long serialVersionUID = 2928476384584894968L; - - private static final String INVALID_DMI_URL = "Invalid dmi resource url"; - final Integer httpStatus; - - 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/utils/DmiServiceUrlBuilder.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/DmiServiceUrlBuilder.java deleted file mode 100644 index aeeeb6430f..0000000000 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/DmiServiceUrlBuilder.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * ============LICENSE_START======================================================= - * Copyright (C) 2022-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.utils; - -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.Map; -import lombok.NoArgsConstructor; -import org.apache.logging.log4j.util.Strings; -import org.springframework.web.util.UriComponentsBuilder; - -@NoArgsConstructor -public class DmiServiceUrlBuilder { - - private static final String FIXED_PATH_SEGMENT = null; - - final UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.newInstance(); - final Map<String, Object> pathSegments = new LinkedHashMap<>(); - - public static DmiServiceUrlBuilder newInstance() { - return new DmiServiceUrlBuilder(); - } - - /** - * Add a fixed pathSegment to the URI. - * - * @param pathSegment the path segment - * @return this builder - */ - public DmiServiceUrlBuilder pathSegment(final String pathSegment) { - pathSegments.put(pathSegment, FIXED_PATH_SEGMENT); - return this; - } - - /** - * Add a variable pathSegment to the URI. - * Do NOT add { } braces. the builder will take care of that - * - * @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 DmiServiceUrlBuilder variablePathSegment(final String pathSegment, final Object value) { - pathSegments.put(pathSegment, value); - return this; - } - - /** - * Add a query parameter to the URI. - * Do NOT encode as the builder wil take care of encoding - * - * @param name the name of the variable - * @param value the value of the variable (only Strings are supported). - * - * @return this builder - */ - public DmiServiceUrlBuilder queryParameter(final String name, final String value) { - if (Strings.isNotBlank(value)) { - uriComponentsBuilder.queryParam(name, value); - } - return this; - } - - /** - * 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 - * - * @return URI as a string - */ - public String build(final String dmiServiceName, final String dmiBasePath) { - uriComponentsBuilder - .path("{dmiServiceName}") - .pathSegment("{dmiBasePath}") - .pathSegment("v1"); - - final Map<String, Object> uriVariables = new HashMap<>(); - uriVariables.put("dmiServiceName", dmiServiceName); - uriVariables.put("dmiBasePath", dmiBasePath); - - 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(); - } - -} diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/url/builder/DmiServiceUrlTemplateBuilder.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/url/builder/DmiServiceUrlTemplateBuilder.java new file mode 100644 index 0000000000..b89b7b3221 --- /dev/null +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/url/builder/DmiServiceUrlTemplateBuilder.java @@ -0,0 +1,137 @@ +/* + * ============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.utils.url.builder; + +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.util.Strings; +import org.springframework.web.util.UriComponentsBuilder; + +@NoArgsConstructor +public class DmiServiceUrlTemplateBuilder { + + private final UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.newInstance(); + private static final String FIXED_PATH_SEGMENT = null; + private static final String VERSION_SEGMENT = "v1"; + private final Map<String, String> pathSegments = new LinkedHashMap<>(); + private final Map<String, String> queryParameters = new LinkedHashMap<>(); + + /** + * Static factory method to create a new instance of DmiServiceUrlTemplateBuilder. + * + * @return a new instance of DmiServiceUrlTemplateBuilder + */ + public static DmiServiceUrlTemplateBuilder newInstance() { + return new DmiServiceUrlTemplateBuilder(); + } + + /** + * Add a fixed pathSegment to the URL. + * + * @param pathSegment the path segment + * @return this builder instance + */ + public DmiServiceUrlTemplateBuilder fixedPathSegment(final String pathSegment) { + pathSegments.put(pathSegment, FIXED_PATH_SEGMENT); + return this; + } + + /** + * Add a variable pathSegment to the URL. + * Do NOT add { } braces. the builder will take care of that + * + * @param pathSegment the name of the variable path segment (with { and } + * @param value the value to be insert in teh URL for the given variable path segment + * @return this builder instance + */ + public DmiServiceUrlTemplateBuilder variablePathSegment(final String pathSegment, final String value) { + pathSegments.put(pathSegment, value); + return this; + } + + /** + * Add a query parameter to the URL. + * Do NOT encode as the builder wil take care of encoding + * + * @param queryParameterName the name of the variable + * @param queryParameterValue the value of the variable (only Strings are supported). + * + * @return this builder instance + */ + public DmiServiceUrlTemplateBuilder queryParameter(final String queryParameterName, + final String queryParameterValue) { + if (Strings.isNotBlank(queryParameterValue)) { + queryParameters.put(queryParameterName, queryParameterValue); + } + return this; + } + + /** + * Constructs a URL template with variables based on the accumulated path segments and query parameters. + * + * @param dmiServiceBaseUrl the base URL of the DMI service, e.g., "http://dmi-service.com". + * @param dmiBasePath the base path of the DMI service + * @return a UrlTemplateParameters instance containing the complete URL template and URL variables + */ + public UrlTemplateParameters createUrlTemplateParameters(final String dmiServiceBaseUrl, final String dmiBasePath) { + this.uriComponentsBuilder.pathSegment(dmiBasePath) + .pathSegment(VERSION_SEGMENT); + + final Map<String, String> urlTemplateVariables = new HashMap<>(); + + pathSegments.forEach((pathSegmentName, variablePathValue) -> { + if (StringUtils.equals(variablePathValue, FIXED_PATH_SEGMENT)) { + this.uriComponentsBuilder.pathSegment(pathSegmentName); + } else { + this.uriComponentsBuilder.pathSegment("{" + pathSegmentName + "}"); + urlTemplateVariables.put(pathSegmentName, variablePathValue); + } + }); + + queryParameters.forEach((paramName, paramValue) -> { + this.uriComponentsBuilder.queryParam(paramName, "{" + paramName + "}"); + urlTemplateVariables.put(paramName, paramValue); + }); + + final String urlTemplate = dmiServiceBaseUrl + this.uriComponentsBuilder.build().toUriString(); + return new UrlTemplateParameters(urlTemplate, urlTemplateVariables); + } + + /** + * Constructs a URL for DMI health check based on the given base URL. + * + * @param dmiServiceBaseUrl the base URL of the DMI service, e.g., "http://dmi-service.com". + * @return a {@link UrlTemplateParameters} instance containing the complete URL template and empty URL variables, + * suitable for DMI health check. + */ + public UrlTemplateParameters createUrlTemplateParametersForHealthCheck(final String dmiServiceBaseUrl) { + this.uriComponentsBuilder.pathSegment("actuator") + .pathSegment("health"); + + final String urlTemplate = dmiServiceBaseUrl + this.uriComponentsBuilder.build().toUriString(); + return new UrlTemplateParameters(urlTemplate, Collections.emptyMap()); + } + +} diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/url/builder/UrlTemplateParameters.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/url/builder/UrlTemplateParameters.java new file mode 100644 index 0000000000..edf56197b5 --- /dev/null +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/url/builder/UrlTemplateParameters.java @@ -0,0 +1,30 @@ +/* + * ============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.url.builder; + +import java.util.Map; + +/** + * Represents a URL template with associated variables for dynamic substitution. + * This record encapsulates a URL template string and a map of variables used for substitution within the template. + */ +public record UrlTemplateParameters(String urlTemplate, Map<String, String> urlVariables) { +} diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/DmiDataOperations.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/DmiDataOperations.java index e6bb712861..efe0335e8c 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/DmiDataOperations.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/DmiDataOperations.java @@ -23,6 +23,8 @@ package org.onap.cps.ncmp.impl.data; import static org.onap.cps.ncmp.api.data.models.DatastoreType.PASSTHROUGH_OPERATIONAL; import static org.onap.cps.ncmp.api.data.models.DatastoreType.PASSTHROUGH_RUNNING; +import static org.onap.cps.ncmp.api.data.models.OperationType.READ; +import static org.onap.cps.ncmp.impl.models.RequiredDmiService.DATA; import io.micrometer.core.annotation.Timed; import java.util.Collection; @@ -38,7 +40,8 @@ import org.onap.cps.ncmp.api.data.models.OperationType; import org.onap.cps.ncmp.api.impl.client.DmiRestClient; 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.DmiServiceUrlBuilder; +import org.onap.cps.ncmp.api.impl.utils.url.builder.DmiServiceUrlTemplateBuilder; +import org.onap.cps.ncmp.api.impl.utils.url.builder.UrlTemplateParameters; import org.onap.cps.ncmp.impl.data.models.DmiDataOperation; import org.onap.cps.ncmp.impl.data.models.DmiDataOperationRequest; import org.onap.cps.ncmp.impl.data.models.DmiOperationCmHandle; @@ -47,7 +50,6 @@ import org.onap.cps.ncmp.impl.inventory.InventoryPersistence; import org.onap.cps.ncmp.impl.inventory.models.CmHandleState; import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle; import org.onap.cps.ncmp.impl.models.DmiRequestBody; -import org.onap.cps.ncmp.impl.models.RequiredDmiService; import org.onap.cps.spi.exceptions.CpsException; import org.onap.cps.utils.JsonObjectMapper; import org.springframework.http.ResponseEntity; @@ -92,11 +94,11 @@ public class DmiDataOperations { final YangModelCmHandle yangModelCmHandle = getYangModelCmHandle(cmResourceAddress.cmHandleId()); final CmHandleState cmHandleState = yangModelCmHandle.getCompositeState().getCmHandleState(); validateIfCmHandleStateReady(yangModelCmHandle, cmHandleState); - final String jsonRequestBody = getDmiRequestBody(OperationType.READ, requestId, null, null, yangModelCmHandle); - final String dmiUrl = getDmiResourceDataUrl(cmResourceAddress.datastoreName(), yangModelCmHandle, - cmResourceAddress.resourceIdentifier(), options, topic); - return dmiRestClient.asynchronousPostOperationWithJsonData(RequiredDmiService.DATA, - dmiUrl, jsonRequestBody, OperationType.READ, authorization); + final String jsonRequestBody = getDmiRequestBody(READ, requestId, null, null, yangModelCmHandle); + final UrlTemplateParameters urlTemplateParameters = getUrlTemplateParameters(cmResourceAddress + .datastoreName(), yangModelCmHandle, cmResourceAddress.resourceIdentifier(), options, topic); + return dmiRestClient.asynchronousPostOperationWithJsonData(DATA, urlTemplateParameters, jsonRequestBody, READ, + authorization); } /** @@ -113,11 +115,12 @@ public class DmiDataOperations { final CmHandleState cmHandleState = yangModelCmHandle.getCompositeState().getCmHandleState(); validateIfCmHandleStateReady(yangModelCmHandle, cmHandleState); - final String jsonRequestBody = getDmiRequestBody(OperationType.READ, requestId, null, null, yangModelCmHandle); - final String dmiUrl = - getDmiResourceDataUrl(PASSTHROUGH_OPERATIONAL.getDatastoreName(), yangModelCmHandle, "/", null, null); - return dmiRestClient.synchronousPostOperationWithJsonData(RequiredDmiService.DATA, dmiUrl, jsonRequestBody, - OperationType.READ, null); + final String jsonRequestBody = getDmiRequestBody(READ, requestId, null, null, yangModelCmHandle); + final UrlTemplateParameters urlTemplateParameters = getUrlTemplateParameters( + PASSTHROUGH_OPERATIONAL.getDatastoreName(), yangModelCmHandle, "/", null, + null); + return dmiRestClient.synchronousPostOperationWithJsonData(DATA, urlTemplateParameters, jsonRequestBody, READ, + null); } /** @@ -135,7 +138,7 @@ public class DmiDataOperations { final String authorization) { final Set<String> cmHandlesIds - = getDistinctCmHandleIdsFromDataOperationRequest(dataOperationRequest); + = getDistinctCmHandleIds(dataOperationRequest); final Collection<YangModelCmHandle> yangModelCmHandles = inventoryPersistence.getYangModelCmHandles(cmHandlesIds); @@ -144,7 +147,7 @@ public class DmiDataOperations { = DmiDataOperationsHelper.processPerDefinitionInDataOperationsRequest(topicParamInQuery, requestId, dataOperationRequest, yangModelCmHandles); - buildDataOperationRequestUrlAndSendToDmiService(requestId, topicParamInQuery, operationsOutPerDmiServiceName, + asyncSendMultipleRequest(requestId, topicParamInQuery, operationsOutPerDmiServiceName, authorization); } @@ -172,10 +175,11 @@ public class DmiDataOperations { final String jsonRequestBody = getDmiRequestBody(operationType, null, requestData, dataType, yangModelCmHandle); - final String dmiUrl = getDmiResourceDataUrl(PASSTHROUGH_RUNNING.getDatastoreName(), - yangModelCmHandle, resourceId, null, null); - return dmiRestClient.synchronousPostOperationWithJsonData(RequiredDmiService.DATA, dmiUrl, jsonRequestBody, - operationType, authorization); + final UrlTemplateParameters urlTemplateParameters = getUrlTemplateParameters( + PASSTHROUGH_RUNNING.getDatastoreName(), yangModelCmHandle, resourceId, null, + null); + return dmiRestClient.synchronousPostOperationWithJsonData(DATA, urlTemplateParameters, jsonRequestBody, + operationType, authorization); } private YangModelCmHandle getYangModelCmHandle(final String cmHandleId) { @@ -198,22 +202,32 @@ public class DmiDataOperations { return jsonObjectMapper.asJsonString(dmiRequestBody); } - 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 UrlTemplateParameters getUrlTemplateParameters(final String datastoreName, + final YangModelCmHandle yangModelCmHandle, + final String resourceIdentifier, + final String optionsParamInQuery, + final String topicParamInQuery) { + final String dmiServiceName = yangModelCmHandle.resolveDmiServiceName(DATA); + return DmiServiceUrlTemplateBuilder.newInstance() + .fixedPathSegment("ch") + .variablePathSegment("cmHandleId", yangModelCmHandle.getId()) + .fixedPathSegment("data") + .fixedPathSegment("ds") + .variablePathSegment("datastore", datastoreName) + .queryParameter("resourceIdentifier", resourceIdentifier) + .queryParameter("options", optionsParamInQuery) + .queryParameter("topic", topicParamInQuery) + .createUrlTemplateParameters(dmiServiceName, dmiProperties.getDmiBasePath()); + } + + private UrlTemplateParameters getUrlTemplateParameters(final String dmiServiceName, + final String requestId, + final String topicParamInQuery) { + return DmiServiceUrlTemplateBuilder.newInstance() + .fixedPathSegment("data") + .queryParameter("requestId", requestId) + .queryParameter("topic", topicParamInQuery) + .createUrlTemplateParameters(dmiServiceName, dmiProperties.getDmiBasePath()); } private void validateIfCmHandleStateReady(final YangModelCmHandle yangModelCmHandle, @@ -225,51 +239,37 @@ public class DmiDataOperations { } } - private static Set<String> getDistinctCmHandleIdsFromDataOperationRequest(final DataOperationRequest - dataOperationRequest) { + private static Set<String> getDistinctCmHandleIds(final DataOperationRequest dataOperationRequest) { return dataOperationRequest.getDataOperationDefinitions().stream() .flatMap(dataOperationDefinition -> dataOperationDefinition.getCmHandleIds().stream()).collect(Collectors.toSet()); } - private void buildDataOperationRequestUrlAndSendToDmiService(final String requestId, - final String topicParamInQuery, - final Map<String, List<DmiDataOperation>> - groupsOutPerDmiServiceName, - final String 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 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 void asyncSendMultipleRequest(final String requestId, final String topicParamInQuery, + final Map<String, List<DmiDataOperation>> dmiDataOperationsPerDmi, + final String authorization) { - private Mono<Void> sendDataOperationRequestToDmiService(final String dmiUrl, - final List<DmiDataOperation> dmiDataOperationRequestBodies, - final String authorization) { - final String dmiDataOperationRequestAsJsonString - = createDmiDataOperationRequestAsJsonString(dmiDataOperationRequestBodies); - return dmiRestClient.asynchronousPostOperationWithJsonData(RequiredDmiService.DATA, dmiUrl, - dmiDataOperationRequestAsJsonString, OperationType.READ, authorization) - .then() - .onErrorResume(DmiClientRequestException.class, dmiClientRequestException -> { - handleTaskCompletionException(dmiClientRequestException, dmiUrl, dmiDataOperationRequestBodies); - return Mono.empty(); - }); + Flux.fromIterable(dmiDataOperationsPerDmi.entrySet()) + .flatMap(entry -> { + final String dmiServiceName = entry.getKey(); + final UrlTemplateParameters urlTemplateParameters = getUrlTemplateParameters(dmiServiceName, + requestId, topicParamInQuery); + final List<DmiDataOperation> dmiDataOperations = entry.getValue(); + final String dmiDataOperationRequestAsJsonString + = createDmiDataOperationRequestAsJsonString(dmiDataOperations); + return dmiRestClient.asynchronousPostOperationWithJsonData(DATA, urlTemplateParameters, + dmiDataOperationRequestAsJsonString, READ, authorization) + .then() + .onErrorResume(DmiClientRequestException.class, dmiClientRequestException -> { + final String dataOperationResourceUrl = UriComponentsBuilder + .fromUriString(urlTemplateParameters.urlTemplate()) + .buildAndExpand(urlTemplateParameters.urlVariables()) + .toUriString(); + handleTaskCompletionException(dmiClientRequestException, dataOperationResourceUrl, + dmiDataOperations); + return Mono.empty(); + }); + }).subscribe(); } private String createDmiDataOperationRequestAsJsonString( @@ -282,7 +282,7 @@ public class DmiDataOperations { private void handleTaskCompletionException(final DmiClientRequestException dmiClientRequestException, final String dataOperationResourceUrl, - final List<DmiDataOperation> dmiDataOperationRequestBodies) { + final List<DmiDataOperation> dmiDataOperations) { final MultiValueMap<String, String> dataOperationResourceUrlParameters = UriComponentsBuilder.fromUriString(dataOperationResourceUrl).build().getQueryParams(); final String topicName = dataOperationResourceUrlParameters.get("topic").get(0); @@ -291,7 +291,7 @@ public class DmiDataOperations { final MultiValueMap<DmiDataOperation, Map<NcmpResponseStatus, List<String>>> cmHandleIdsPerResponseCodesPerOperation = new LinkedMultiValueMap<>(); - dmiDataOperationRequestBodies.forEach(dmiDataOperationRequestBody -> { + dmiDataOperations.forEach(dmiDataOperationRequestBody -> { final List<String> cmHandleIds = dmiDataOperationRequestBody.getCmHandles().stream() .map(DmiOperationCmHandle::getId).toList(); cmHandleIdsPerResponseCodesPerOperation.add(dmiDataOperationRequestBody, diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/datajobs/DmiSubJobRequestHandler.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/datajobs/DmiSubJobRequestHandler.java index 25144ad974..973a2a9b2d 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/datajobs/DmiSubJobRequestHandler.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/datajobs/DmiSubJobRequestHandler.java @@ -33,7 +33,8 @@ import org.onap.cps.ncmp.api.datajobs.models.SubJobWriteRequest; import org.onap.cps.ncmp.api.datajobs.models.SubJobWriteResponse; import org.onap.cps.ncmp.api.impl.client.DmiRestClient; 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.utils.url.builder.DmiServiceUrlTemplateBuilder; +import org.onap.cps.ncmp.api.impl.utils.url.builder.UrlTemplateParameters; import org.onap.cps.ncmp.impl.models.RequiredDmiService; import org.onap.cps.utils.JsonObjectMapper; import org.springframework.http.ResponseEntity; @@ -64,10 +65,10 @@ public class DmiSubJobRequestHandler { final SubJobWriteRequest subJobWriteRequest = new SubJobWriteRequest(dataJobMetadata.dataAcceptType(), dataJobMetadata.dataContentType(), dataJobId, dmi3ggpWriteOperations); - final String dmiResourceUrl = getDmiResourceUrl(dataJobId, producerKey); + final UrlTemplateParameters urlTemplateParameters = getUrlTemplateParameters(dataJobId, producerKey); final ResponseEntity<Object> responseEntity = dmiRestClient.synchronousPostOperationWithJsonData( RequiredDmiService.DATA, - dmiResourceUrl, + urlTemplateParameters, jsonObjectMapper.asJsonString(subJobWriteRequest), OperationType.CREATE, NO_AUTH_HEADER); @@ -78,8 +79,10 @@ public class DmiSubJobRequestHandler { return subJobWriteResponses; } - private String getDmiResourceUrl(final String dataJobId, final ProducerKey producerKey) { - return DmiServiceUrlBuilder.newInstance().pathSegment("writeJob").variablePathSegment("requestId", dataJobId) - .build(producerKey.dmiServiceName(), dmiProperties.getDmiBasePath()); + private UrlTemplateParameters getUrlTemplateParameters(final String dataJobId, final ProducerKey producerKey) { + return DmiServiceUrlTemplateBuilder.newInstance() + .fixedPathSegment("writeJob") + .variablePathSegment("requestId", dataJobId) + .createUrlTemplateParameters(producerKey.dmiServiceName(), dmiProperties.getDmiBasePath()); } } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/sync/DmiModelOperations.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/sync/DmiModelOperations.java index 6a49360b9d..7d6677ca38 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/sync/DmiModelOperations.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/sync/DmiModelOperations.java @@ -21,6 +21,9 @@ package org.onap.cps.ncmp.impl.inventory.sync; +import static org.onap.cps.ncmp.api.data.models.OperationType.READ; +import static org.onap.cps.ncmp.impl.models.RequiredDmiService.MODEL; + import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; @@ -32,14 +35,13 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import lombok.RequiredArgsConstructor; -import org.onap.cps.ncmp.api.data.models.OperationType; import org.onap.cps.ncmp.api.impl.client.DmiRestClient; 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.utils.url.builder.DmiServiceUrlTemplateBuilder; +import org.onap.cps.ncmp.api.impl.utils.url.builder.UrlTemplateParameters; import org.onap.cps.ncmp.api.inventory.models.YangResource; import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle; import org.onap.cps.ncmp.impl.models.DmiRequestBody; -import org.onap.cps.ncmp.impl.models.RequiredDmiService; import org.onap.cps.spi.model.ModuleReference; import org.onap.cps.utils.JsonObjectMapper; import org.springframework.http.ResponseEntity; @@ -67,7 +69,7 @@ public class DmiModelOperations { .moduleSetTag(yangModelCmHandle.getModuleSetTag()).build(); dmiRequestBody.asDmiProperties(yangModelCmHandle.getDmiProperties()); final ResponseEntity<Object> dmiFetchModulesResponseEntity = getResourceFromDmiWithJsonData( - yangModelCmHandle.resolveDmiServiceName(RequiredDmiService.MODEL), + yangModelCmHandle.resolveDmiServiceName(MODEL), jsonObjectMapper.asJsonString(dmiRequestBody), yangModelCmHandle.getId(), "modules"); return toModuleReferences((Map) dmiFetchModulesResponseEntity.getBody()); } @@ -87,7 +89,7 @@ public class DmiModelOperations { final String jsonWithDataAndDmiProperties = getRequestBodyToFetchYangResources(newModuleReferences, yangModelCmHandle.getDmiProperties(), yangModelCmHandle.getModuleSetTag()); final ResponseEntity<Object> responseEntity = getResourceFromDmiWithJsonData( - yangModelCmHandle.resolveDmiServiceName(RequiredDmiService.MODEL), + yangModelCmHandle.resolveDmiServiceName(MODEL), jsonWithDataAndDmiProperties, yangModelCmHandle.getId(), "moduleResources"); @@ -107,13 +109,13 @@ public class DmiModelOperations { final String jsonRequestBody, final String cmHandle, final String resourceName) { - final String dmiUrl = DmiServiceUrlBuilder.newInstance() - .pathSegment("ch") + final UrlTemplateParameters urlTemplateParameters = DmiServiceUrlTemplateBuilder.newInstance() + .fixedPathSegment("ch") .variablePathSegment("cmHandleId", cmHandle) - .variablePathSegment("resourceName", resourceName) - .build(dmiServiceName, dmiProperties.getDmiBasePath()); - return dmiRestClient.synchronousPostOperationWithJsonData(RequiredDmiService.MODEL, dmiUrl, jsonRequestBody, - OperationType.READ, null); + .fixedPathSegment(resourceName) + .createUrlTemplateParameters(dmiServiceName, dmiProperties.getDmiBasePath()); + return dmiRestClient.synchronousPostOperationWithJsonData(MODEL, urlTemplateParameters, jsonRequestBody, READ, + null); } private static String getRequestBodyToFetchYangResources(final Collection<ModuleReference> newModuleReferences, diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/trustlevel/DmiPluginTrustLevelWatchDog.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/trustlevel/DmiPluginTrustLevelWatchDog.java index 19597a205b..8be8ead44c 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/trustlevel/DmiPluginTrustLevelWatchDog.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/trustlevel/DmiPluginTrustLevelWatchDog.java @@ -25,6 +25,8 @@ import java.util.Map; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.onap.cps.ncmp.api.impl.client.DmiRestClient; +import org.onap.cps.ncmp.api.impl.utils.url.builder.DmiServiceUrlTemplateBuilder; +import org.onap.cps.ncmp.api.impl.utils.url.builder.UrlTemplateParameters; import org.onap.cps.ncmp.api.inventory.models.TrustLevel; import org.onap.cps.ncmp.impl.inventory.CmHandleQueryService; import org.springframework.beans.factory.annotation.Qualifier; @@ -50,10 +52,8 @@ public class DmiPluginTrustLevelWatchDog { */ @Scheduled(fixedDelayString = "${ncmp.timers.trust-level.dmi-availability-watchdog-ms:30000}") public void checkDmiAvailability() { - trustLevelPerDmiPlugin.entrySet().forEach(entry -> { + trustLevelPerDmiPlugin.forEach((dmiServiceName, oldDmiTrustLevel) -> { final TrustLevel newDmiTrustLevel; - final TrustLevel oldDmiTrustLevel = entry.getValue(); - final String dmiServiceName = entry.getKey(); final String dmiHealthStatus = getDmiHealthStatus(dmiServiceName); log.debug("The health status for dmi-plugin: {} is {}", dmiServiceName, dmiHealthStatus); @@ -72,7 +72,9 @@ public class DmiPluginTrustLevelWatchDog { }); } - private String getDmiHealthStatus(final String dmiServiceName) { - return dmiRestClient.getDmiHealthStatus(dmiServiceName); + private String getDmiHealthStatus(final String dmiServiceBaseUrl) { + final UrlTemplateParameters urlTemplateParameters = DmiServiceUrlTemplateBuilder.newInstance() + .createUrlTemplateParametersForHealthCheck(dmiServiceBaseUrl); + return dmiRestClient.getDmiHealthStatus(urlTemplateParameters).block(); } } 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 9798040a67..a935d70ce6 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 @@ -26,12 +26,14 @@ import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.node.ObjectNode 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.utils.url.builder.UrlTemplateParameters import org.onap.cps.ncmp.utils.TestUtils import org.onap.cps.utils.JsonObjectMapper import org.springframework.http.HttpHeaders import org.springframework.http.HttpStatus +import org.springframework.http.HttpStatusCode import org.springframework.http.ResponseEntity +import org.springframework.web.client.HttpServerErrorException import org.springframework.web.reactive.function.client.WebClient import org.springframework.web.reactive.function.client.WebClientRequestException import org.springframework.web.reactive.function.client.WebClientResponseException @@ -41,8 +43,6 @@ import spock.lang.Specification 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.onap.cps.ncmp.api.data.models.OperationType.CREATE -import static org.onap.cps.ncmp.api.data.models.OperationType.PATCH import static org.onap.cps.ncmp.api.data.models.OperationType.READ import static org.onap.cps.ncmp.impl.models.RequiredDmiService.DATA import static org.onap.cps.ncmp.impl.models.RequiredDmiService.MODEL @@ -52,6 +52,7 @@ class DmiRestClientSpec extends Specification { static final NO_AUTH_HEADER = null static final BASIC_AUTH_HEADER = 'Basic c29tZSB1c2VyOnNvbWUgcGFzc3dvcmQ=' static final BEARER_AUTH_HEADER = 'Bearer my-bearer-token' + static final urlTemplateParameters = new UrlTemplateParameters('/{pathParam1}/{pathParam2}', ['pathParam1': 'my', 'pathParam2': 'url']) def mockDataServicesWebClient = Mock(WebClient) def mockModelServicesWebClient = Mock(WebClient) @@ -67,7 +68,7 @@ class DmiRestClientSpec extends Specification { DmiRestClient objectUnderTest = new DmiRestClient(mockDmiProperties, jsonObjectMapper, mockDataServicesWebClient, mockModelServicesWebClient, mockHealthChecksWebClient) def setup() { - mockRequestBody.uri(_) >> mockRequestBody + mockRequestBody.uri(_,_) >> mockRequestBody mockRequestBody.headers(_) >> mockRequestBody mockRequestBody.body(_) >> mockRequestBody mockRequestBody.retrieve() >> mockResponse @@ -78,7 +79,7 @@ class DmiRestClientSpec extends Specification { mockDataServicesWebClient.post() >> mockRequestBody mockResponse.toEntity(Object.class) >> Mono.just(new ResponseEntity<>('from Data service', HttpStatus.I_AM_A_TEAPOT)) when: 'POST operation is invoked fro Data Service' - def response = objectUnderTest.synchronousPostOperationWithJsonData(DATA, '/my/url', 'some json', READ, NO_AUTH_HEADER) + def response = objectUnderTest.synchronousPostOperationWithJsonData(DATA, urlTemplateParameters, 'some json', READ, NO_AUTH_HEADER) then: 'the output of the method is equal to the output from the test template' assert response.statusCode == HttpStatus.I_AM_A_TEAPOT assert response.body == 'from Data service' @@ -89,30 +90,18 @@ class DmiRestClientSpec extends Specification { mockModelServicesWebClient.post() >> mockRequestBody mockResponse.toEntity(Object.class) >> Mono.just(new ResponseEntity<>('from Model service', HttpStatus.I_AM_A_TEAPOT)) when: 'POST operation is invoked for Model Service' - def response = objectUnderTest.synchronousPostOperationWithJsonData(MODEL, '/my/url', 'some json', READ, NO_AUTH_HEADER) + def response = objectUnderTest.synchronousPostOperationWithJsonData(MODEL, urlTemplateParameters, 'some json', READ, NO_AUTH_HEADER) then: 'the output of the method is equal to the output from the test template' assert response.statusCode == HttpStatus.I_AM_A_TEAPOT assert response.body == 'from Model service' } - def 'Failing DMI POST operation due to invalid dmi resource url.'() { - when: 'POST operation is invoked with invalid dmi resource url' - objectUnderTest.synchronousPostOperationWithJsonData(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.synchronousPostOperationWithJsonData(DATA, '/my/url', 'some json', READ, NO_AUTH_HEADER) + objectUnderTest.synchronousPostOperationWithJsonData(DATA, urlTemplateParameters, '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' @@ -123,6 +112,7 @@ class DmiRestClientSpec extends Specification { '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 + 'dmi service unavailable' | 503 | new HttpServerErrorException(HttpStatusCode.valueOf(503)) || DMI_SERVICE_NOT_RESPONDING 'unknown error' | 500 | new Throwable('message') || UNKNOWN_ERROR } @@ -132,25 +122,29 @@ class DmiRestClientSpec extends Specification { def jsonNode = jsonObjectMapper.convertJsonString(dmiPluginHealthCheckResponseJsonData, JsonNode.class) ((ObjectNode) jsonNode).put('status', 'my status') mockHealthChecksWebClient.get() >> mockRequestBody + mockResponse.onStatus(_,_)>> mockResponse mockResponse.bodyToMono(JsonNode.class) >> Mono.just(jsonNode) when: 'get trust level of the dmi plugin' - def result = objectUnderTest.getDmiHealthStatus('some/url') + def urlTemplateParameters = new UrlTemplateParameters('some url', [:]) + def result = objectUnderTest.getDmiHealthStatus(urlTemplateParameters).block() 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' + given: 'web client instance with #scenario' mockHealthChecksWebClient.get() >> mockRequestBody - mockResponse.bodyToMono(_) >> healthStatusResponse + mockResponse.onStatus(_, _) >> mockResponse + mockResponse.bodyToMono(_) >> Mono.error(exceptionType) when: 'attempt to get health status of the dmi plugin' - def result = objectUnderTest.getDmiHealthStatus('some url') + def urlTemplateParameters = new UrlTemplateParameters('some url', [:]) + def result = objectUnderTest.getDmiHealthStatus(urlTemplateParameters).block() then: 'result will be empty' assert result == '' where: 'the following responses are used' - scenario | healthStatusResponse - 'null' | null - 'exception' | { throw new Exception() } + scenario | exceptionType + 'dmi request timeout' | new WebClientResponseException('some-message', 408, 'some-text', null, null, null) + 'dmi service unavailable' | new HttpServerErrorException(HttpStatus.SERVICE_UNAVAILABLE) } def 'DMI auth header #scenario'() { 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 index 05ecaa11b1..fa995aa7c3 100644 --- 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 @@ -33,6 +33,13 @@ import spock.lang.Specification @EnableConfigurationProperties class DmiWebClientConfigurationSpec extends Specification { + def webClientBuilder = Mock(WebClient.Builder) { + defaultHeaders(_) >> it + clientConnector(_) >> it + codecs(_) >> it + build() >> Mock(WebClient) + } + def httpClientConfiguration = Spy(HttpClientConfiguration.class) def objectUnderTest = new DmiWebClientConfiguration(httpClientConfiguration) @@ -44,7 +51,7 @@ class DmiWebClientConfigurationSpec extends Specification { def 'Creating a web client instance data service.'() { given: 'Web client configuration is invoked' - def dataServicesWebClient = objectUnderTest.dataServicesWebClient() + def dataServicesWebClient = objectUnderTest.dataServicesWebClient(webClientBuilder) expect: 'the system can create an instance for data service' assert dataServicesWebClient != null assert dataServicesWebClient instanceof WebClient @@ -52,7 +59,7 @@ class DmiWebClientConfigurationSpec extends Specification { def 'Creating a web client instance model service.'() { given: 'Web client configuration invoked' - def modelServicesWebClient = objectUnderTest.modelServicesWebClient() + def modelServicesWebClient = objectUnderTest.modelServicesWebClient(webClientBuilder) expect: 'the system can create an instance for model service' assert modelServicesWebClient != null assert modelServicesWebClient instanceof WebClient @@ -60,7 +67,7 @@ class DmiWebClientConfigurationSpec extends Specification { def 'Creating a web client instance health service.'() { given: 'Web client configuration invoked' - def healthChecksWebClient = objectUnderTest.healthChecksWebClient() + def healthChecksWebClient = objectUnderTest.healthChecksWebClient(webClientBuilder) 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/utils/DmiServiceUrlBuilderSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/DmiServiceUrlBuilderSpec.groovy deleted file mode 100644 index 69d08e3de6..0000000000 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/DmiServiceUrlBuilderSpec.groovy +++ /dev/null @@ -1,86 +0,0 @@ -/* - * ============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.utils - -import spock.lang.Specification - -class DmiServiceUrlBuilderSpec extends Specification { - - def objectUnderTest = new DmiServiceUrlBuilder() - - 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 '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 '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 '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, '', ' ' ] - } - -} diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/url/builder/DmiServiceUrlTemplateBuilderSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/url/builder/DmiServiceUrlTemplateBuilderSpec.groovy new file mode 100644 index 0000000000..6d56f432d3 --- /dev/null +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/url/builder/DmiServiceUrlTemplateBuilderSpec.groovy @@ -0,0 +1,63 @@ +/* + * ============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.utils.url.builder + +import spock.lang.Specification + +class DmiServiceUrlTemplateBuilderSpec extends Specification { + + def objectUnderTest = new DmiServiceUrlTemplateBuilder() + + def 'Build URL template parameters with (variable) path segments and query parameters.'() { + given: 'the URL details are given to the builder' + objectUnderTest.fixedPathSegment('segment') + objectUnderTest.variablePathSegment('myVariableSegment','someValue') + objectUnderTest.fixedPathSegment('segment?with:special&characters') + objectUnderTest.queryParameter('param1', 'abc') + objectUnderTest.queryParameter('param2', 'value?with#special:characters') + when: 'the URL template parameters are created' + def result = objectUnderTest.createUrlTemplateParameters('myDmiServer', 'myBasePath') + then: 'the URL template contains variable names instead of value and un-encoded fixed segment' + assert result.urlTemplate == 'myDmiServer/myBasePath/v1/segment/{myVariableSegment}/segment?with:special&characters?param1={param1}¶m2={param2}' + and: 'URL variables contains name and un-encoded value pairs' + assert result.urlVariables == ['myVariableSegment': 'someValue', 'param1': 'abc', 'param2': 'value?with#special:characters'] + } + + def 'Build URL template parameters with special characters in query parameters.'() { + given: 'the query parameter is given to the builder' + objectUnderTest.queryParameter('my¶m', 'special&characters=are?not\\encoded') + when: 'the URL template parameters are created' + def result = objectUnderTest.createUrlTemplateParameters('myDmiServer', 'myBasePath') + then: 'Special characters are not encoded' + assert result.urlVariables == ['my¶m': 'special&characters=are?not\\encoded'] + } + + def 'Build URL template parameters with empty query parameters.'() { + when: 'the query parameter is given to the builder' + objectUnderTest.queryParameter('param', value) + and: 'the URL template parameters are create' + def result = objectUnderTest.createUrlTemplateParameters('myDmiServer', 'myBasePath') + then: 'no parameter gets added' + assert result.urlVariables.isEmpty() + where: 'the following parameter values are used' + value << [ null, '', ' ' ] + } +} diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/DmiOperationsBaseSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/DmiOperationsBaseSpec.groovy index f2d2ab0a19..050932f654 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/DmiOperationsBaseSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/DmiOperationsBaseSpec.groovy @@ -45,7 +45,7 @@ abstract class DmiOperationsBaseSpec extends Specification { ObjectMapper spyObjectMapper = Spy() def yangModelCmHandle = new YangModelCmHandle() - def static dmiServiceName = 'someServiceName' + def static dmiServiceName = 'myServiceName' def static cmHandleId = 'some-cm-handle' def static resourceIdentifier = 'parent/child' diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/DmiDataOperationsSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/DmiDataOperationsSpec.groovy index 65d3100d1e..3a199ff417 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/DmiDataOperationsSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/DmiDataOperationsSpec.groovy @@ -28,6 +28,7 @@ import org.onap.cps.ncmp.api.data.models.DataOperationRequest 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.impl.utils.url.builder.UrlTemplateParameters import org.onap.cps.ncmp.events.async1_0_0.DataOperationEvent import org.onap.cps.ncmp.impl.DmiOperationsBaseSpec import org.onap.cps.ncmp.impl.inventory.models.CmHandleState @@ -55,7 +56,6 @@ import static org.onap.cps.ncmp.utils.events.CloudEventMapper.toTargetEvent @ContextConfiguration(classes = [EventsPublisher, CpsApplicationContext, DmiProperties, DmiDataOperations]) class DmiDataOperationsSpec extends DmiOperationsBaseSpec { - def dmiServiceBaseUrl = "${DmiOperationsBaseSpec.dmiServiceName}/dmi/v1/ch/${DmiOperationsBaseSpec.cmHandleId}/data/ds/ncmp-datastore:" def NO_TOPIC = null def NO_REQUEST_ID = null def NO_AUTH_HEADER = null @@ -72,28 +72,28 @@ class DmiDataOperationsSpec extends DmiOperationsBaseSpec { @SpringBean EventsPublisher eventsPublisher = Stub() - def 'call get resource data for #expectedDatastoreInUrl from DMI without topic #scenario.'() { + def 'call get resource data for #expectedDataStore from DMI without topic #scenario.'() { 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 = Mono.just(new ResponseEntity<Object>('{some-key:some-value}', HttpStatus.OK)) - def expectedUrl = "${dmiServiceBaseUrl}${expectedDatastoreInUrl}?resourceIdentifier=${DmiOperationsBaseSpec.resourceIdentifier}${expectedOptionsInUrl}" + def expectedUrlTemplateWithVariables = getExpectedUrlTemplateWithVariables(expectedOptions, expectedDataStore) def expectedJson = '{"operation":"read","cmHandleProperties":' + expectedProperties + ',"moduleSetTag":""}' - mockDmiRestClient.asynchronousPostOperationWithJsonData(DATA, expectedUrl, expectedJson, READ, NO_AUTH_HEADER) >> responseFromDmi + mockDmiRestClient.asynchronousPostOperationWithJsonData(DATA, expectedUrlTemplateWithVariables, expectedJson, READ, NO_AUTH_HEADER) >> responseFromDmi when: 'get resource data is invoked' - def cmResourceAddress = new CmResourceAddress(dataStore.datastoreName, DmiOperationsBaseSpec.cmHandleId, DmiOperationsBaseSpec.resourceIdentifier) - def result = objectUnderTest.getResourceDataFromDmi(cmResourceAddress, options, NO_TOPIC, NO_REQUEST_ID, NO_AUTH_HEADER).block() + def cmResourceAddress = new CmResourceAddress(expectedDataStore.datastoreName, cmHandleId, resourceIdentifier) + def result = objectUnderTest.getResourceDataFromDmi(cmResourceAddress, expectedOptions, NO_TOPIC, NO_REQUEST_ID, NO_AUTH_HEADER).block() then: 'the result is the response from the DMI service' assert result.body == '{some-key:some-value}' assert result.statusCode.'2xxSuccessful' where: 'the following parameters are used' - 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)' + scenario | dmiProperties || expectedDataStore | expectedOptions | expectedProperties + 'without properties' | [] || PASSTHROUGH_OPERATIONAL | OPTIONS_PARAM | '{}' + 'with properties' | [yangModelCmHandleProperty] || PASSTHROUGH_OPERATIONAL | OPTIONS_PARAM | '{"prop1":"val1"}' + 'null options' | [yangModelCmHandleProperty] || PASSTHROUGH_OPERATIONAL | null | '{"prop1":"val1"}' + 'empty options' | [yangModelCmHandleProperty] || PASSTHROUGH_OPERATIONAL | '' | '{"prop1":"val1"}' + 'datastore running without properties' | [] || PASSTHROUGH_RUNNING | OPTIONS_PARAM | '{}' + 'datastore running with properties' | [yangModelCmHandleProperty] || PASSTHROUGH_RUNNING | OPTIONS_PARAM | '{"prop1":"val1"}' } def 'Execute (async) data operation from DMI service.'() { @@ -101,16 +101,16 @@ class DmiDataOperationsSpec extends DmiOperationsBaseSpec { mockYangModelCmHandleCollectionRetrieval([yangModelCmHandleProperty]) def dataOperationBatchRequestJsonData = TestUtils.getResourceFileContent('dataOperationRequest.json') def dataOperationRequest = spiedJsonObjectMapper.convertJsonString(dataOperationBatchRequestJsonData, DataOperationRequest.class) - dataOperationRequest.dataOperationDefinitions[0].cmHandleIds = [DmiOperationsBaseSpec.cmHandleId] + dataOperationRequest.dataOperationDefinitions[0].cmHandleIds = [cmHandleId] and: 'a positive response from DMI service when it is called with valid request parameters' def responseFromDmi = Mono.just(new ResponseEntity<Object>(HttpStatus.ACCEPTED)) - def expectedDmiBatchResourceDataUrl = "someServiceName/dmi/v1/data?requestId=requestId&topic=my-topic-name" + def expectedUrlTemplateWithVariables = new UrlTemplateParameters('myServiceName/dmi/v1/data?requestId={requestId}&topic={topic}', ['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.asynchronousPostOperationWithJsonData(DATA, expectedDmiBatchResourceDataUrl, _, READ, NO_AUTH_HEADER) >> responseFromDmi + mockDmiRestClient.asynchronousPostOperationWithJsonData(DATA, expectedUrlTemplateWithVariables, _, 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 with the expected URL and JSON request body' - 1 * mockDmiRestClient.asynchronousPostOperationWithJsonData(DATA, expectedDmiBatchResourceDataUrl, expectedBatchRequestAsJson, READ, NO_AUTH_HEADER) + 1 * mockDmiRestClient.asynchronousPostOperationWithJsonData(DATA, expectedUrlTemplateWithVariables, expectedBatchRequestAsJson, READ, NO_AUTH_HEADER) } def 'Execute (async) data operation from DMI service with Exception.'() { @@ -118,7 +118,7 @@ class DmiDataOperationsSpec extends DmiOperationsBaseSpec { mockYangModelCmHandleCollectionRetrieval([yangModelCmHandleProperty]) def dataOperationBatchRequestJsonData = TestUtils.getResourceFileContent('dataOperationRequest.json') def dataOperationRequest = spiedJsonObjectMapper.convertJsonString(dataOperationBatchRequestJsonData, DataOperationRequest.class) - dataOperationRequest.dataOperationDefinitions[0].cmHandleIds = [DmiOperationsBaseSpec.cmHandleId] + dataOperationRequest.dataOperationDefinitions[0].cmHandleIds = [cmHandleId] and: 'the published cloud event will be captured' def actualDataOperationCloudEvent = null eventsPublisher.publishCloudEvent('my-topic-name', 'my-request-id', _) >> { args -> actualDataOperationCloudEvent = args[2] } @@ -140,11 +140,11 @@ class DmiDataOperationsSpec extends DmiOperationsBaseSpec { 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=/" + def expectedTemplateWithVariables = new UrlTemplateParameters('myServiceName/dmi/v1/ch/{cmHandleId}/data/ds/{datastore}?resourceIdentifier={resourceIdentifier}', ['resourceIdentifier': '/', 'datastore': 'ncmp-datastore:passthrough-operational', 'cmHandleId': cmHandleId]) def expectedJson = '{"operation":"read","cmHandleProperties":{"prop1":"val1"},"moduleSetTag":"my-module-set-tag"}' - mockDmiRestClient.synchronousPostOperationWithJsonData(DATA, expectedUrl, expectedJson, READ, null) >> responseFromDmi + mockDmiRestClient.synchronousPostOperationWithJsonData(DATA, expectedTemplateWithVariables, expectedJson, READ, null) >> responseFromDmi when: 'get resource data is invoked' - def result = objectUnderTest.getAllResourceDataFromDmi(DmiOperationsBaseSpec.cmHandleId, NO_REQUEST_ID) + def result = objectUnderTest.getAllResourceDataFromDmi(cmHandleId, NO_REQUEST_ID) then: 'the result is the response from the DMI service' assert result == responseFromDmi } @@ -153,12 +153,12 @@ 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=${DmiOperationsBaseSpec.resourceIdentifier}" + def expectedUrlTemplateParameters = new UrlTemplateParameters('myServiceName/dmi/v1/ch/{cmHandleId}/data/ds/{datastore}?resourceIdentifier={resourceIdentifier}', ['resourceIdentifier': resourceIdentifier, 'datastore': 'ncmp-datastore:passthrough-running', 'cmHandleId': cmHandleId]) def expectedJson = '{"operation":"' + expectedOperationInUrl + '","dataType":"some data type","data":"requestData","cmHandleProperties":{"prop1":"val1"},"moduleSetTag":""}' def responseFromDmi = new ResponseEntity<Object>(HttpStatus.OK) - mockDmiRestClient.synchronousPostOperationWithJsonData(DATA, expectedUrl, expectedJson, operation, NO_AUTH_HEADER) >> responseFromDmi + mockDmiRestClient.synchronousPostOperationWithJsonData(DATA, expectedUrlTemplateParameters, expectedJson, operation, NO_AUTH_HEADER) >> responseFromDmi when: 'write resource method is invoked' - def result = objectUnderTest.writeResourceDataPassThroughRunningFromDmi(DmiOperationsBaseSpec.cmHandleId, 'parent/child', operation, 'requestData', 'some data type', NO_AUTH_HEADER) + 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' assert result == responseFromDmi where: 'the following operation is performed' @@ -168,7 +168,7 @@ class DmiDataOperationsSpec extends DmiOperationsBaseSpec { } def 'State Ready validation'() { - given: ' a yang model cm handle' + given: 'a yang model cm handle' populateYangModelCmHandle([] ,'') when: 'Validating State of #cmHandleState' def caughtException = null @@ -177,13 +177,13 @@ class DmiDataOperationsSpec extends DmiOperationsBaseSpec { } catch (Exception e) { caughtException = e } - then: 'only when not ready a exception is thrown' + 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' + where: 'the following states are used' cmHandleState || expecteException CmHandleState.READY || false CmHandleState.ADVISED || true @@ -192,4 +192,11 @@ class DmiDataOperationsSpec extends DmiOperationsBaseSpec { def extractDataValue(actualDataOperationCloudEvent) { return toTargetEvent(actualDataOperationCloudEvent, DataOperationEvent).data.responses[0] } + + def getExpectedUrlTemplateWithVariables(expectedOptions, expectedDataStore) { + def includeOptions = !(expectedOptions == null || expectedOptions.trim().isEmpty()) + def expectedUrlTemplate = 'myServiceName/dmi/v1/ch/{cmHandleId}/data/ds/{datastore}?resourceIdentifier={resourceIdentifier}' + (includeOptions ? '&options={options}' : '') + def urlVariables = ['resourceIdentifier': resourceIdentifier, 'datastore': expectedDataStore.datastoreName, 'cmHandleId': cmHandleId] + (includeOptions ? ['options': expectedOptions] : [:]) + return new UrlTemplateParameters(expectedUrlTemplate, urlVariables) + } } diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/sync/DmiModelOperationsSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/sync/DmiModelOperationsSpec.groovy index bae87d94c8..1268bc7683 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/sync/DmiModelOperationsSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/sync/DmiModelOperationsSpec.groovy @@ -27,6 +27,7 @@ import org.onap.cps.ncmp.api.impl.config.DmiProperties import org.onap.cps.ncmp.impl.DmiOperationsBaseSpec import org.onap.cps.spi.model.ModuleReference import org.onap.cps.utils.JsonObjectMapper +import org.onap.cps.ncmp.api.impl.utils.url.builder.UrlTemplateParameters import org.spockframework.spring.SpringBean import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest @@ -42,6 +43,9 @@ import static org.onap.cps.ncmp.impl.models.RequiredDmiService.MODEL @ContextConfiguration(classes = [DmiProperties, DmiModelOperations]) class DmiModelOperationsSpec extends DmiOperationsBaseSpec { + def expectedModulesUrlTemplateWithVariables = new UrlTemplateParameters('myServiceName/dmi/v1/ch/{cmHandleId}/modules', ['cmHandleId': cmHandleId]) + def expectedModuleResourcesUrlTemplateWithVariables = new UrlTemplateParameters('myServiceName/dmi/v1/ch/{cmHandleId}/moduleResources', ['cmHandleId': cmHandleId]) + @Shared def newModuleReferences = [new ModuleReference('mod1','A'), new ModuleReference('mod2','X')] @@ -58,9 +62,8 @@ class DmiModelOperationsSpec extends DmiOperationsBaseSpec { mockYangModelCmHandleRetrieval([]) and: 'a positive response from DMI service when it is called with the expected parameters' def moduleReferencesAsLisOfMaps = [[moduleName: 'mod1', revision: 'A'], [moduleName: 'mod2', revision: 'X']] - def expectedUrl = "${DmiOperationsBaseSpec.dmiServiceName}/dmi/v1/ch/${DmiOperationsBaseSpec.cmHandleId}/modules" def responseFromDmi = new ResponseEntity([schemas: moduleReferencesAsLisOfMaps], HttpStatus.OK) - mockDmiRestClient.synchronousPostOperationWithJsonData(MODEL, expectedUrl, '{"cmHandleProperties":{},"moduleSetTag":""}', READ, NO_AUTH_HEADER) >> responseFromDmi + mockDmiRestClient.synchronousPostOperationWithJsonData(MODEL, expectedModulesUrlTemplateWithVariables, '{"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' @@ -91,7 +94,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.synchronousPostOperationWithJsonData(MODEL, "${DmiOperationsBaseSpec.dmiServiceName}/dmi/v1/ch/${DmiOperationsBaseSpec.cmHandleId}/modules", + mockDmiRestClient.synchronousPostOperationWithJsonData(MODEL, expectedModulesUrlTemplateWithVariables, '{"cmHandleProperties":' + expectedAdditionalPropertiesInRequest + ',"moduleSetTag":""}', READ, NO_AUTH_HEADER) >> responseFromDmi when: 'a get module references is called' def result = objectUnderTest.getModuleReferences(yangModelCmHandle) @@ -110,7 +113,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.synchronousPostOperationWithJsonData(MODEL, "${DmiOperationsBaseSpec.dmiServiceName}/dmi/v1/ch/${DmiOperationsBaseSpec.cmHandleId}/moduleResources", + mockDmiRestClient.synchronousPostOperationWithJsonData(MODEL, expectedModuleResourcesUrlTemplateWithVariables, '{"data":{"modules":[' + expectedModuleReferencesInRequest + ']},"cmHandleProperties":{}}', READ, NO_AUTH_HEADER) >> responseFromDmi when: 'get new yang resources from DMI service' def result = objectUnderTest.getNewYangResourcesFromDmi(yangModelCmHandle, newModuleReferences) @@ -142,7 +145,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.synchronousPostOperationWithJsonData(MODEL, "${DmiOperationsBaseSpec.dmiServiceName}/dmi/v1/ch/${DmiOperationsBaseSpec.cmHandleId}/moduleResources", + mockDmiRestClient.synchronousPostOperationWithJsonData(MODEL, expectedModuleResourcesUrlTemplateWithVariables, '{"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' @@ -160,9 +163,8 @@ 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.synchronousPostOperationWithJsonData(MODEL, "${DmiOperationsBaseSpec.dmiServiceName}/dmi/v1/ch/${DmiOperationsBaseSpec.cmHandleId}/moduleResources", - '{' + expectedModuleSetTagInRequest + '"data":{"modules":[{"name":"mod1","revision":"A"},{"name":"mod2","revision":"X"}]},"cmHandleProperties":{}}', - READ, NO_AUTH_HEADER) >> responseFromDmi + mockDmiRestClient.synchronousPostOperationWithJsonData(MODEL, expectedModuleResourcesUrlTemplateWithVariables, + '{' + 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' @@ -205,5 +207,4 @@ class DmiModelOperationsSpec extends DmiOperationsBaseSpec { and: 'the message indicates a parsing error' exceptionThrown.message.toLowerCase().contains('parsing error') } - } diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/trustlevel/DmiPluginTrustLevelWatchDogSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/trustlevel/DmiPluginTrustLevelWatchDogSpec.groovy index 7fa8b2cce9..f975d23ba2 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/trustlevel/DmiPluginTrustLevelWatchDogSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/trustlevel/DmiPluginTrustLevelWatchDogSpec.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. @@ -21,8 +21,10 @@ package org.onap.cps.ncmp.impl.inventory.trustlevel import org.onap.cps.ncmp.api.impl.client.DmiRestClient +import org.onap.cps.ncmp.api.impl.utils.url.builder.UrlTemplateParameters import org.onap.cps.ncmp.api.inventory.models.TrustLevel import org.onap.cps.ncmp.impl.inventory.CmHandleQueryService +import reactor.core.publisher.Mono import spock.lang.Specification class DmiPluginTrustLevelWatchDogSpec extends Specification { @@ -39,7 +41,8 @@ class DmiPluginTrustLevelWatchDogSpec extends Specification { given: 'the cache has been initialised and "knows" about dmi-1' trustLevelPerDmiPlugin.put('dmi-1', dmiOldTrustLevel) and: 'dmi client returns health status #dmiHealhStatus' - mockDmiRestClient.getDmiHealthStatus('dmi-1') >> dmiHealhStatus + def urlTemplateParameters = new UrlTemplateParameters('dmi-1/actuator/health', [:]) + mockDmiRestClient.getDmiHealthStatus(urlTemplateParameters) >> Mono.just(dmiHealhStatus) when: 'dmi watch dog method runs' objectUnderTest.checkDmiAvailability() then: 'the update delegated to manager' @@ -52,5 +55,4 @@ class DmiPluginTrustLevelWatchDogSpec extends Specification { 'UP' | TrustLevel.NONE | TrustLevel.COMPLETE || 1 '' | TrustLevel.COMPLETE | TrustLevel.NONE || 1 } - } |