Merge "Async: NCMP Rest impl. including Request ID generation"
authorBruno Sakoto <bruno.sakoto@bell.ca>
Fri, 11 Mar 2022 22:32:13 +0000 (22:32 +0000)
committerGerrit Code Review <gerrit@onap.org>
Fri, 11 Mar 2022 22:32:13 +0000 (22:32 +0000)
20 files changed:
cps-application/src/main/resources/application.yml
cps-ncmp-rest/docs/openapi/components.yaml
cps-ncmp-rest/docs/openapi/ncmp.yml
cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyController.java
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/controller/NetworkCmProxyControllerSpec.groovy
cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/exceptions/NetworkCmProxyRestExceptionHandlerSpec.groovy
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/NetworkCmProxyDataService.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImpl.java
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/operations/DmiRequestBody.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/DmiServiceUrlBuilder.java [new file with mode: 0644]
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplSpec.groovy
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/utils/DmiServiceUrlBuilderSpec.groovy [new file with mode: 0644]
cps-ncmp-service/src/test/resources/application.yml

index 723e2ca..4dfeee8 100644 (file)
@@ -135,4 +135,4 @@ dmi:
         username: ${DMI_USERNAME}\r
         password: ${DMI_PASSWORD}\r
     api:\r
-        base-path: /dmi\r
+        base-path: dmi\r
index 6477e34..8b02dd1 100644 (file)
@@ -329,6 +329,18 @@ components:
         sample 3:
           value:
             options: (depth=2,fields=book/authors)
+    topicParamInQuery:
+      name: topic
+      in: query
+      description: topic parameter in query.
+      required: false
+      schema:
+        type: string
+      allowReserved: true
+      examples:
+        sample 1:
+          value:
+            topic: my-topic-name
     contentParamInHeader:
       name: Content-Type
       in: header
index a267fb4..1afd7c9 100755 (executable)
@@ -29,6 +29,7 @@ getResourceDataForPassthroughOperational:
       - $ref: 'components.yaml#/components/parameters/resourceIdentifierInQuery'
       - $ref: 'components.yaml#/components/parameters/acceptParamInHeader'
       - $ref: 'components.yaml#/components/parameters/optionsParamInQuery'
+      - $ref: 'components.yaml#/components/parameters/topicParamInQuery'
     responses:
       200:
         description: OK
@@ -60,6 +61,7 @@ resourceDataForPassthroughRunning:
       - $ref: 'components.yaml#/components/parameters/resourceIdentifierInQuery'
       - $ref: 'components.yaml#/components/parameters/acceptParamInHeader'
       - $ref: 'components.yaml#/components/parameters/optionsParamInQuery'
+      - $ref: 'components.yaml#/components/parameters/topicParamInQuery'
     responses:
       200:
         description: OK
index 2a336d5..b5c8d14 100755 (executable)
@@ -76,17 +76,20 @@ public class NetworkCmProxyController implements NetworkCmProxyApi {
      * @param resourceIdentifier resource identifier
      * @param acceptParamInHeader accept header parameter
      * @param optionsParamInQuery options query parameter
+     * @param topicParamInQuery topic query parameter
      * @return {@code ResponseEntity} response from dmi plugin
      */
     @Override
     public ResponseEntity<Object> getResourceDataOperationalForCmHandle(final String cmHandle,
                                                                         final @NotNull @Valid String resourceIdentifier,
                                                                         final String acceptParamInHeader,
-                                                                        final @Valid String optionsParamInQuery) {
+                                                                        final @Valid String optionsParamInQuery,
+                                                                        final @Valid String topicParamInQuery) {
         final Object responseObject = networkCmProxyDataService.getResourceDataOperationalForCmHandle(cmHandle,
                 resourceIdentifier,
                 acceptParamInHeader,
-                optionsParamInQuery);
+                optionsParamInQuery,
+                topicParamInQuery);
         return ResponseEntity.ok(responseObject);
     }
 
@@ -97,17 +100,20 @@ public class NetworkCmProxyController implements NetworkCmProxyApi {
      * @param resourceIdentifier resource identifier
      * @param acceptParamInHeader accept header parameter
      * @param optionsParamInQuery options query parameter
+     * @param topicParamInQuery topic query parameter
      * @return {@code ResponseEntity} response from dmi plugin
      */
     @Override
     public ResponseEntity<Object> getResourceDataRunningForCmHandle(final String cmHandle,
                                                                     final @NotNull @Valid String resourceIdentifier,
                                                                     final String acceptParamInHeader,
-                                                                    final @Valid String optionsParamInQuery) {
+                                                                    final @Valid String optionsParamInQuery,
+                                                                    final @Valid String topicParamInQuery) {
         final Object responseObject = networkCmProxyDataService.getResourceDataPassThroughRunningForCmHandle(cmHandle,
                 resourceIdentifier,
                 acceptParamInHeader,
-                optionsParamInQuery);
+                optionsParamInQuery,
+                topicParamInQuery);
         return ResponseEntity.ok(responseObject);
     }
 
index 5aaf1c3..2495f36 100755 (executable)
@@ -30,6 +30,7 @@ import org.onap.cps.ncmp.rest.controller.NetworkCmProxyController;
 import org.onap.cps.ncmp.rest.controller.NetworkCmProxyInventoryController;
 import org.onap.cps.ncmp.rest.model.ErrorMessage;
 import org.onap.cps.spi.exceptions.CpsException;
+import org.onap.cps.spi.exceptions.DataNodeNotFoundException;
 import org.onap.cps.spi.exceptions.DataValidationException;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
@@ -69,6 +70,11 @@ public class NetworkCmProxyRestExceptionHandler {
         return buildErrorResponse(HttpStatus.BAD_REQUEST, exception);
     }
 
