Replaced RestTemplate with WebClient in synchronous DMI calls 79/137879/6
authorsourabh_sourabh <sourabh.sourabh@est.tech>
Tue, 7 May 2024 11:31:44 +0000 (12:31 +0100)
committersourabh_sourabh <sourabh.sourabh@est.tech>
Fri, 17 May 2024 11:23:36 +0000 (12:23 +0100)
    - added DmiWebClientConfiguration
    - use WebClient in DmiRestClient
    - fixed unit tests
    - encode query params for DMI request
    - added configurable buffer size
    - Re-used ncmp.dmi.httpclient.maximumConnectionsTotal parameter
(as documented in RTD) to control webclient connection poolsize

Issue-ID: CPS-2173
Change-Id: I21584563034d58e8ae3ff3cbcf172e0d14b408fb
Signed-off-by: sourabh_sourabh <sourabh.sourabh@est.tech>
23 files changed:
cps-application/src/main/resources/application.yml
cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/exceptions/NetworkCmProxyRestExceptionHandler.java
cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/exceptions/NetworkCmProxyRestExceptionHandlerSpec.groovy
cps-ncmp-service/pom.xml
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/client/DmiRestClient.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/DmiWebClientConfiguration.java [new file with mode: 0644]
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/NcmpConfiguration.java [deleted file]
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/exception/DmiClientRequestException.java [new file with mode: 0644]
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/exception/InvalidDmiResourceUrlException.java [moved from cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/exception/HttpClientRequestException.java with 60% similarity]
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiDataOperations.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiModelOperations.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/operations/DmiOperations.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/DmiServiceUrlBuilder.java
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/client/DmiRestClientSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/DmiWebClientConfigurationSpec.groovy [new file with mode: 0644]
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/NcmpConfigurationSpec.groovy [deleted file]
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiDataOperationsSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiModelOperationsSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/operations/DmiOperationsBaseSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/DmiServiceUrlBuilderSpec.groovy
cps-ncmp-service/src/test/resources/application.yml
integration-test/src/test/groovy/org/onap/cps/integration/functional/NcmpRestApiSpec.groovy
integration-test/src/test/resources/application.yml

index 8100680..7683f0b 100644 (file)
@@ -178,6 +178,7 @@ ncmp:
             maximumConnectionsPerRoute: 50
             maximumConnectionsTotal: 100
             idleConnectionEvictionThresholdInSeconds: 5
+            maximumInMemorySizeInMegabytes: 16
         auth:
             username: ${DMI_USERNAME:cpsuser}
             password: ${DMI_PASSWORD:cpsr0cks!}
index d323691..7263000 100755 (executable)
@@ -23,9 +23,10 @@ package org.onap.cps.ncmp.rest.exceptions;
 import lombok.AccessLevel;
 import lombok.NoArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+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.HttpClientRequestException;
 import org.onap.cps.ncmp.api.impl.exception.InvalidDatastoreException;
+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.rest.controller.NetworkCmProxyController;
@@ -69,14 +70,15 @@ public class NetworkCmProxyRestExceptionHandler {
         return buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, exception);
     }
 
-    @ExceptionHandler({HttpClientRequestException.class})
+    @ExceptionHandler({DmiClientRequestException.class})
     public static ResponseEntity<Object> handleClientRequestExceptions(
-            final HttpClientRequestException httpClientRequestException) {
-        return wrapDmiErrorResponse(httpClientRequestException);
+            final DmiClientRequestException dmiClientRequestException) {
+        return wrapDmiErrorResponse(dmiClientRequestException);
     }
 
     @ExceptionHandler({DmiRequestException.class, DataValidationException.class, OperationNotSupportedException.class,
-            HttpMessageNotReadableException.class, InvalidTopicException.class, InvalidDatastoreException.class})
+            HttpMessageNotReadableException.class, InvalidTopicException.class, InvalidDatastoreException.class,
+            InvalidDmiResourceUrlException.class})
     public static ResponseEntity<Object> handleDmiRequestExceptions(final Exception exception) {
         return buildErrorResponse(HttpStatus.BAD_REQUEST, exception);
     }