+    @ExceptionHandler({DataNodeNotFoundException.class})
+    public static ResponseEntity<Object> handleNotFoundExceptions(final CpsException exception) {
+        return buildErrorResponse(HttpStatus.NOT_FOUND, exception);
+    }
+
     private static ResponseEntity<Object> buildErrorResponse(final HttpStatus status, final Exception exception) {
         if (exception.getCause() != null || !(exception instanceof CpsException)) {
             log.error("Exception occurred", exception);
index c997714..9cbf626 100644 (file)
@@ -24,6 +24,7 @@ package org.onap.cps.ncmp.rest.controller
 
 
 import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle
+import spock.lang.Shared
 
 import static org.onap.cps.ncmp.api.impl.operations.DmiRequestBody.OperationEnum.PATCH
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete
@@ -70,7 +71,10 @@ class NetworkCmProxyControllerSpec extends Specification {
 
     def requestBody = '{"some-key":"some-value"}'
 
-    def 'Get Resource Data from pass-through operational.' () {
+    @Shared
+    def NO_TOPIC = null
+
+    def 'Get Resource Data from pass-through operational.'() {
         given: 'resource data url'
             def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-operational" +
                     "?resourceIdentifier=parent/child&options=(a=1,b=2)"
@@ -84,12 +88,40 @@ class NetworkCmProxyControllerSpec extends Specification {
             1 * mockNetworkCmProxyDataService.getResourceDataOperationalForCmHandle('testCmHandle',
                     'parent/child',
                     'application/json',
-                    '(a=1,b=2)')
+                    '(a=1,b=2)',
+                    NO_TOPIC)
+        and: 'response status is Ok'
+            response.status == HttpStatus.OK.value()
+    }
+
+    def 'Get Resource Data from pass-through operational with #scenario.'() {
+        given: 'resource data url'
+            def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-operational" +
+                    "?resourceIdentifier=parent/child&options=(a=1,b=2)${topicQueryParam}"
+        when: 'get data resource request is performed'
+            def response = mvc.perform(
+                    get(getUrl)
+                    .contentType(MediaType.APPLICATION_JSON)
+                    .accept(MediaType.APPLICATION_JSON_VALUE)
+            ).andReturn().response
+        then: 'the NCMP data service is called with operational data for cm handle'
+            1 * mockNetworkCmProxyDataService.getResourceDataOperationalForCmHandle('testCmHandle',
+                    'parent/child',
+                    'application/json',
+                    '(a=1,b=2)',
+                    expectedTopicName)
         and: 'response status is Ok'
             response.status == HttpStatus.OK.value()
+        where: 'the following parameters are used'
+            scenario               | topicQueryParam        || expectedTopicName
+            'Url with valid topic' | "&topic=my-topic-name" || "my-topic-name"
+            'No topic in url'      | ''                     || NO_TOPIC
+            'Null topic in url'    | "&topic=null"          || "null"
+            'Empty topic in url'   | "&topic=\"\""          || "\"\""
+            'Missing topic in url' | "&topic="              || ""
     }
 
-    def 'Get Resource Data from pass-through running with #scenario value in resource identifier param.' () {
+    def 'Get Resource Data from pass-through running with #scenario value in resource identifier param.'() {
         given: 'resource data url'
             def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running" +
                     "?resourceIdentifier=" + resourceIdentifier + "&options=(a=1,b=2)"
@@ -97,7 +129,8 @@ class NetworkCmProxyControllerSpec extends Specification {
             mockNetworkCmProxyDataService.getResourceDataPassThroughRunningForCmHandle('testCmHandle',
                     resourceIdentifier,
                     'application/json',
-                    '(a=1,b=2)') >> '{valid-json}'
+                    '(a=1,b=2)',
+                    NO_TOPIC) >> '{valid-json}'
         when: 'get data resource request is performed'
             def response = mvc.perform(
                     get(getUrl)
index 8004328..260ffd2 100644 (file)
@@ -45,6 +45,7 @@ import static org.onap.cps.ncmp.rest.exceptions.NetworkCmProxyRestExceptionHandl
 import static org.onap.cps.ncmp.rest.exceptions.NetworkCmProxyRestExceptionHandlerSpec.ApiType.NCMPINVENTORY
 import static org.springframework.http.HttpStatus.BAD_REQUEST
 import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR
+import static org.springframework.http.HttpStatus.NOT_FOUND
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
 
@@ -76,40 +77,39 @@ class NetworkCmProxyRestExceptionHandlerSpec extends Specification {
     def dataNodeBaseEndpointNcmpInventory
 
     @Shared
-    def errorMessage = 'some error message'
+    def sampleErrorMessage = 'some error message'
     @Shared
-    def errorDetails = 'some error details'
-    @Shared
-    def dataNodeNotFoundErrorMessage = 'DataNode not found'
+    def sampleErrorDetails = 'some error details'
 
     def setup() {
         dataNodeBaseEndpointNcmp = "$basePathNcmp/v1"
         dataNodeBaseEndpointNcmpInventory = "$basePathNcmpInventory/v1"
     }
 
-    def 'Get request with generic #scenario exception returns correct HTTP Status.'() {
+    def 'Get request with generic #scenario exception returns correct HTTP Status with #scenario'() {
         when: 'an exception is thrown by the service'
             setupTestException(exception, NCMP)
             def response = performTestRequest(NCMP)
         then: 'an HTTP response is returned with correct message and details'
             assertTestResponse(response, expectedErrorCode, expectedErrorMessage, expectedErrorDetails)
         where:
-            scenario              | exception                                                                 || expectedErrorDetails | expectedErrorMessage          | expectedErrorCode
-            'CPS'                 | new CpsException(errorMessage, errorDetails)                              || errorDetails         |  errorMessage                 | INTERNAL_SERVER_ERROR
-            'NCMP-server'         | new ServerNcmpException(errorMessage, errorDetails)                       || null                 |  errorMessage                 | INTERNAL_SERVER_ERROR
-            'NCMP-client'         | new DmiRequestException(errorMessage, errorDetails)                       || null                 |  errorMessage                 | BAD_REQUEST
-            'DataNode Validation' | new DataNodeNotFoundException(dataNodeNotFoundErrorMessage, errorDetails) || null                 |  dataNodeNotFoundErrorMessage | BAD_REQUEST
-            'other'               | new IllegalStateException(errorMessage)                                   || null                 |  errorMessage                 | INTERNAL_SERVER_ERROR
+            scenario              | exception                                                        || expectedErrorDetails | expectedErrorMessage | expectedErrorCode
+            'CPS'                 | new CpsException(sampleErrorMessage, sampleErrorDetails)         || sampleErrorDetails   | sampleErrorMessage   | INTERNAL_SERVER_ERROR
+            'NCMP-server'         | new ServerNcmpException(sampleErrorMessage, sampleErrorDetails)  || null                 | sampleErrorMessage   | INTERNAL_SERVER_ERROR
+            'NCMP-client'         | new DmiRequestException(sampleErrorMessage, sampleErrorDetails)  || null                 | sampleErrorMessage   | BAD_REQUEST
+            'DataNode Validation' | new DataNodeNotFoundException('myDataspaceName', 'myAnchorName') || null                 | 'DataNode not found' | NOT_FOUND
+            'other'               | new IllegalStateException(sampleErrorMessage)                    || null                 | sampleErrorMessage   | INTERNAL_SERVER_ERROR
+            'Data Node Not Found' | new DataNodeNotFoundException('myDataspaceName', 'myAnchorName') || 'DataNode not found' | 'DataNode not found' | NOT_FOUND
     }
 
     def 'Post request with exception returns correct HTTP Status.'() {
         given: 'the service throws data validation exception'
-            def exception = new DataValidationException(errorMessage, errorDetails)
+            def exception = new DataValidationException(sampleErrorMessage, sampleErrorDetails)
             setupTestException(exception, NCMPINVENTORY)
         when: 'the HTTP request is made'
             def response = performTestRequest(NCMPINVENTORY)
         then: 'an HTTP response is returned with correct message and details'
-            assertTestResponse(response, BAD_REQUEST, errorMessage, errorDetails)
+            assertTestResponse(response, BAD_REQUEST, sampleErrorMessage, sampleErrorDetails)
     }
 
     def setupTestException(exception, apiType) {
@@ -130,9 +130,9 @@ class NetworkCmProxyRestExceptionHandlerSpec extends Specification {
     static void assertTestResponse(response, expectedStatus , expectedErrorMessage , expectedErrorDetails) {
         assert response.status == expectedStatus.value()
         def content = new JsonSlurper().parseText(response.contentAsString)
-        assert content['status'] == expectedStatus.toString()
-        assert content['message'] == expectedErrorMessage
-        assert expectedErrorDetails == null || content['details'] == expectedErrorDetails
+        assert content['status'].toString().contains(expectedStatus.toString())
+        assert content['message'].toString().contains(expectedErrorMessage)
+        assert expectedErrorDetails == null || content['details'].toString().contains(expectedErrorDetails)
     }
 
     enum ApiType {
index 471e97e..d942d26 100644 (file)
@@ -49,12 +49,14 @@ public interface NetworkCmProxyDataService {
      * @param resourceIdentifier resource identifier
      * @param acceptParamInHeader accept param
      * @param optionsParamInQuery options query
+     * @param topicParamInQuery topic name for (triggering) async responses
      * @return {@code Object} resource data
      */
     Object getResourceDataOperationalForCmHandle(String cmHandleId,
                                                  String resourceIdentifier,
                                                  String acceptParamInHeader,
-                                                 String optionsParamInQuery);
+                                                 String optionsParamInQuery,
+                                                 String topicParamInQuery);
 
     /**
      * Get resource data for data store pass-through running
@@ -64,12 +66,14 @@ public interface NetworkCmProxyDataService {
      * @param resourceIdentifier resource identifier
      * @param acceptParamInHeader accept param
      * @param optionsParamInQuery options query
+     * @param topicParamInQuery topic query
      * @return {@code Object} resource data
      */
     Object getResourceDataPassThroughRunningForCmHandle(String cmHandleId,
                                                         String resourceIdentifier,
                                                         String acceptParamInHeader,
-                                                        String optionsParamInQuery);
+                                                        String optionsParamInQuery,
+                                                        String topicParamInQuery);
 
     /**
      * Write resource data for data store pass-through running
index 1762e46..e923ce4 100755 (executable)
@@ -37,9 +37,12 @@ import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.UUID;
+import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.logging.log4j.util.Strings;
 import org.onap.cps.api.CpsAdminService;
 import org.onap.cps.api.CpsDataService;
 import org.onap.cps.api.CpsModuleService;
@@ -57,6 +60,7 @@ import org.onap.cps.spi.exceptions.DataNodeNotFoundException;
 import org.onap.cps.spi.exceptions.DataValidationException;
 import org.onap.cps.spi.model.ModuleReference;
 import org.onap.cps.utils.JsonObjectMapper;
+import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
 import org.springframework.stereotype.Service;
 
@@ -81,6 +85,12 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService
 
     private final YangModelCmHandleRetriever yangModelCmHandleRetriever;
 
+    // valid kafka topic name regex
+    private static final Pattern TOPIC_NAME_PATTERN = Pattern.compile("^[a-zA-Z0-9]([._-](?![._-])|"
+            + "[a-zA-Z0-9]){0,120}[a-zA-Z0-9]$");
+    private static final String NO_REQUEST_ID = null;
+    private static final String NO_TOPIC = null;
+
     @Override
     public void updateDmiRegistrationAndSyncModule(final DmiPluginRegistration dmiPluginRegistration) {
         dmiPluginRegistration.validateDmiPluginRegistration();
@@ -104,26 +114,21 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService
     public Object getResourceDataOperationalForCmHandle(final String cmHandleId,
                                                         final String resourceIdentifier,
                                                         final String acceptParamInHeader,
-                                                        final String optionsParamInQuery) {
-        return handleResponse(dmiDataOperations.getResourceDataFromDmi(
-            cmHandleId,
-            resourceIdentifier,
-            optionsParamInQuery,
-            acceptParamInHeader,
-            DmiOperations.DataStoreEnum.PASSTHROUGH_OPERATIONAL), "Not able to get resource data.");
+                                                        final String optionsParamInQuery,
+                                                        final String topicParamInQuery) {
+
+        return validateTopicNameAndGetResourceData(cmHandleId, resourceIdentifier, acceptParamInHeader,
+                DmiOperations.DataStoreEnum.PASSTHROUGH_OPERATIONAL, optionsParamInQuery, topicParamInQuery);
     }
 
     @Override
     public Object getResourceDataPassThroughRunningForCmHandle(final String cmHandleId,
                                                                final String resourceIdentifier,
                                                                final String acceptParamInHeader,
-                                                               final String optionsParamInQuery) {
-        return handleResponse(dmiDataOperations.getResourceDataFromDmi(
-            cmHandleId,
-            resourceIdentifier,
-            optionsParamInQuery,
-            acceptParamInHeader,
-            DmiOperations.DataStoreEnum.PASSTHROUGH_RUNNING), "Not able to get resource data.");
+                                                               final String optionsParamInQuery,
+                                                               final String topicParamInQuery) {
+        return validateTopicNameAndGetResourceData(cmHandleId, resourceIdentifier, acceptParamInHeader,
+                DmiOperations.DataStoreEnum.PASSTHROUGH_RUNNING, optionsParamInQuery, topicParamInQuery);
     }
 
     @Override
@@ -297,4 +302,32 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService
         cpsAdminService.createAnchor(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, yangModelCmHandle.getId(),
             yangModelCmHandle.getId());
     }
-}
+
+    private static boolean isValidTopicName(final String topicName) {
+        return Strings.isNotEmpty(topicName) && TOPIC_NAME_PATTERN.matcher(topicName).matches();
+    }
+
+    private Map<String, Object> buildDmiResponse(final String requestId) {
+        final Map<String, Object> dmiResponseMap = new HashMap<>();
+        dmiResponseMap.put("requestId", requestId);
+        return dmiResponseMap;
+    }
+
+    private Object validateTopicNameAndGetResourceData(final String cmHandleId,
+                                                       final String resourceIdentifier,
+                                                       final String acceptParamInHeader,
+                                                       final DmiOperations.DataStoreEnum dataStore,
+                                                       final String optionsParamInQuery,
+                                                       final String topicParamInQuery) {
+        final boolean processAsynchronously = isValidTopicName(topicParamInQuery);
+        if (processAsynchronously) {
+            final String resourceDataRequestId = UUID.randomUUID().toString();
+            return ResponseEntity.status(HttpStatus.OK)
+                    .body(buildDmiResponse(resourceDataRequestId));
+        }
+        final ResponseEntity<?> responseEntity = dmiDataOperations.getResourceDataFromDmi(
+                cmHandleId, resourceIdentifier, optionsParamInQuery, acceptParamInHeader,
+                dataStore, NO_REQUEST_ID, NO_TOPIC);
+        return handleResponse(responseEntity, "Not able to get resource data.");
+    }
+}
\ No newline at end of file
index 229d4fc..68de9d5 100644 (file)
@@ -23,10 +23,10 @@ package org.onap.cps.ncmp.api.impl.operations;
 import static org.onap.cps.ncmp.api.impl.operations.DmiOperations.DataStoreEnum.PASSTHROUGH_RUNNING;
 import static org.onap.cps.ncmp.api.impl.operations.DmiRequestBody.OperationEnum;
 import static org.onap.cps.ncmp.api.impl.operations.DmiRequestBody.OperationEnum.READ;
-import static org.onap.cps.ncmp.api.impl.operations.RequiredDmiService.DATA;
 
 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.utils.DmiServiceUrlBuilder;
 import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle;
 import org.onap.cps.utils.JsonObjectMapper;
 import org.springframework.http.HttpHeaders;
@@ -47,8 +47,8 @@ public class DmiDataOperations extends DmiOperations {
     public DmiDataOperations(final YangModelCmHandleRetriever cmHandlePropertiesRetriever,
                              final JsonObjectMapper jsonObjectMapper,
                              final NcmpConfiguration.DmiProperties dmiProperties,
-                             final DmiRestClient dmiRestClient) {
-        super(cmHandlePropertiesRetriever, jsonObjectMapper, dmiProperties, dmiRestClient);
+                             final DmiRestClient dmiRestClient, final DmiServiceUrlBuilder dmiServiceUrlBuilder) {
+        super(cmHandlePropertiesRetriever, jsonObjectMapper, dmiProperties, dmiRestClient, dmiServiceUrlBuilder);
     }
 
     /**
@@ -59,25 +59,31 @@ public class DmiDataOperations extends DmiOperations {
      * @param resourceId  resource identifier
      * @param optionsParamInQuery options query
      * @param acceptParamInHeader accept parameter
-     * @param dataStore  data store enum
+     * @param dataStore           data store enum
+     * @param requestId           requestId for async responses
+     * @param topicParamInQuery   topic name for (triggering) async responses
      * @return {@code ResponseEntity} response entity
      */
     public ResponseEntity<Object> getResourceDataFromDmi(final String cmHandleId,
-                                                          final String resourceId,
-                                                          final String optionsParamInQuery,
-                                                          final String acceptParamInHeader,
-                                                          final DataStoreEnum dataStore) {
+                                                         final String resourceId,
+                                                         final String optionsParamInQuery,
+                                                         final String acceptParamInHeader,
+                                                         final DataStoreEnum dataStore,
+                                                         final String requestId,
+                                                         final String topicParamInQuery) {
         final YangModelCmHandle yangModelCmHandle =
-            yangModelCmHandleRetriever.getDmiServiceNamesAndProperties(cmHandleId);
+                yangModelCmHandleRetriever.getDmiServiceNamesAndProperties(cmHandleId);
         final DmiRequestBody dmiRequestBody = DmiRequestBody.builder()
             .operation(READ)
+            .requestId(requestId)
             .build();
         dmiRequestBody.asDmiProperties(yangModelCmHandle.getDmiProperties());
         final String jsonBody = jsonObjectMapper.asJsonString(dmiRequestBody);
 
-        final var dmiResourceDataUrl = getDmiDatastoreUrlWithOptions(
-            yangModelCmHandle.resolveDmiServiceName(DATA), cmHandleId, resourceId,
-            optionsParamInQuery, dataStore);
+        final var dmiResourceDataUrl = dmiServiceUrlBuilder.getDmiDatastoreUrl(
+                dmiServiceUrlBuilder.populateQueryParams(resourceId, optionsParamInQuery,
+                topicParamInQuery), dmiServiceUrlBuilder.populateUriVariables(
+                        yangModelCmHandle, cmHandleId, dataStore));
         final var httpHeaders = prepareHeader(acceptParamInHeader);
         return dmiRestClient.postOperationWithJsonData(dmiResourceDataUrl, jsonBody, httpHeaders);
     }
@@ -108,33 +114,10 @@ public class DmiDataOperations extends DmiOperations {
         dmiRequestBody.asDmiProperties(yangModelCmHandle.getDmiProperties());
         final String jsonBody = jsonObjectMapper.asJsonString(dmiRequestBody);
         final String dmiUrl =
-            getResourceInDataStoreUrl(yangModelCmHandle.resolveDmiServiceName(DATA),
-                cmHandleId, resourceId, PASSTHROUGH_RUNNING);
+                dmiServiceUrlBuilder.getDmiDatastoreUrl(dmiServiceUrlBuilder.populateQueryParams(resourceId,
+                                null, null),
+                        dmiServiceUrlBuilder.populateUriVariables(yangModelCmHandle, cmHandleId, PASSTHROUGH_RUNNING));
         return dmiRestClient.postOperationWithJsonData(dmiUrl, jsonBody, new HttpHeaders());
     }
 
-    private String getResourceInDataStoreUrl(final String dmiServiceName,
-                                             final String cmHandleId,
-                                             final String resourceId,
-                                             final DataStoreEnum dataStoreEnum) {
-        return getCmHandleUrl(dmiServiceName, cmHandleId)
-            + "data"
-            + URL_SEPARATOR
-            + "ds"
-            + URL_SEPARATOR
-            + dataStoreEnum.getValue()
-            + "?resourceIdentifier="
-            + resourceId;
-    }
-
-    private String getDmiDatastoreUrlWithOptions(final String dmiServiceName,
-                                                 final String cmHandleId,
-                                                 final String resourceId,
-                                                 final String optionsParamInQuery,
-                                                 final DataStoreEnum dataStoreEnum) {
-        final String resourceInDataStoreUrl = getResourceInDataStoreUrl(dmiServiceName,
-            cmHandleId, resourceId, dataStoreEnum);
-        return appendOptionsQuery(resourceInDataStoreUrl, optionsParamInQuery);
-    }
-
 }
index bfe934d..d79988e 100644 (file)
@@ -31,6 +31,7 @@ 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.utils.DmiServiceUrlBuilder;
 import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle;
 import org.onap.cps.ncmp.api.models.YangResource;
 import org.onap.cps.spi.model.ModuleReference;
@@ -53,8 +54,8 @@ public class DmiModelOperations extends DmiOperations {
     public DmiModelOperations(final YangModelCmHandleRetriever dmiPropertiesRetriever,
                               final JsonObjectMapper jsonObjectMapper,
                               final NcmpConfiguration.DmiProperties dmiProperties,
-                              final DmiRestClient dmiRestClient) {
-        super(dmiPropertiesRetriever, jsonObjectMapper, dmiProperties, dmiRestClient);
+                              final DmiRestClient dmiRestClient, final DmiServiceUrlBuilder dmiServiceUrlBuilder) {
+        super(dmiPropertiesRetriever, jsonObjectMapper, dmiProperties, dmiRestClient, dmiServiceUrlBuilder);
     }
 
     /**
index 645d979..75ba91b 100644 (file)
 
 package org.onap.cps.ncmp.api.impl.operations;
 
-import com.google.common.base.Strings;
 import lombok.Getter;
 import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
 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.utils.DmiServiceUrlBuilder;
 import org.onap.cps.utils.JsonObjectMapper;
 import org.springframework.http.HttpHeaders;
 import org.springframework.stereotype.Service;
 
-@Slf4j
 @RequiredArgsConstructor
 @Service
 public class DmiOperations {
@@ -39,7 +37,7 @@ public class DmiOperations {
     public enum DataStoreEnum {
         PASSTHROUGH_OPERATIONAL("ncmp-datastore:passthrough-operational"),
         PASSTHROUGH_RUNNING("ncmp-datastore:passthrough-running");
-        private String value;
+        private final String value;
 
         DataStoreEnum(final String value) {
             this.value = value;
@@ -50,30 +48,12 @@ public class DmiOperations {
     protected final JsonObjectMapper jsonObjectMapper;
     protected final NcmpConfiguration.DmiProperties dmiProperties;
     protected final DmiRestClient dmiRestClient;
-
-    static final String URL_SEPARATOR = "/";
-
-    String getCmHandleUrl(final String dmiServiceName, final String cmHandle) {
-        return dmiServiceName
-            + dmiProperties.getDmiBasePath()
-            + URL_SEPARATOR
-            + "v1"
-            + URL_SEPARATOR
-            + "ch"
-            + URL_SEPARATOR
-            + cmHandle
-            + URL_SEPARATOR;
-    }
+    protected final DmiServiceUrlBuilder dmiServiceUrlBuilder;
 
     String getDmiResourceUrl(final String dmiServiceName, final String cmHandle, final String resourceName) {
-        return getCmHandleUrl(dmiServiceName, cmHandle) + resourceName;
-    }
-
-    static String appendOptionsQuery(final String url, final String optionsParamInQuery) {
-        if (Strings.isNullOrEmpty(optionsParamInQuery)) {
-            return url;
-        }
-        return url + "&options=" + optionsParamInQuery;
+        return dmiServiceUrlBuilder.getCmHandleUrl()
+                .pathSegment("{resourceName}")
+                .buildAndExpand(dmiServiceName, dmiProperties.getDmiBasePath(), cmHandle, resourceName).toUriString();
     }
 
     static HttpHeaders prepareHeader(final String acceptParam) {
index d97e90c..c84e4cb 100644 (file)
@@ -58,6 +58,7 @@ public class DmiRequestBody {
     private String data;
     @JsonProperty("cmHandleProperties")
     private Map<String, String> dmiProperties;
+    private String requestId;
 
     /**
      * Set DMI Properties by converting a list of YangModelCmHandle.Property objects.
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
new file mode 100644 (file)
index 0000000..b60aac9
--- /dev/null
@@ -0,0 +1,124 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2022 Nordix Foundation
+ *  ================================================================================
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ *  SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.ncmp.api.impl.utils;
+
+import static org.onap.cps.ncmp.api.impl.operations.RequiredDmiService.DATA;
+
+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.operations.DmiOperations;
+import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle;
+import org.springframework.stereotype.Component;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.util.UriComponentsBuilder;
+
+@Component
+@RequiredArgsConstructor
+public class DmiServiceUrlBuilder {
+
+    private final NcmpConfiguration.DmiProperties dmiProperties;
+
+    /**
+     * This method creates the dmi service url.
+     *
+     * @param queryParams  query param map as key,value pair
+     * @param uriVariables uri param map as key (placeholder),value pair
+     * @return {@code String} dmi service url as string
+     */
+    public String getDmiDatastoreUrl(final MultiValueMap<String, String> queryParams,
+                                     final Map<String, Object> uriVariables) {
+        final UriComponentsBuilder uriComponentsBuilder = getCmHandleUrl()
+                .pathSegment("data")
+                .pathSegment("ds")
+                .pathSegment("{dataStore}")
+                .queryParams(queryParams)
+                .uriVariables(uriVariables);
+        return uriComponentsBuilder.buildAndExpand().toUriString();
+    }
+
+    /**
+     * This method creates the dmi service url builder object with path variables.
+     *
+     * @return {@code UriComponentsBuilder} dmi service url builder object
+     */
+    public UriComponentsBuilder getCmHandleUrl() {
+        return UriComponentsBuilder.newInstance()
+                .path("{dmiServiceName}")
+                .pathSegment("{dmiBasePath}")
+                .pathSegment("v1")
+                .pathSegment("ch")
+                .pathSegment("{cmHandle}");
+    }
+
+    /**
+     * This method populates uri variables.
+     *
+     * @param yangModelCmHandle get dmi service name
+     * @param cmHandle          cm handle name for dmi registration
+     * @return {@code String} dmi service url as string
+     */
+    public Map<String, Object> populateUriVariables(final YangModelCmHandle yangModelCmHandle,
+                                                    final String cmHandle,
+                                                    final DmiOperations.DataStoreEnum dataStore) {
+        final Map<String, Object> uriVariables = new HashMap<>();
+        final String dmiBasePath = dmiProperties.getDmiBasePath();
+        uriVariables.put("dmiServiceName",
+                yangModelCmHandle.resolveDmiServiceName(DATA));
+        uriVariables.put("dmiBasePath", dmiBasePath);
+        uriVariables.put("cmHandle", cmHandle);
+        uriVariables.put("dataStore", dataStore.getValue());
+        return uriVariables;
+    }
+
+    /**
+     * This method is used to populate map from query params.
+     *
+     * @param resourceId          unique id of response for valid topic
+     * @param optionsParamInQuery options into url param
+     * @param topicParamInQuery   topic into url param
+     * @return all valid query params as map
+     */
+    public MultiValueMap<String, String> populateQueryParams(final String resourceId,
+                                                             final String optionsParamInQuery,
+                                                             final String topicParamInQuery) {
+        final MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
+        getQueryParamConsumer().accept("resourceIdentifier",
+                resourceId, queryParams);
+        getQueryParamConsumer().accept("options", optionsParamInQuery, queryParams);
+        if (Strings.isNotEmpty(topicParamInQuery)) {
+            getQueryParamConsumer().accept("topic", topicParamInQuery, queryParams);
+        }
+        return queryParams;
+    }
+
+    private TriConsumer<String, String, MultiValueMap<String, String>> getQueryParamConsumer() {
+        return (paramName, paramValue, paramMap) -> {
+            if (Strings.isNotEmpty(paramValue)) {
+                paramMap.add(paramName, paramValue);
+            }
+        };
+    }
+}
index b2a3d77..e6d18d9 100644 (file)
@@ -24,6 +24,7 @@ package org.onap.cps.ncmp.api.impl
 
 import org.onap.cps.ncmp.api.impl.operations.YangModelCmHandleRetriever
 import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle
+import spock.lang.Shared
 
 import static org.onap.cps.ncmp.api.impl.operations.DmiOperations.DataStoreEnum.PASSTHROUGH_OPERATIONAL
 import static org.onap.cps.ncmp.api.impl.operations.DmiOperations.DataStoreEnum.PASSTHROUGH_RUNNING
@@ -56,6 +57,10 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
     def mockDmiDataOperations = Mock(DmiDataOperations)
     def nullNetworkCmProxyDataServicePropertyHandler = null
     def mockYangModelCmHandleRetriever = Mock(YangModelCmHandleRetriever)
+    def NO_TOPIC = null
+    def NO_REQUEST_ID = null
+    @Shared
+    def OPTIONS_PARAM = '(a=1,b=2)'
 
     def objectUnderTest = new NetworkCmProxyDataServiceImpl(mockCpsDataService, spiedJsonObjectMapper, mockDmiDataOperations, mockDmiModelOperations,
         mockCpsModuleService, mockCpsAdminService, nullNetworkCmProxyDataServicePropertyHandler, mockYangModelCmHandleRetriever)
@@ -64,7 +69,6 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
 
     def dataNode = new DataNode(leaves: ['dmi-service-name': 'testDmiService'])
 
-
     def 'Write resource data for pass-through running from DMI using POST #scenario cm handle properties.'() {
         given: 'cpsDataService returns valid datanode'
             mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
@@ -104,18 +108,21 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
                 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode
         and: 'get resource data from DMI is called'
             mockDmiDataOperations.getResourceDataFromDmi(
-                'testCmHandle',
-                'testResourceId',
-                '(a=1,b=2)',
-                'testAcceptParam' ,
-                PASSTHROUGH_OPERATIONAL) >> new ResponseEntity<>('result-json', HttpStatus.OK)
+                    'testCmHandle',
+                    'testResourceId',
+                    OPTIONS_PARAM,
+                    'testAcceptParam',
+                    PASSTHROUGH_OPERATIONAL,
+                    NO_REQUEST_ID,
+                    NO_TOPIC) >> new ResponseEntity<>('dmi-response', HttpStatus.OK)
         when: 'get resource data operational for cm-handle is called'
             def response = objectUnderTest.getResourceDataOperationalForCmHandle('testCmHandle',
-                'testResourceId',
-                'testAcceptParam',
-                '(a=1,b=2)')
+                    'testResourceId',
+                    'testAcceptParam',
+                    OPTIONS_PARAM,
+                    NO_TOPIC)
         then: 'DMI returns a json response'
-            response == 'result-json'
+            response == 'dmi-response'
     }
 
     def 'Get resource data for pass-through operational from DMI with Json Processing Exception.'() {
@@ -129,9 +136,10 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
                 >> new ResponseEntity<>('NOK-json', HttpStatus.NOT_FOUND)
         when: 'get resource data is called'
             objectUnderTest.getResourceDataOperationalForCmHandle('testCmHandle',
-                'testResourceId',
-                'testAcceptParam',
-                '(a=1,b=2)')
+                    'testResourceId',
+                    'testAcceptParam',
+                    OPTIONS_PARAM,
+                    NO_TOPIC)
         then: 'exception is thrown with the expected details'
             def exceptionThrown = thrown(ServerNcmpException.class)
             exceptionThrown.details == 'DMI status code: 404, DMI response body: NOK-json'
@@ -143,16 +151,19 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
                 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode
         and: 'DMI returns NOK response'
             mockDmiDataOperations.getResourceDataFromDmi('testCmHandle',
-                'testResourceId',
-                '(a=1,b=2)',
-                'testAcceptParam',
-                PASSTHROUGH_OPERATIONAL)
-                >> new ResponseEntity<>('NOK-json', HttpStatus.NOT_FOUND)
+                    'testResourceId',
+                    OPTIONS_PARAM,
+                    'testAcceptParam',
+                    PASSTHROUGH_OPERATIONAL,
+                    NO_REQUEST_ID,
+                    NO_TOPIC)
+                    >> new ResponseEntity<>('NOK-json', HttpStatus.NOT_FOUND)
         when: 'get resource data is called'
             objectUnderTest.getResourceDataOperationalForCmHandle('testCmHandle',
-                'testResourceId',
-                'testAcceptParam',
-                '(a=1,b=2)')
+                    'testResourceId',
+                    'testAcceptParam',
+                    OPTIONS_PARAM,
+                    NO_TOPIC)
         then: 'exception is thrown'
             def exceptionThrown = thrown(ServerNcmpException.class)
         and: 'details contains the original response'
@@ -165,17 +176,20 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
                 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode
         and: 'DMI returns valid response and data'
             mockDmiDataOperations.getResourceDataFromDmi('testCmHandle',
-                'testResourceId',
-                '(a=1,b=2)',
-                'testAcceptParam',
-                PASSTHROUGH_RUNNING) >> new ResponseEntity<>('{result-json}', HttpStatus.OK)
+                    'testResourceId',
+                    OPTIONS_PARAM,
+                    'testAcceptParam',
+                    PASSTHROUGH_RUNNING,
+                    NO_REQUEST_ID,
+                    NO_TOPIC) >> new ResponseEntity<>('{dmi-response}', HttpStatus.OK)
         when: 'get resource data is called'
             def response = objectUnderTest.getResourceDataPassThroughRunningForCmHandle('testCmHandle',
-                'testResourceId',
-                'testAcceptParam',
-                '(a=1,b=2)')
+                    'testResourceId',
+                    'testAcceptParam',
+                    OPTIONS_PARAM,
+                    NO_TOPIC)
         then: 'get resource data returns expected response'
-            response == '{result-json}'
+            response == '{dmi-response}'
     }
 
     def 'Get resource data for pass-through running from DMI return NOK response.'() {
@@ -184,22 +198,91 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
                 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode
         and: 'DMI returns NOK response'
             mockDmiDataOperations.getResourceDataFromDmi('testCmHandle',
-                'testResourceId',
-                '(a=1,b=2)',
-                'testAcceptParam',
-                PASSTHROUGH_RUNNING)
-                >> new ResponseEntity<>('NOK-json', HttpStatus.NOT_FOUND)
+                    'testResourceId',
+                    OPTIONS_PARAM,
+                    'testAcceptParam',
+                    PASSTHROUGH_RUNNING,
+                    NO_REQUEST_ID,
+                    NO_TOPIC)
+                    >> new ResponseEntity<>('NOK-json', HttpStatus.NOT_FOUND)
         when: 'get resource data is called'
             objectUnderTest.getResourceDataPassThroughRunningForCmHandle('testCmHandle',
-                'testResourceId',
-                'testAcceptParam',
-                '(a=1,b=2)')
+                    'testResourceId',
+                    'testAcceptParam',
+                    OPTIONS_PARAM,
+                    NO_TOPIC)
         then: 'exception is thrown'
             def exceptionThrown = thrown(ServerNcmpException.class)
         and: 'details contains the original response'
             exceptionThrown.details.contains('NOK-json')
     }
 
+    def 'Get resource data for operational from DMI with empty topic sync request.'() {
+        given: 'cps data service returns valid data node'
+            mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
+                    cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode
+        and: 'dmi data operation returns valid response and data'
+            mockDmiDataOperations.getResourceDataFromDmi(_, _, _, _, _, NO_REQUEST_ID, NO_TOPIC)
+                    >> new ResponseEntity<>('{dmi-response}', HttpStatus.OK)
+        when: 'get resource data is called data operational with blank topic'
+            def responseData = objectUnderTest.getResourceDataOperationalForCmHandle('', '',
+                    '', '', emptyTopic)
+        then: '(synchronous) the dmi response is expected'
+            assert responseData == '{dmi-response}'
+        where: 'the following parameters are used'
+            scenario             | emptyTopic
+            'No topic in url'    | ''
+            'Null topic in url'  | null
+            'Empty topic in url' | '\"\"'
+            'Blank topic in url' | ' '
+    }
+
+    def 'Get resource data for data operational from DMI with valid topic i.e. async request.'() {
+        given: 'cps data service returns valid data node'
+            mockCpsDataService.getDataNode(*_) >> dataNode
+        and: 'dmi data operation returns valid response and data'
+            mockDmiDataOperations.getResourceDataFromDmi(_, _, _, _, _, _, 'my-topic-name')
+                    >> new ResponseEntity<>('{dmi-response}', HttpStatus.OK)
+        when: 'get resource data is called for data operational with valid topic'
+            def responseData = objectUnderTest.getResourceDataOperationalForCmHandle('', '', '', '', 'my-topic-name')
+        then: 'non empty request id is generated'
+            assert responseData.body.requestId.length() > 0
+    }
+
+    def 'Get resource data for pass through running from DMI with valid topic async request.'() {
+        given: 'cps data service returns valid data node'
+            mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
+                    cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode
+        and: 'dmi data operation returns valid response and data'
+            mockDmiDataOperations.getResourceDataFromDmi(_, _, _, _, _, _, 'my-topic-name')
+                    >> new ResponseEntity<>('{dmi-response}', HttpStatus.OK)
+        when: 'get resource data is called for data operational with valid topic'
+            def responseData = objectUnderTest.getResourceDataPassThroughRunningForCmHandle('',
+                    '', '', OPTIONS_PARAM, 'my-topic-name')
+        then: 'non empty request id is generated'
+            assert responseData.body.requestId.length() > 0
+    }
+
+    def 'Get resource data for pass through running from DMI sync request where #scenario.'() {
+        given: 'cps data service returns valid data node'
+            mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
+                    cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode
+        and: 'dmi data operation returns valid response and data'
+            mockDmiDataOperations.getResourceDataFromDmi(_, _, _, _, _, NO_REQUEST_ID, NO_TOPIC)
+                    >> new ResponseEntity<>('{dmi-response}', HttpStatus.OK)
+        when: 'get resource data is called for data operational with valid topic'
+            def responseData = objectUnderTest.getResourceDataPassThroughRunningForCmHandle('',
+                    '', '', '', emptyTopic)
+        then: '(synchronous) the dmi response is expected'
+            assert responseData == '{dmi-response}'
+        where: 'the following parameters are used'
+            scenario             | emptyTopic
+            'No topic in url'    | ''
+            'Null topic in url'  | null
+            'Empty topic in url' | '\"\"'
+            'Blank topic in url' | ' '
+    }
+
     def 'Getting Yang Resources.'() {
         when: 'yang resources is called'
             objectUnderTest.getYangResourcesModuleReferences('some cm handle')
@@ -255,7 +338,7 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
                 givenOperation,
                 '{some-json}',
                 'application/json')
-        then: 'an exception is thrown with the expected error message detailsd with correct operation'
+        then: 'an exception is thrown with the expected error message details with correct operation'
             def exceptionThrown = thrown(ServerNcmpException.class)
             exceptionThrown.getMessage().contains(expectedResponseMessage)
         where:
index e585825..3df862a 100644 (file)
@@ -22,12 +22,15 @@ package org.onap.cps.ncmp.api.impl.operations
 
 import com.fasterxml.jackson.databind.ObjectMapper
 import org.onap.cps.ncmp.api.impl.config.NcmpConfiguration
+import org.onap.cps.ncmp.api.impl.utils.DmiServiceUrlBuilder
 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.ResponseEntity
 import org.springframework.test.context.ContextConfiguration
+import org.springframework.util.MultiValueMap
+import spock.lang.Shared
 
 import static org.onap.cps.ncmp.api.impl.operations.DmiOperations.DataStoreEnum.PASSTHROUGH_OPERATIONAL
 import static org.onap.cps.ncmp.api.impl.operations.DmiOperations.DataStoreEnum.PASSTHROUGH_RUNNING
@@ -40,43 +43,54 @@ import org.springframework.http.HttpStatus
 class DmiDataOperationsSpec extends DmiOperationsBaseSpec {
 
     @SpringBean
-    JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
+    DmiServiceUrlBuilder dmiServiceUrlBuilder = Mock()
+    def dmiServiceBaseUrl = "${dmiServiceName}/dmi/v1/ch/${cmHandleId}/data/ds/ncmp-datastore:"
+    def NO_TOPIC = null
+    def NO_REQUEST_ID = null
+    @Shared
+    def OPTIONS_PARAM = '(a=1,b=2)'
+
+    @SpringBean
+    JsonObjectMapper spiedJsonObjectMapper = Spy(new JsonObjectMapper(new ObjectMapper()))
 
     @Autowired
     DmiDataOperations objectUnderTest
 
-    def 'call get resource data for #expectedDatastoreInUrl from DMI #scenario.'() {
+    def 'call get resource data for #expectedDatastoreInUrl 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 = new ResponseEntity<Object>(HttpStatus.OK)
-            mockDmiRestClient.postOperationWithJsonData(
-                "${dmiServiceName}/dmi/v1/ch/${cmHandleId}/data/ds/ncmp-datastore:${expectedDatastoreInUrl}?resourceIdentifier=${resourceIdentifier}${expectedOptionsInUrl}",
-                expectedJson, [Accept:['sample accept header']]) >> responseFromDmi
+            def expectedUrl = dmiServiceBaseUrl + "${expectedDatastoreInUrl}?resourceIdentifier=${resourceIdentifier}${expectedOptionsInUrl}"
+            mockDmiRestClient.postOperationWithJsonData(expectedUrl,
+                    expectedJson, [Accept: ['sample accept header']]) >> responseFromDmi
+            dmiServiceUrlBuilder.getDmiDatastoreUrl(_, _) >> expectedUrl
         when: 'get resource data is invoked'
-            def result = objectUnderTest.getResourceDataFromDmi(cmHandleId,resourceIdentifier, options,'sample accept header', dataStore)
+            def result = objectUnderTest.getResourceDataFromDmi(cmHandleId, resourceIdentifier,
+                    options, 'sample accept header', dataStore, NO_REQUEST_ID, NO_TOPIC)
         then: 'the result is the response from the DMI service'
             assert result == responseFromDmi
         where: 'the following parameters are used'
-            scenario             | dmiProperties        | dataStore               | options     || expectedJson                                                 | expectedDatastoreInUrl    | expectedOptionsInUrl
-            'without properties' | []                   | PASSTHROUGH_OPERATIONAL | '(a=1,b=2)' || '{"operation":"read","cmHandleProperties":{}}'               | 'passthrough-operational' | '&options=(a=1,b=2)'
-            'with properties'    | [yangModelCmHandleProperty] | PASSTHROUGH_OPERATIONAL | '(a=1,b=2)' || '{"operation":"read","cmHandleProperties":{"prop1":"val1"}}' | 'passthrough-operational' | '&options=(a=1,b=2)'
-            'null options'       | [yangModelCmHandleProperty] | PASSTHROUGH_OPERATIONAL | null        || '{"operation":"read","cmHandleProperties":{"prop1":"val1"}}' | 'passthrough-operational' | ''
-            'empty options'      | [yangModelCmHandleProperty] | PASSTHROUGH_OPERATIONAL | ''          || '{"operation":"read","cmHandleProperties":{"prop1":"val1"}}' | 'passthrough-operational' | ''
-            'datastore running'  | []                   | PASSTHROUGH_RUNNING     | '(a=1,b=2)' || '{"operation":"read","cmHandleProperties":{}}'               | 'passthrough-running'     | '&options=(a=1,b=2)'
+            scenario                               | dmiProperties               | dataStore               | options       || expectedJson                                                 | expectedDatastoreInUrl    | expectedOptionsInUrl
+            'without properties'                   | []                          | PASSTHROUGH_OPERATIONAL | OPTIONS_PARAM || '{"operation":"read","cmHandleProperties":{}}'               | 'passthrough-operational' | '&options=(a=1,b=2)'
+            'with properties'                      | [yangModelCmHandleProperty] | PASSTHROUGH_OPERATIONAL | OPTIONS_PARAM || '{"operation":"read","cmHandleProperties":{"prop1":"val1"}}' | 'passthrough-operational' | '&options=(a=1,b=2)'
+            'null options'                         | [yangModelCmHandleProperty] | PASSTHROUGH_OPERATIONAL | null          || '{"operation":"read","cmHandleProperties":{"prop1":"val1"}}' | 'passthrough-operational' | ''
+            'empty options'                        | [yangModelCmHandleProperty] | PASSTHROUGH_OPERATIONAL | ''            || '{"operation":"read","cmHandleProperties":{"prop1":"val1"}}' | 'passthrough-operational' | ''
+            'datastore running without properties' | []                          | PASSTHROUGH_RUNNING     | OPTIONS_PARAM || '{"operation":"read","cmHandleProperties":{}}'               | 'passthrough-running'     | '&options=(a=1,b=2)'
+            'datastore running with properties'    | [yangModelCmHandleProperty] | PASSTHROUGH_RUNNING     | OPTIONS_PARAM || '{"operation":"read","cmHandleProperties":{"prop1":"val1"}}' | 'passthrough-running'     | '&options=(a=1,b=2)'
     }
 
     def 'Write data for pass-through:running datastore in DMI.'() {
         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 = "${dmiServiceName}/dmi/v1/ch/${cmHandleId}/data/ds" +
-                "/ncmp-datastore:passthrough-running?resourceIdentifier=${resourceIdentifier}"
+            def expectedUrl = dmiServiceBaseUrl + "passthrough-running?resourceIdentifier=${resourceIdentifier}"
             def expectedJson = '{"operation":"' + expectedOperationInUrl + '","dataType":"some data type","data":"requestData","cmHandleProperties":{"prop1":"val1"}}'
             def responseFromDmi = new ResponseEntity<Object>(HttpStatus.OK)
+            dmiServiceUrlBuilder.getDmiDatastoreUrl(_, _) >> expectedUrl
             mockDmiRestClient.postOperationWithJsonData(expectedUrl, expectedJson, [:]) >> responseFromDmi
         when: 'write resource method is invoked'
-            def result = objectUnderTest.writeResourceDataPassThroughRunningFromDmi(cmHandleId,'parent/child', operation, 'requestData', 'some data type')
+            def result = objectUnderTest.writeResourceDataPassThroughRunningFromDmi(cmHandleId, 'parent/child', operation, 'requestData', 'some data type')
         then: 'the result is the response from the DMI service'
             assert result == responseFromDmi
         where: 'the following operation is performed'
@@ -84,5 +98,4 @@ class DmiDataOperationsSpec extends DmiOperationsBaseSpec {
             CREATE    || 'create'
             UPDATE    || 'update'
     }
-
 }
\ No newline at end of file
index cd2cb71..d3fc17c 100644 (file)
@@ -23,6 +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.utils.DmiServiceUrlBuilder
 import org.onap.cps.spi.model.ModuleReference
 import org.onap.cps.utils.JsonObjectMapper
 import org.spockframework.spring.SpringBean
@@ -31,6 +32,7 @@ import org.springframework.boot.test.context.SpringBootTest
 import org.springframework.http.HttpStatus
 import org.springframework.http.ResponseEntity
 import org.springframework.test.context.ContextConfiguration
+import org.springframework.web.util.UriComponentsBuilder
 import spock.lang.Shared
 
 @SpringBootTest
@@ -50,14 +52,15 @@ class DmiModelOperationsSpec extends DmiOperationsBaseSpec {
         given: 'a cm handle'
             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 responseFromDmi = new ResponseEntity([schemas:moduleReferencesAsLisOfMaps], HttpStatus.OK)
-            mockDmiRestClient.postOperationWithJsonData("${dmiServiceName}/dmi/v1/ch/${cmHandleId}/modules",
-                '{"cmHandleProperties":{}}', [:]) >> responseFromDmi
+            def moduleReferencesAsLisOfMaps = [[moduleName: 'mod1', revision: 'A'], [moduleName: 'mod2', revision: 'X']]
+            def expectedUrl = "${dmiServiceName}/dmi/v1/ch/${cmHandleId}/modules"
+            def responseFromDmi = new ResponseEntity([schemas: moduleReferencesAsLisOfMaps], HttpStatus.OK)
+            mockDmiRestClient.postOperationWithJsonData(expectedUrl, '{"cmHandleProperties":{}}', [:])
+                    >> responseFromDmi
         when: 'get module references is called'
             def result = objectUnderTest.getModuleReferences(yangModelCmHandle)
         then: 'the result consists of expected module references'
-            assert result == [new ModuleReference(moduleName:'mod1',revision:'A'), new ModuleReference(moduleName:'mod2',revision:'X')]
+            assert result == [new ModuleReference(moduleName: 'mod1', revision: 'A'), new ModuleReference(moduleName: 'mod2', revision: 'X')]
     }
 
     def 'Retrieving module references edge case: #scenario.'() {
index dd0d64d..e6f63ce 100644 (file)
@@ -22,7 +22,9 @@ package org.onap.cps.ncmp.api.impl.operations
 
 import com.fasterxml.jackson.databind.ObjectMapper
 import org.onap.cps.ncmp.api.impl.client.DmiRestClient
+import org.onap.cps.ncmp.api.impl.config.NcmpConfiguration
 import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle
+import org.onap.cps.ncmp.api.impl.utils.DmiServiceUrlBuilder
 import org.spockframework.spring.SpringBean
 import spock.lang.Shared
 import spock.lang.Specification
@@ -41,6 +43,9 @@ abstract class DmiOperationsBaseSpec extends Specification {
     @SpringBean
     ObjectMapper spyObjectMapper = Spy()
 
+    @SpringBean
+    DmiServiceUrlBuilder dmiServiceUrlBuilder = new DmiServiceUrlBuilder(new NcmpConfiguration.DmiProperties())
+
     def yangModelCmHandle = new YangModelCmHandle()
     def static dmiServiceName = 'some service name'
     def static cmHandleId = 'some cm handle'
diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/utils/DmiServiceUrlBuilderSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/utils/DmiServiceUrlBuilderSpec.groovy
new file mode 100644 (file)
index 0000000..1615d05
--- /dev/null
@@ -0,0 +1,79 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2022 Nordix Foundation
+ *  ================================================================================
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ *  SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.ncmp.api.utils
+
+import static org.onap.cps.ncmp.api.impl.operations.DmiOperations.DataStoreEnum.PASSTHROUGH_RUNNING
+
+import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle
+import org.onap.cps.ncmp.api.impl.config.NcmpConfiguration
+import org.onap.cps.ncmp.api.impl.utils.DmiServiceUrlBuilder
+import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle
+import spock.lang.Shared
+import spock.lang.Specification
+
+class DmiServiceUrlBuilderSpec extends Specification {
+
+    @Shared
+    YangModelCmHandle yangModelCmHandle = YangModelCmHandle.toYangModelCmHandle("dmiServiceName",
+            "dmiDataServiceName", "dmiModuleServiceName", new NcmpServiceCmHandle())
+
+    NcmpConfiguration.DmiProperties dmiProperties = new NcmpConfiguration.DmiProperties();
+
+    def objectUnderTest = new DmiServiceUrlBuilder(dmiProperties)
+
+    def 'Create the dmi service url with #scenario.'() {
+        given: 'uri variables'
+            dmiProperties.dmiBasePath = 'dmi';
+            def uriVars = objectUnderTest.populateUriVariables(yangModelCmHandle,
+                    "cmHandle", PASSTHROUGH_RUNNING);
+        and: 'query params'
+            def uriQueries = objectUnderTest.populateQueryParams(resourceId,
+                    'optionsParamInQuery', topicParamInQuery);
+        when: 'a dmi datastore service url is generated'
+            def dmiServiceUrl = objectUnderTest.getDmiDatastoreUrl(uriQueries, uriVars)
+        then: 'service url is generated as expected'
+            assert dmiServiceUrl == expectedDmiServiceUrl
+        where: 'the following parameters are used'
+            scenario                       | topicParamInQuery   | resourceId   || expectedDmiServiceUrl
+            'With valid resourceId'        | 'topicParamInQuery' | 'resourceId' || 'dmiServiceName/dmi/v1/ch/cmHandle/data/ds/ncmp-datastore:passthrough-running?resourceIdentifier=resourceId&options=optionsParamInQuery&topic=topicParamInQuery'
+            'With Empty resourceId'        | 'topicParamInQuery' | ''           || 'dmiServiceName/dmi/v1/ch/cmHandle/data/ds/ncmp-datastore:passthrough-running?options=optionsParamInQuery&topic=topicParamInQuery'
+            'With Empty dmi base path'     | 'topicParamInQuery' | 'resourceId' || 'dmiServiceName/dmi/v1/ch/cmHandle/data/ds/ncmp-datastore:passthrough-running?resourceIdentifier=resourceId&options=optionsParamInQuery&topic=topicParamInQuery'
+            'With Empty topicParamInQuery' | ''                  | 'resourceId' || 'dmiServiceName/dmi/v1/ch/cmHandle/data/ds/ncmp-datastore:passthrough-running?resourceIdentifier=resourceId&options=optionsParamInQuery'
+    }
+
+    def 'Populate dmi data store url #scenario.'() {
+        given: 'uri variables are created'
+            dmiProperties.dmiBasePath = dmiBasePath;
+            def uriVars = objectUnderTest.populateUriVariables(yangModelCmHandle,
+                    "cmHandle", PASSTHROUGH_RUNNING);
+        and: 'null query params'
+            def uriQueries = objectUnderTest.populateQueryParams(null,
+                    null, null);
+        when: 'a dmi datastore service url is generated'
+            def dmiServiceUrl = objectUnderTest.getDmiDatastoreUrl(uriQueries, uriVars)
+        then: 'the created dmi service url matches the expected'
+            assert dmiServiceUrl == expectedDmiServiceUrl
+        where: 'the following parameters are used'
+            scenario               | decription                                | dmiBasePath || expectedDmiServiceUrl
+            'with base path  / '   | 'Invalid base path as it starts with /'   | '/dmi'      || 'dmiServiceName//dmi/v1/ch/cmHandle/data/ds/ncmp-datastore:passthrough-running'
+            'without base path / ' | 'Valid path as it does not starts with /' | 'dmi'       || 'dmiServiceName/dmi/v1/ch/cmHandle/data/ds/ncmp-datastore:passthrough-running'
+    }
+}
index d8fbb64..c23926e 100644 (file)
@@ -1,5 +1,5 @@
 #  ============LICENSE_START=======================================================
-#  Copyright (C) 2021 Nordix Foundation
+#  Copyright (C) 2021-2022 Nordix Foundation
 #  ================================================================================
 #  Licensed under the Apache License, Version 2.0 (the "License");
 #  you may not use this file except in compliance with the License.
@@ -21,5 +21,5 @@ dmi:
         username: some-user
         password: some-password
     api:
-        base-path: /dmi
+        base-path: dmi