@@ -115,13 +117,13 @@ public class NetworkCmProxyRestExceptionHandler {
         return new ResponseEntity<>(errorMessage, status);
     }
 
-    private static ResponseEntity<Object> wrapDmiErrorResponse(final HttpClientRequestException
-                                                                     httpClientRequestException) {
+    private static ResponseEntity<Object> wrapDmiErrorResponse(final DmiClientRequestException
+                                                                       dmiClientRequestException) {
         final var dmiErrorMessage = new DmiErrorMessage();
         final var dmiErrorResponse = new DmiErrorMessageDmiResponse();
-        dmiErrorResponse.setHttpCode(httpClientRequestException.getHttpStatus());
-        dmiErrorResponse.setBody(httpClientRequestException.getDetails());
-        dmiErrorMessage.setMessage(httpClientRequestException.getMessage());
+        dmiErrorResponse.setHttpCode(dmiClientRequestException.getHttpStatusCode());
+        dmiErrorResponse.setBody(dmiClientRequestException.getResponseBodyAsString());
+        dmiErrorMessage.setMessage(dmiClientRequestException.getMessage());
         dmiErrorMessage.setDmiResponse(dmiErrorResponse);
         return new ResponseEntity<>(dmiErrorMessage, HttpStatus.BAD_GATEWAY);
     }
index a79ea25..ea472cd 100644 (file)
@@ -31,13 +31,14 @@ import static org.onap.cps.ncmp.rest.exceptions.NetworkCmProxyRestExceptionHandl
 import static org.onap.cps.ncmp.rest.exceptions.NetworkCmProxyRestExceptionHandlerSpec.ApiType.NCMPINVENTORY
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
+import static org.onap.cps.ncmp.api.NcmpResponseStatus.UNABLE_TO_READ_RESOURCE_DATA
 
 import groovy.json.JsonSlurper
 import org.mapstruct.factory.Mappers
 import org.onap.cps.TestUtils
 import org.onap.cps.ncmp.api.NetworkCmProxyDataService
 import org.onap.cps.ncmp.api.impl.exception.DmiRequestException
-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.ServerNcmpException
 import org.onap.cps.ncmp.rest.controller.NcmpRestInputMapper
 import org.onap.cps.ncmp.rest.controller.handlers.NcmpCachedResourceRequestHandler
@@ -143,7 +144,7 @@ class NetworkCmProxyRestExceptionHandlerSpec extends Specification {
 
     def 'Failing DMI Request - passthrough scenario'() {
         given: 'failing DMI request'
-            setupTestException(new HttpClientRequestException('Error Message Details NCMP', 'Bad Request from DMI', 400), NCMP)
+            setupTestException(new DmiClientRequestException(400, 'Error Message Details NCMP', 'Bad Request from DMI', UNABLE_TO_READ_RESOURCE_DATA), NCMP)
         when: 'the DMI request is executed'
             def response = performTestRequest(NCMP)
         then: 'NCMP service responds with 502 Bad Gateway status'
index fc41da3..77e13db 100644 (file)
@@ -70,8 +70,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>
index 798a280..97ae677 100644 (file)
@@ -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");
 
 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 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.DmiWebClientConfiguration.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.utils.JsonObjectMapper;
 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.WebClientResponseException;
 
 @Component
 @RequiredArgsConstructor
@@ -43,8 +54,10 @@ 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 WebClient webClient;
     private final DmiProperties dmiProperties;
+    private final JsonObjectMapper jsonObjectMapper;
 
     /**
      * Sends POST operation to DMI with json body containing module references.
@@ -59,14 +72,16 @@ public class DmiRestClient {
                                                             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 ResponseEntity.ok(webClient.post().uri(toUri(dmiResourceUrl))
+                    .headers(httpHeaders -> configureHttpHeaders(httpHeaders, authorization))
+                    .body(BodyInserters.fromValue(requestBodyAsJsonString))
+                    .retrieve()
+                    .bodyToMono(Object.class)
+                    .onErrorMap(httpError -> handleDmiClientException(httpError, operationType.getOperationName()))
+                    .block());
+        } catch (final HttpServerErrorException e) {
+            throw handleDmiClientException(e, operationType.getOperationName());
         }
     }
 
@@ -77,13 +92,14 @@ public class DmiRestClient {
      * @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));
         try {
-            final JsonNode responseHealthStatus =
-                restTemplate.getForObject(dmiPluginBaseUrl + HEALTH_CHECK_URL_EXTENSION,
-                    JsonNode.class, httpHeaders);
+            final JsonNode responseHealthStatus = webClient.get()
+                    .uri(toUri(dmiPluginBaseUrl + HEALTH_CHECK_URL_EXTENSION))
+                    .headers(httpHeaders -> configureHttpHeaders(httpHeaders, NO_AUTHORIZATION))
+                    .retrieve()
+                    .bodyToMono(JsonNode.class).block();
             return responseHealthStatus == null ? NOT_SPECIFIED :
-                responseHealthStatus.get("status").asText();
+                    responseHealthStatus.get("status").asText();
         } catch (final Exception e) {
             log.warn("Failed to retrieve health status from {}. Error Message: {}", dmiPluginBaseUrl, e.getMessage());
             return NOT_SPECIFIED;
@@ -96,7 +112,33 @@ public class DmiRestClient {
         } 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 e, final String operationType) {
+        final String exceptionMessage = "Unable to " + operationType + " resource data.";
+        if (e instanceof WebClientResponseException wcre) {
+            if (wcre.getStatusCode().isSameCodeAs(HttpStatus.REQUEST_TIMEOUT)) {
+                throw new DmiClientRequestException(wcre.getStatusCode().value(), wcre.getMessage(),
+                        jsonObjectMapper.asJsonString(wcre.getResponseBodyAsString()), DMI_SERVICE_NOT_RESPONDING);
+            }
+            throw new DmiClientRequestException(wcre.getStatusCode().value(), wcre.getMessage(),
+                    jsonObjectMapper.asJsonString(wcre.getResponseBodyAsString()), UNABLE_TO_READ_RESOURCE_DATA);
+
+        }
+        if (e instanceof HttpServerErrorException httpServerErrorException) {
+            throw new DmiClientRequestException(httpServerErrorException.getStatusCode().value(), exceptionMessage,
+                    httpServerErrorException.getResponseBodyAsString(), DMI_SERVICE_NOT_RESPONDING);
+        }
+        throw new DmiClientRequestException(INTERNAL_SERVER_ERROR.value(), exceptionMessage, e.getMessage(),
+                UNKNOWN_ERROR);
+    }
 }
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 (file)
index 0000000..2127166
--- /dev/null
@@ -0,0 +1,94 @@
+/*
+ * ============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.Getter;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
+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.stereotype.Component;
+import org.springframework.web.reactive.function.client.WebClient;
+import reactor.netty.http.client.HttpClient;
+import reactor.netty.resources.ConnectionProvider;
+
+@Configuration
+@RequiredArgsConstructor
+public class DmiWebClientConfiguration {
+
+    @Value("${ncmp.dmi.httpclient.connectionTimeoutInSeconds:20000}")
+    private Integer connectionTimeoutInSeconds;
+
+    @Value("${ncmp.dmi.httpclient.maximumInMemorySizeInMegabytes:1}")
+    private Integer maximumInMemorySizeInMegabytes;
+
+    @Value("${ncmp.dmi.httpclient.maximumConnectionsTotal:100}")
+    private Integer maximumConnectionsTotal;
+
+    @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;
+    }
+
+    /**
+     * Configures and create 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.
+     *
+     * @return a WebClient instance.
+     */
+    @Bean
+    public WebClient webClient() {
+
+        final ConnectionProvider dmiWebClientConnectionProvider
+                = ConnectionProvider.create("dmiWebClientConnectionPool", maximumConnectionsTotal);
+
+        final HttpClient httpClient = HttpClient.create(dmiWebClientConnectionProvider)
+                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectionTimeoutInSeconds * 1000)
+                .doOnConnected(connection ->
+                        connection
+                                .addHandlerLast(new ReadTimeoutHandler(connectionTimeoutInSeconds, TimeUnit.SECONDS))
+                                .addHandlerLast(new WriteTimeoutHandler(connectionTimeoutInSeconds, TimeUnit.SECONDS)));
+        httpClient.warmup().block();
+        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/NcmpConfiguration.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/NcmpConfiguration.java
deleted file mode 100644 (file)
index c6ff116..0000000
+++ /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/exception/DmiClientRequestException.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/exception/DmiClientRequestException.java
new file mode 100644 (file)
index 0000000..ab0fa68
--- /dev/null
@@ -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;
+    }
+}
@@ -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;
     }
 }
index a9ec124..20b8916 100644 (file)
@@ -21,8 +21,6 @@
 
 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;
 
@@ -35,8 +33,8 @@ import java.util.stream.Collectors;
 import lombok.extern.slf4j.Slf4j;
 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.DmiWebClientConfiguration.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;
@@ -61,7 +59,7 @@ public class DmiDataOperations extends DmiOperations {
 
     public DmiDataOperations(final InventoryPersistence inventoryPersistence,
                              final JsonObjectMapper jsonObjectMapper,
-                             final NcmpConfiguration.DmiProperties dmiProperties,
+                             final DmiProperties dmiProperties,
                              final DmiRestClient dmiRestClient,
                              final DmiServiceUrlBuilder dmiServiceUrlBuilder) {
         super(inventoryPersistence, jsonObjectMapper, dmiProperties, dmiRestClient, dmiServiceUrlBuilder);
@@ -226,7 +224,7 @@ 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());
@@ -235,7 +233,7 @@ public class DmiDataOperations extends DmiOperations {
     private void buildDataOperationRequestUrlAndSendToDmiService(final String topicParamInQuery,
                                                                  final String requestId,
                                                                  final Map<String, List<DmiDataOperation>>
-                                                                groupsOutPerDmiServiceName,
+                                                                         groupsOutPerDmiServiceName,
                                                                  final String authorization) {
 
         groupsOutPerDmiServiceName.forEach((dmiServiceName, dmiDataOperationRequestBodies) -> {
@@ -256,36 +254,29 @@ public class DmiDataOperations extends DmiOperations {
         try {
             dmiRestClient.postOperationWithJsonData(dataOperationResourceUrl, dmiDataOperationRequestAsJsonString, READ,
                     authorization);
-        } catch (final Exception exception) {
-            handleTaskCompletionException(exception, dataOperationResourceUrl, dmiDataOperationRequestBodies);
+        } catch (final DmiClientRequestException e) {
+            handleTaskCompletionException(e, dataOperationResourceUrl, dmiDataOperationRequestBodies);
         }
     }
 
-    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(CmHandle::getId).toList();
+            cmHandleIdsPerResponseCodesPerOperation.add(dmiDataOperationRequestBody,
+                    Map.of(dmiClientRequestException.getNcmpResponseStatus(), cmHandleIds));
+        });
+        ResourceDataOperationRequestUtils.publishErrorMessageToClientTopic(topicName, requestId,
+                cmHandleIdsPerResponseCodesPerOperation);
     }
 }
index 798f6de..2a9248a 100644 (file)
@@ -34,7 +34,7 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 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.config.DmiWebClientConfiguration.DmiProperties;
 import org.onap.cps.ncmp.api.impl.inventory.InventoryPersistence;
 import org.onap.cps.ncmp.api.impl.utils.DmiServiceUrlBuilder;
 import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle;
@@ -57,7 +57,7 @@ public class DmiModelOperations extends DmiOperations {
      */
     public DmiModelOperations(final InventoryPersistence inventoryPersistence,
                               final JsonObjectMapper jsonObjectMapper,
-                              final NcmpConfiguration.DmiProperties dmiProperties,
+                              final DmiProperties dmiProperties,
                               final DmiRestClient dmiRestClient, final DmiServiceUrlBuilder dmiServiceUrlBuilder) {
         super(inventoryPersistence, jsonObjectMapper, dmiProperties, dmiRestClient, dmiServiceUrlBuilder);
     }
index c8d73ea..f4166d4 100644 (file)
@@ -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");
@@ -23,7 +23,7 @@ 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.config.DmiWebClientConfiguration.DmiProperties;
 import org.onap.cps.ncmp.api.impl.inventory.InventoryPersistence;
 import org.onap.cps.ncmp.api.impl.utils.DmiServiceUrlBuilder;
 import org.onap.cps.utils.JsonObjectMapper;
@@ -35,7 +35,7 @@ public class DmiOperations {
 
     protected final InventoryPersistence inventoryPersistence;
     protected final JsonObjectMapper jsonObjectMapper;
-    protected final NcmpConfiguration.DmiProperties dmiProperties;
+    protected final DmiProperties dmiProperties;
     protected final DmiRestClient dmiRestClient;
     protected final DmiServiceUrlBuilder dmiServiceUrlBuilder;
 
@@ -44,6 +44,4 @@ public class DmiOperations {
                 .pathSegment("{resourceName}")
                 .buildAndExpand(dmiServiceName, dmiProperties.getDmiBasePath(), cmHandle, resourceName).toUriString();
     }
-
-
 }
index 04acaa5..e0c9568 100644 (file)
 
 package org.onap.cps.ncmp.api.impl.utils;
 
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
 import java.util.HashMap;
 import java.util.Map;
 import lombok.RequiredArgsConstructor;
 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.ncmp.api.impl.config.DmiWebClientConfiguration.DmiProperties;
 import org.onap.cps.spi.utils.CpsValidator;
 import org.springframework.stereotype.Component;
 import org.springframework.util.LinkedMultiValueMap;
@@ -35,8 +37,7 @@ import org.springframework.web.util.UriComponentsBuilder;
 @Component
 @RequiredArgsConstructor
 public class DmiServiceUrlBuilder {
-
-    private final NcmpConfiguration.DmiProperties dmiProperties;
+    private final DmiProperties dmiProperties;
     private final CpsValidator cpsValidator;
 
     /**
@@ -141,8 +142,7 @@ public class DmiServiceUrlBuilder {
                                                              final String optionsParamInQuery,
                                                              final String topicParamInQuery) {
         final MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
-        getQueryParamConsumer().accept("resourceIdentifier",
-                resourceId, queryParams);
+        getQueryParamConsumer().accept("resourceIdentifier", resourceId, queryParams);
         getQueryParamConsumer().accept("options", optionsParamInQuery, queryParams);
         if (Strings.isNotEmpty(topicParamInQuery)) {
             getQueryParamConsumer().accept("topic", topicParamInQuery, queryParams);
@@ -168,7 +168,7 @@ public class DmiServiceUrlBuilder {
     private TriConsumer<String, String, MultiValueMap<String, String>> getQueryParamConsumer() {
         return (paramName, paramValue, paramMap) -> {
             if (Strings.isNotEmpty(paramValue)) {
-                paramMap.add(paramName, paramValue);
+                paramMap.add(paramName, URLEncoder.encode(paramValue, StandardCharsets.UTF_8));
             }
         };
     }
index c8e34b1..547d08a 100644 (file)
@@ -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");
 
 package org.onap.cps.ncmp.api.impl.client
 
+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
+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.config.DmiWebClientConfiguration
+import org.onap.cps.ncmp.api.impl.exception.InvalidDmiResourceUrlException
+import org.onap.cps.ncmp.api.impl.exception.DmiClientRequestException
 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.boot.test.context.SpringBootTest
-import org.springframework.http.HttpEntity
 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 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
+import org.springframework.web.reactive.function.client.WebClientResponseException
 
 @SpringBootTest
-@ContextConfiguration(classes = [DmiProperties, DmiRestClient, ObjectMapper])
+@ContextConfiguration(classes = [DmiWebClientConfiguration, DmiRestClient, ObjectMapper])
 class DmiRestClientSpec extends Specification {
 
     static final NO_AUTH_HEADER = null
     static final BASIC_AUTH_HEADER = 'Basic c29tZS11c2VyOnNvbWUtcGFzc3dvcmQ='
     static final BEARER_AUTH_HEADER = 'Bearer my-bearer-token'
 
-    @SpringBean
-    RestTemplate mockRestTemplate = Mock(RestTemplate)
-
     @Autowired
-    NcmpConfiguration.DmiProperties dmiProperties
+    DmiWebClientConfiguration.DmiProperties dmiProperties
 
     @Autowired
     DmiRestClient objectUnderTest
 
-    @Autowired
-    ObjectMapper objectMapper
+    @SpringBean
+    WebClient mockWebClient = Mock(WebClient);
 
-    def responseFromRestTemplate = Mock(ResponseEntity)
+    @SpringBean
+    JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
+
+    def mockRequestBodyUriSpec = Mock(WebClient.RequestBodyUriSpec)
+    def mockResponseSpec = Mock(WebClient.ResponseSpec)
+    def mockResponseEntity = Mock(ResponseEntity)
+
+    def setup() {
+        mockRequestBodyUriSpec.uri(_) >> mockRequestBodyUriSpec
+        mockRequestBodyUriSpec.headers(_) >> mockRequestBodyUriSpec
+        mockRequestBodyUriSpec.retrieve() >> mockResponseSpec
+    }
 
     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
+        given: 'the web client returns a valid response entity for the expected parameters'
+            mockWebClient.post() >> mockRequestBodyUriSpec
+            mockRequestBodyUriSpec.body(_) >> mockRequestBodyUriSpec
+            def monoSpec = Mono.just(mockResponseEntity)
+            mockResponseSpec.bodyToMono(Object.class) >>  monoSpec
+            monoSpec.block() >> mockResponseEntity
         when: 'POST operation is invoked'
-            def result = objectUnderTest.postOperationWithJsonData('my url', 'some json', READ, null)
+            def response = objectUnderTest.postOperationWithJsonData('/my/url', 'some json', READ, null)
         then: 'the output of the method is equal to the output from the test template'
-            result == responseFromRestTemplate
+            assert response.statusCode.value() == 200
+            assert response.hasBody()
     }
 
-    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'
+            mockWebClient.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('/some', 'some json', READ, null)
+        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.code == '102'
+            assert 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('/invalid dmi url', null, null, null)
+        then: 'invalid dmi resource url exception is thrown'
+            def thrown = thrown(InvalidDmiResourceUrlException)
         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'
+            assert thrown.httpStatus == 400
+            assert 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'
+            mockWebClient.post() >> mockRequestBodyUriSpec
+            mockRequestBodyUriSpec.body(_) >> mockRequestBodyUriSpec
+            def monoSpec = Mono.error(new WebClientResponseException('message', httpStatusCode, null, null, null, null))
+            mockResponseSpec.bodyToMono(Object.class) >>  monoSpec
+        when: 'POST operation is invoked'
+            objectUnderTest.postOperationWithJsonData('/my/url', 'some json', READ, null)
+        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.code == expectedNcmpResponseStatusCode
+            assert thrown.httpStatusCode == httpStatusCode
+        where: 'the following errors occur'
+            scenario              | httpStatusCode | expectedNcmpResponseStatusCode
+            'dmi request timeout' | 408            | DMI_SERVICE_NOT_RESPONDING.code
+            'other error code'    | 500            | UNABLE_TO_READ_RESOURCE_DATA.code
+    }
+
     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}
+            def monoResponse = Mono.just(jsonNode)
+            mockWebClient.get() >> mockRequestBodyUriSpec
+            mockResponseSpec.bodyToMono(_) >> monoResponse
+            monoResponse.block() >> jsonNode
         when: 'get trust level of the dmi plugin'
-            def result = objectUnderTest.getDmiHealthStatus('some url')
+            def result = objectUnderTest.getDmiHealthStatus('some/url')
         then: 'the status value from the json is return'
             assert result == 'my status'
     }
 
     def 'Failing to get dmi plugin health status #scenario'() {
         given: 'rest template with #scenario'
-            mockRestTemplate.getForObject(*_) >> healthStatusResponse
+            mockWebClient.get() >> healthStatusResponse
         when: 'attempt to get health status of the dmi plugin'
             def result = objectUnderTest.getDmiHealthStatus('some url')
         then: 'result will be empty'
@@ -132,5 +178,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
     }
-
 }
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 (file)
index 0000000..6a73089
--- /dev/null
@@ -0,0 +1,64 @@
+/*-
+ * ============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.beans.factory.annotation.Autowired
+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 = [DmiWebClientConfiguration.DmiProperties])
+@TestPropertySource(properties = ['ncmp.dmi.httpclient.connectionTimeoutInSeconds=1', 'ncmp.dmi.httpclient.maximumInMemorySizeInMegabytes=1'])
+class DmiWebClientConfigurationSpec extends Specification {
+
+    @Autowired
+    DmiWebClientConfiguration.DmiProperties dmiProperties
+
+    def objectUnderTest = new DmiWebClientConfiguration()
+
+    def setup() {
+        objectUnderTest.connectionTimeoutInSeconds = 10
+        objectUnderTest.maximumInMemorySizeInMegabytes = 1
+        objectUnderTest.maximumConnectionsTotal = 2
+    }
+
+    def 'DMI Properties.'() {
+        expect: 'properties are set to values in test configuration yaml file'
+            dmiProperties.authUsername == 'some-user'
+            dmiProperties.authPassword == 'some-password'
+    }
+
+    def 'Web Client Configuration construction.'() {
+        expect: 'the system can create an instance'
+            new DmiWebClientConfiguration() != null
+    }
+
+    def 'Creating a WebClient instance.'() {
+        given: 'WebClient configuration invoked'
+            def webClientInstance = objectUnderTest.webClient()
+        expect: 'the system can create an instance'
+            assert webClientInstance != null
+            assert webClientInstance instanceof WebClient
+    }
+}
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 (file)
index 74e3424..0000000
+++ /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];
-    }
-}
index eb6c7a0..312cd8a 100644 (file)
 
 package org.onap.cps.ncmp.api.impl.operations
 
+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 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.config.DmiWebClientConfiguration
+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.context.CpsApplicationContext
 import org.onap.cps.ncmp.api.models.DataOperationRequest
@@ -40,19 +48,8 @@ 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
-
 @SpringBootTest
-@ContextConfiguration(classes = [EventsPublisher, CpsApplicationContext, NcmpConfiguration.DmiProperties, DmiDataOperations])
+@ContextConfiguration(classes = [EventsPublisher, CpsApplicationContext, DmiWebClientConfiguration.DmiProperties, DmiDataOperations])
 class DmiDataOperationsSpec extends DmiOperationsBaseSpec {
 
     @SpringBean
@@ -114,26 +111,22 @@ class DmiDataOperationsSpec extends DmiOperationsBaseSpec {
             1 * mockDmiRestClient.postOperationWithJsonData(expectedDmiBatchResourceDataUrl, expectedBatchRequestAsJson, READ, NO_AUTH_HEADER)
     }
 
-    def 'Execute (async) data operation from DMI service for #scenario.'() {
+    def 'Execute (async) data operation from DMI service with dmi client exception.'() {
         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 actualDataOperationCloudEvent = null
         when: 'exception occurs after sending request to dmi service'
-            objectUnderTest.handleTaskCompletionException(new Throwable(exception), dmiDataOperationResourceDataUrl, List.of(dmiDataOperation))
+            objectUnderTest.handleTaskCompletionException(new DmiClientRequestException(123, 'message', 'details', UNABLE_TO_READ_RESOURCE_DATA), 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'
             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 == '103'
+            assert eventDataValue.statusMessage == UNABLE_TO_READ_RESOURCE_DATA.message
     }
 
     def 'call get all resource data.'() {
index 9aab467..ae9c174 100644 (file)
@@ -23,7 +23,7 @@ package org.onap.cps.ncmp.api.impl.operations
 
 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.DmiWebClientConfiguration
 import org.onap.cps.spi.model.ModuleReference
 import org.onap.cps.utils.JsonObjectMapper
 import org.spockframework.spring.SpringBean
@@ -37,7 +37,7 @@ import spock.lang.Shared
 import static org.onap.cps.ncmp.api.impl.operations.OperationType.READ
 
 @SpringBootTest
-@ContextConfiguration(classes = [NcmpConfiguration.DmiProperties, DmiModelOperations])
+@ContextConfiguration(classes = [DmiWebClientConfiguration.DmiProperties, DmiModelOperations])
 class DmiModelOperationsSpec extends DmiOperationsBaseSpec {
 
     @Shared
index 72a0f2f..042cb4a 100644 (file)
@@ -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,7 +22,7 @@ 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.config.DmiWebClientConfiguration
 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
@@ -50,7 +50,7 @@ abstract class DmiOperationsBaseSpec extends Specification {
     ObjectMapper spyObjectMapper = Spy()
 
     @SpringBean
-    DmiServiceUrlBuilder dmiServiceUrlBuilder = new DmiServiceUrlBuilder(new NcmpConfiguration.DmiProperties(), mockCpsValidator)
+    DmiServiceUrlBuilder dmiServiceUrlBuilder = new DmiServiceUrlBuilder(new DmiWebClientConfiguration.DmiProperties(), mockCpsValidator)
 
     def yangModelCmHandle = new YangModelCmHandle()
     def static dmiServiceName = 'some service name'
index fbf2c3d..65e6dda 100644 (file)
@@ -22,10 +22,10 @@ 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.config.DmiWebClientConfiguration
 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
 
@@ -34,7 +34,7 @@ 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()
+    DmiWebClientConfiguration.DmiProperties dmiProperties = new DmiWebClientConfiguration.DmiProperties()
 
     def mockCpsValidator = Mock(CpsValidator)
 
@@ -85,7 +85,7 @@ class DmiServiceUrlBuilderSpec extends Specification {
         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'
+            assert result.toString() == 'some-service/testBase/v1/data?topic=some+topic&requestId=some+id'
     }
 
     def 'Populate batch uri variables.'() {
index 574b499..eca28b9 100644 (file)
@@ -38,6 +38,7 @@ ncmp:
     dmi:
         httpclient:
             connectionTimeoutInSeconds: 180
+            maximumInMemorySizeInMegabytes: 16
         auth:
             username: some-user
             password: some-password
index 950cd65..5325f1a 100644 (file)
@@ -20,9 +20,6 @@
 
 package org.onap.cps.integration.functional
 
-import org.onap.cps.integration.base.CpsIntegrationSpecBase
-import org.springframework.http.MediaType
-import spock.util.concurrent.PollingConditions
 import static org.hamcrest.Matchers.containsInAnyOrder
 import static org.hamcrest.Matchers.hasSize
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
@@ -30,6 +27,10 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
 
+import org.onap.cps.integration.base.CpsIntegrationSpecBase
+import org.springframework.http.MediaType
+import spock.util.concurrent.PollingConditions
+
 class NcmpRestApiSpec extends CpsIntegrationSpecBase {
 
     def 'Register CM Handles using REST API.'() {
index 6fd3bca..407210f 100644 (file)
@@ -169,6 +169,7 @@ ncmp:
       maximumConnectionsPerRoute: 50
       maximumConnectionsTotal: 100
       idleConnectionEvictionThresholdInSeconds: 5
+      maximumInMemorySizeInMegabytes: 16
     auth:
       username: dmi
       password: dmi