Merge "dmi plugin version updated to 1.2.0-SNAPSHOT-latest"
authorBruno Sakoto <bruno.sakoto@bell.ca>
Wed, 6 Apr 2022 18:57:06 +0000 (18:57 +0000)
committerGerrit Code Review <gerrit@onap.org>
Wed, 6 Apr 2022 18:57:06 +0000 (18:57 +0000)
27 files changed:
cps-ncmp-rest/docs/openapi/components.yaml
cps-ncmp-rest/docs/openapi/ncmp.yml
cps-ncmp-rest/docs/openapi/openapi.yml
cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyController.java
cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyControllerSpec.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/models/CmHandleQueryApiParameters.java [new file with mode: 0644]
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImplSpec.groovy
cps-ri/src/main/java/org/onap/cps/spi/impl/CpsAdminPersistenceServiceImpl.java
cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceQuery.java
cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceRepository.java
cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceRepositoryImpl.java
cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsAdminPersistenceServiceSpec.groovy
cps-ri/src/test/resources/data/fragment.sql
cps-service/src/main/java/org/onap/cps/api/CpsAdminService.java
cps-service/src/main/java/org/onap/cps/api/impl/CpsAdminServiceImpl.java
cps-service/src/main/java/org/onap/cps/spi/CpsAdminPersistenceService.java
cps-service/src/main/java/org/onap/cps/spi/model/CmHandleQueryParameters.java [new file with mode: 0644]
cps-service/src/main/java/org/onap/cps/spi/model/DataNode.java
cps-service/src/main/java/org/onap/cps/utils/CpsValidator.java
cps-service/src/test/groovy/org/onap/cps/api/impl/CpsAdminServiceImplSpec.groovy
cps-service/src/test/groovy/org/onap/cps/utils/CpsValidatorSpec.groovy
csit/data/cmHandleRegistration.json [deleted file]
csit/plans/cps/testplan.txt
csit/tests/cps-model-sync/cps-model-sync.robot
csit/tests/public-properties-query/public-properties-query.robot [new file with mode: 0644]

index 247fabd..a7955c1 100644 (file)
@@ -211,6 +211,16 @@ components:
           type: string
           example: my-module-revision
 
+    CmHandleQueryRestParameters:
+      type: object
+      title: Cm Handle query parameters for executing cm handle search
+      properties:
+        publicCmHandleProperties:
+          type: object
+          additionalProperties:
+            type: string
+            example: Book Type
+
     RestOutputCmHandle:
       type: object
       title: CM handle Details
index 03ed98a..05e4b84 100755 (executable)
@@ -291,6 +291,33 @@ retrieveCmHandleDetailsById:
           application/json:
             schema:
               $ref: 'components.yaml#/components/schemas/RestOutputCmHandle'
+      404:
+        $ref: 'components.yaml#/components/responses/NotFound'
+      500:
+        $ref: 'components.yaml#/components/responses/InternalServerError'
+
+queryCmHandles:
+  post:
+    description: Execute cm handle query search
+    tags:
+      - network-cm-proxy
+    summary: Execute cm handle query upon a given set of query parameters
+    operationId: queryCmHandles
+    requestBody:
+      required: true
+      content:
+        application/json:
+          schema:
+            $ref: 'components.yaml#/components/schemas/CmHandleQueryRestParameters'
+    responses:
+      200:
+        description: OK
+        content:
+          application/json:
+            schema:
+              type: array
+              items:
+                type: string
       400:
         $ref: 'components.yaml#/components/responses/BadRequest'
       401:
index 12a8318..935b657 100755 (executable)
@@ -39,4 +39,7 @@ paths:
     $ref: 'ncmp.yml#/executeCmHandleSearch'
 
   /v1/ch/{cm-handle}:
-    $ref: 'ncmp.yml#/retrieveCmHandleDetailsById'
\ No newline at end of file
+    $ref: 'ncmp.yml#/retrieveCmHandleDetailsById'
+
+  /v1/data/ch/searches:
+    $ref: 'ncmp.yml#/queryCmHandles'
index de6c3c4..84fcd88 100755 (executable)
@@ -31,18 +31,25 @@ import static org.onap.cps.ncmp.api.impl.operations.DmiRequestBody.OperationEnum
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
 import java.util.stream.Collectors;
 import javax.validation.Valid;
 import javax.validation.constraints.NotNull;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.onap.cps.ncmp.api.NetworkCmProxyDataService;
+import org.onap.cps.ncmp.api.impl.exception.InvalidTopicException;
+import org.onap.cps.ncmp.api.models.CmHandleQueryApiParameters;
 import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle;
 import org.onap.cps.ncmp.rest.api.NetworkCmProxyApi;
 import org.onap.cps.ncmp.rest.model.CmHandleProperties;
 import org.onap.cps.ncmp.rest.model.CmHandleProperty;
 import org.onap.cps.ncmp.rest.model.CmHandlePublicProperties;
+import org.onap.cps.ncmp.rest.model.CmHandleQueryRestParameters;
 import org.onap.cps.ncmp.rest.model.CmHandles;
 import org.onap.cps.ncmp.rest.model.ConditionProperties;
 import org.onap.cps.ncmp.rest.model.Conditions;
@@ -50,6 +57,7 @@ import org.onap.cps.ncmp.rest.model.ModuleNameAsJsonObject;
 import org.onap.cps.ncmp.rest.model.ModuleNamesAsJsonArray;
 import org.onap.cps.ncmp.rest.model.RestModuleReference;
 import org.onap.cps.ncmp.rest.model.RestOutputCmHandle;
+import org.onap.cps.utils.CpsValidator;
 import org.onap.cps.utils.JsonObjectMapper;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
@@ -63,6 +71,9 @@ import org.springframework.web.bind.annotation.RestController;
 public class NetworkCmProxyController implements NetworkCmProxyApi {
 
     private static final String NO_BODY = null;
+    private static final String NO_REQUEST_ID = null;
+    private static final String NO_TOPIC = null;
+    public static final String ASYNC_REQUEST_ID = "requestId";
 
     private final NetworkCmProxyDataService networkCmProxyDataService;
     private final JsonObjectMapper jsonObjectMapper;
@@ -82,11 +93,19 @@ public class NetworkCmProxyController implements NetworkCmProxyApi {
                                                                         final @NotNull @Valid String resourceIdentifier,
                                                                         final @Valid String optionsParamInQuery,
                                                                         final @Valid String topicParamInQuery) {
+        final ResponseEntity<Map<String, Object>> asyncResponse = populateAsyncResponse(topicParamInQuery);
+        final Map<String, Object> asyncResponseData = asyncResponse.getBody();
+
         final Object responseObject = networkCmProxyDataService.getResourceDataOperationalForCmHandle(cmHandle,
                 resourceIdentifier,
                 optionsParamInQuery,
-                topicParamInQuery);
-        return ResponseEntity.ok(responseObject);
+                asyncResponseData == null ? NO_TOPIC : topicParamInQuery,
+                asyncResponseData == null ? NO_REQUEST_ID : asyncResponseData.get(ASYNC_REQUEST_ID).toString());
+
+        if (asyncResponseData == null) {
+            return ResponseEntity.ok(responseObject);
+        }
+        return ResponseEntity.ok(asyncResponse);
     }
 
     /**
@@ -103,11 +122,19 @@ public class NetworkCmProxyController implements NetworkCmProxyApi {
                                                                     final @NotNull @Valid String resourceIdentifier,
                                                                     final @Valid String optionsParamInQuery,
                                                                     final @Valid String topicParamInQuery) {
+        final ResponseEntity<Map<String, Object>> asyncResponse = populateAsyncResponse(topicParamInQuery);
+        final Map<String, Object> asyncResponseData = asyncResponse.getBody();
+
         final Object responseObject = networkCmProxyDataService.getResourceDataPassThroughRunningForCmHandle(cmHandle,
                 resourceIdentifier,
                 optionsParamInQuery,
-                topicParamInQuery);
-        return ResponseEntity.ok(responseObject);
+                asyncResponseData == null ? NO_TOPIC : topicParamInQuery,
+                asyncResponseData == null ? NO_REQUEST_ID : asyncResponseData.get(ASYNC_REQUEST_ID).toString());
+
+        if (asyncResponseData == null) {
+            return ResponseEntity.ok(responseObject);
+        }
+        return ResponseEntity.ok(asyncResponse);
     }
 
     @Override
@@ -188,6 +215,19 @@ public class NetworkCmProxyController implements NetworkCmProxyApi {
         return ResponseEntity.ok(cmHandles);
     }
 
+    /**
+     * Query and return cm handles that match the given query parameters.
+     *
+     * @param cmHandleQueryRestParameters the cm handle query parameters
+     * @return collection of cm handle ids
+     */
+    public ResponseEntity<List<String>> queryCmHandles(
+        final CmHandleQueryRestParameters cmHandleQueryRestParameters) {
+        final Set<String> cmHandleIds = networkCmProxyDataService.queryCmHandles(
+            jsonObjectMapper.convertToValueType(cmHandleQueryRestParameters, CmHandleQueryApiParameters.class));
+        return ResponseEntity.ok(List.copyOf(cmHandleIds));
+    }
+
     /**
      * Search for Cm Handle and Properties by Name.
      * @param cmHandleId cm-handle identifier
@@ -257,4 +297,33 @@ public class NetworkCmProxyController implements NetworkCmProxyApi {
         restOutputCmHandle.setPublicCmHandleProperties(cmHandlePublicProperties);
         return restOutputCmHandle;
     }
+
+    private ResponseEntity<Map<String, Object>> populateAsyncResponse(final String topicParamInQuery) {
+        final boolean processAsynchronously = hasTopicParameter(topicParamInQuery);
+        final Map<String, Object> responseData;
+        if (processAsynchronously) {
+            responseData = getAsyncResponseData();
+        } else {
+            responseData = null;
+        }
+        return ResponseEntity.ok().body(responseData);
+    }
+
+    private static boolean hasTopicParameter(final String topicName) {
+        if (topicName == null) {
+            return false;
+        }
+        if (CpsValidator.validateTopicName(topicName)) {
+            return true;
+        }
+        throw new InvalidTopicException("Topic name " + topicName + " is invalid", "invalid topic");
+    }
+
+    private Map<String, Object> getAsyncResponseData() {
+        final Map<String, Object> asyncResponseData = new HashMap<>(1);
+        final String resourceDataRequestId = UUID.randomUUID().toString();
+        asyncResponseData.put(ASYNC_REQUEST_ID, resourceDataRequestId);
+        return asyncResponseData;
+    }
+
 }
index 8863345..efe0f3a 100644 (file)
@@ -60,7 +60,10 @@ class NetworkCmProxyControllerSpec extends Specification {
     NetworkCmProxyDataService mockNetworkCmProxyDataService = Mock()
 
     @SpringBean
-    JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
+    ObjectMapper objectMapper = new ObjectMapper()
+
+    @SpringBean
+    JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(objectMapper)
 
     @SpringBean
     NcmpRestInputMapper ncmpRestInputMapper = Mappers.getMapper(NcmpRestInputMapper)
@@ -72,6 +75,7 @@ class NetworkCmProxyControllerSpec extends Specification {
 
     @Shared
     def NO_TOPIC = null
+    def NO_REQUEST_ID = null
 
     def 'Get Resource Data from pass-through operational.'() {
         given: 'resource data url'
@@ -86,14 +90,15 @@ class NetworkCmProxyControllerSpec extends Specification {
             1 * mockNetworkCmProxyDataService.getResourceDataOperationalForCmHandle('testCmHandle',
                     'parent/child',
                     '(a=1,b=2)',
-                    NO_TOPIC)
+                    NO_TOPIC,
+                    NO_REQUEST_ID)
         and: 'response status is Ok'
             response.status == HttpStatus.OK.value()
     }
 
-    def 'Get Resource Data from pass-through operational with #scenario.'() {
+    def 'Get Resource Data from #datastoreInUrl with #scenario.'() {
         given: 'resource data url'
-            def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-operational" +
+            def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:${datastoreInUrl}" +
                     "?resourceIdentifier=parent/child&options=(a=1,b=2)${topicQueryParam}"
         when: 'get data resource request is performed'
             def response = mvc.perform(
@@ -101,19 +106,30 @@ class NetworkCmProxyControllerSpec extends Specification {
                     .contentType(MediaType.APPLICATION_JSON)
             ).andReturn().response
         then: 'the NCMP data service is called with operational data for cm handle'
-            1 * mockNetworkCmProxyDataService.getResourceDataOperationalForCmHandle('testCmHandle',
+            expectedNumberOfMethodExecutions
+                    * mockNetworkCmProxyDataService."${expectedMethodName}"('testCmHandle',
                     'parent/child',
                     '(a=1,b=2)',
-                    expectedTopicName)
-        and: 'response status is Ok'
-            response.status == HttpStatus.OK.value()
+                    expectedTopicName,
+                    _)
+        then: 'response status is expected'
+            response.status == expectedHttpStatus
         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="              || ""
+            scenario                               | datastoreInUrl            | topicQueryParam        || expectedTopicName | expectedMethodName                             | expectedNumberOfMethodExecutions | expectedHttpStatus
+            'url with valid topic'                 | 'passthrough-operational' | '&topic=my-topic-name' || 'my-topic-name'   | 'getResourceDataOperationalForCmHandle'        | 1                                | HttpStatus.OK.value()
+            'no topic in url'                      | 'passthrough-operational' | ''                     || NO_TOPIC          | 'getResourceDataOperationalForCmHandle'        | 1                                | HttpStatus.OK.value()
+            'null topic in url'                    | 'passthrough-operational' | '&topic=null'          || 'null'            | 'getResourceDataOperationalForCmHandle'        | 1                                | HttpStatus.OK.value()
+            'empty topic in url'                   | 'passthrough-operational' | '&topic=\"\"'          || null              | 'getResourceDataOperationalForCmHandle'        | 0                                | HttpStatus.BAD_REQUEST.value()
+            'missing topic in url'                 | 'passthrough-operational' | '&topic='              || null              | 'getResourceDataOperationalForCmHandle'        | 0                                | HttpStatus.BAD_REQUEST.value()
+            'blank topic value in url'             | 'passthrough-operational' | '&topic=\" \"'         || null              | 'getResourceDataOperationalForCmHandle'        | 0                                | HttpStatus.BAD_REQUEST.value()
+            'invalid non-empty topic value in url' | 'passthrough-operational' | '&topic=1_5_*_#'       || null              | 'getResourceDataOperationalForCmHandle'        | 0                                | HttpStatus.BAD_REQUEST.value()
+            'url with valid topic'                 | 'passthrough-running'     | '&topic=my-topic-name' || 'my-topic-name'   | 'getResourceDataPassThroughRunningForCmHandle' | 1                                | HttpStatus.OK.value()
+            'no topic in url'                      | 'passthrough-running'     | ''                     || NO_TOPIC          | 'getResourceDataPassThroughRunningForCmHandle' | 1                                | HttpStatus.OK.value()
+            'null topic in url'                    | 'passthrough-running'     | '&topic=null'          || 'null'            | 'getResourceDataPassThroughRunningForCmHandle' | 1                                | HttpStatus.OK.value()
+            'empty topic in url'                   | 'passthrough-running'     | '&topic=\"\"'          || null              | 'getResourceDataPassThroughRunningForCmHandle' | 0                                | HttpStatus.BAD_REQUEST.value()
+            'missing topic in url'                 | 'passthrough-running'     | '&topic='              || null              | 'getResourceDataPassThroughRunningForCmHandle' | 0                                | HttpStatus.BAD_REQUEST.value()
+            'blank topic value in url'             | 'passthrough-running'     | '&topic=\" \"'         || null              | 'getResourceDataPassThroughRunningForCmHandle' | 0                                | HttpStatus.BAD_REQUEST.value()
+            'invalid non-empty topic value in url' | 'passthrough-running'     | '&topic=1_5_*_#'       || null              | 'getResourceDataPassThroughRunningForCmHandle' | 0                                | HttpStatus.BAD_REQUEST.value()
     }
 
     def 'Get Resource Data from pass-through running with #scenario value in resource identifier param.'() {
@@ -124,7 +140,8 @@ class NetworkCmProxyControllerSpec extends Specification {
             mockNetworkCmProxyDataService.getResourceDataPassThroughRunningForCmHandle('testCmHandle',
                     resourceIdentifier,
                     '(a=1,b=2)',
-                    NO_TOPIC) >> '{valid-json}'
+                    NO_TOPIC,
+                    NO_REQUEST_ID) >> '{valid-json}'
         when: 'get data resource request is performed'
             def response = mvc.perform(
                     get(getUrl)
@@ -241,6 +258,31 @@ class NetworkCmProxyControllerSpec extends Specification {
             response.contentAsString == '{"cmHandles":[]}'
     }
 
+    def 'Query for cm handles matching query parameters'() {
+        given: 'an endpoint and json data'
+            def searchesEndpoint = "$ncmpBasePathV1/data/ch/searches"
+            String jsonString = '{"publicCmHandleProperties": {"name": "Contact", "value": "newemailforstore@bookstore.com"}}'
+        and: 'the service method is invoked with module names and returns cm handle ids'
+            1 * mockNetworkCmProxyDataService.queryCmHandles(_) >> ['some-cmhandle-id1', 'some-cmhandle-id2']
+        when: 'the searches api is invoked'
+            def response = mvc.perform(post(searchesEndpoint)
+                .contentType(MediaType.APPLICATION_JSON)
+                .content(jsonString)).andReturn().response
+        then: 'cm handle ids are returned'
+            response.contentAsString == '["some-cmhandle-id1","some-cmhandle-id2"]'
+    }
+
+    def 'Query for cm handles with invalid request payload'() {
+        when: 'the searches api is invoked'
+            def searchesEndpoint = "$ncmpBasePathV1/data/ch/searches"
+            def invalidInputData = '{invalidJson}'
+            def response = mvc.perform(post(searchesEndpoint)
+                    .contentType(MediaType.APPLICATION_JSON)
+                    .content(invalidInputData)).andReturn().response
+        then: 'BAD_REQUEST is returned'
+            response.getStatus() == 400
+    }
+
     def 'Patch resource data in pass-through running datastore.' () {
         given: 'patch resource data url'
             def url = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running" +
@@ -271,5 +313,24 @@ class NetworkCmProxyControllerSpec extends Specification {
         and: 'the response is No Content'
             response.status == HttpStatus.NO_CONTENT.value()
     }
+
+    def 'Get resource data from DMI with valid topic i.e. async request for #scenario'() {
+        given: 'resource data url'
+            def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:${datastoreInUrl}" +
+                    "?resourceIdentifier=parent/child&options=(a=1,b=2)&topic=my-topic-name"
+        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: 'async request id is generated'
+            assert response.contentAsString.contains("requestId")
+        where: 'the following parameters are used'
+            scenario                   | datastoreInUrl
+            ':passthrough-operational' | 'passthrough-operational'
+            ':passthrough-running'     | 'passthrough-running'
+    }
+
 }
 
index d50b8c5..058c42b 100644 (file)
@@ -26,6 +26,8 @@ package org.onap.cps.ncmp.api;
 import static org.onap.cps.ncmp.api.impl.operations.DmiRequestBody.OperationEnum;
 
 import java.util.Collection;
+import java.util.Set;
+import org.onap.cps.ncmp.api.models.CmHandleQueryApiParameters;
 import org.onap.cps.ncmp.api.models.DmiPluginRegistration;
 import org.onap.cps.ncmp.api.models.DmiPluginRegistrationResponse;
 import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle;
@@ -52,12 +54,14 @@ public interface NetworkCmProxyDataService {
      * @param resourceIdentifier resource identifier
      * @param optionsParamInQuery options query
      * @param topicParamInQuery topic name for (triggering) async responses
+     * @param requestId unique requestId for async request
      * @return {@code Object} resource data
      */
     Object getResourceDataOperationalForCmHandle(String cmHandleId,
                                                  String resourceIdentifier,
                                                  String optionsParamInQuery,
-                                                 String topicParamInQuery);
+                                                 String topicParamInQuery,
+                                                 String requestId);
 
     /**
      * Get resource data for data store pass-through running
@@ -66,13 +70,15 @@ public interface NetworkCmProxyDataService {
      * @param cmHandleId cm handle identifier
      * @param resourceIdentifier resource identifier
      * @param optionsParamInQuery options query
-     * @param topicParamInQuery topic query
+     * @param topicParamInQuery topic name for (triggering) async responses
+     * @param requestId unique requestId for async request
      * @return {@code Object} resource data
      */
     Object getResourceDataPassThroughRunningForCmHandle(String cmHandleId,
                                                         String resourceIdentifier,
                                                         String optionsParamInQuery,
-                                                        String topicParamInQuery);
+                                                        String topicParamInQuery,
+                                                        String requestId);
 
     /**
      * Write resource data for data store pass-through running
@@ -115,4 +121,11 @@ public interface NetworkCmProxyDataService {
      */
     NcmpServiceCmHandle getNcmpServiceCmHandle(String cmHandleId);
 
+    /**
+     * Query and return cm handles that match the given query parameters.
+     *
+     * @param cmHandleQueryApiParameters the cm handle query parameters
+     * @return collection of cm handle ids
+     */
+    Set<String> queryCmHandles(CmHandleQueryApiParameters cmHandleQueryApiParameters);
 }
index 81c060e..9c3d944 100755 (executable)
@@ -31,14 +31,14 @@ import static org.onap.cps.ncmp.api.impl.constants.DmiRegistryConstants.NO_TIMES
 import static org.onap.cps.ncmp.api.impl.operations.DmiRequestBody.OperationEnum;
 import static org.onap.cps.spi.CascadeDeleteAllowed.CASCADE_DELETE_ALLOWED;
 
+import com.google.common.base.Strings;
 import java.util.ArrayList;
 import java.util.Collection;
 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.Set;
 import java.util.stream.Collectors;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -47,12 +47,12 @@ import org.onap.cps.api.CpsDataService;
 import org.onap.cps.api.CpsModuleService;
 import org.onap.cps.ncmp.api.NetworkCmProxyDataService;
 import org.onap.cps.ncmp.api.impl.exception.HttpClientRequestException;
-import org.onap.cps.ncmp.api.impl.exception.InvalidTopicException;
 import org.onap.cps.ncmp.api.impl.operations.DmiDataOperations;
 import org.onap.cps.ncmp.api.impl.operations.DmiModelOperations;
 import org.onap.cps.ncmp.api.impl.operations.DmiOperations;
 import org.onap.cps.ncmp.api.impl.operations.YangModelCmHandleRetriever;
 import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle;
+import org.onap.cps.ncmp.api.models.CmHandleQueryApiParameters;
 import org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse;
 import org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.RegistrationError;
 import org.onap.cps.ncmp.api.models.DmiPluginRegistration;
@@ -65,7 +65,6 @@ import org.onap.cps.spi.exceptions.SchemaSetNotFoundException;
 import org.onap.cps.spi.model.ModuleReference;
 import org.onap.cps.utils.CpsValidator;
 import org.onap.cps.utils.JsonObjectMapper;
-import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
 import org.springframework.stereotype.Service;
 
@@ -90,12 +89,6 @@ 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 DmiPluginRegistrationResponse updateDmiRegistrationAndSyncModule(
         final DmiPluginRegistration dmiPluginRegistration) {
@@ -119,20 +112,22 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService
     public Object getResourceDataOperationalForCmHandle(final String cmHandleId,
                                                         final String resourceIdentifier,
                                                         final String optionsParamInQuery,
-                                                        final String topicParamInQuery) {
+                                                        final String topicParamInQuery,
+                                                        final String requestId) {
         CpsValidator.validateNameCharacters(cmHandleId);
-        return validateTopicNameAndGetResourceData(cmHandleId, resourceIdentifier,
-                DmiOperations.DataStoreEnum.PASSTHROUGH_OPERATIONAL, optionsParamInQuery, topicParamInQuery);
+        return getResourceDataResponse(cmHandleId, resourceIdentifier,
+                DmiOperations.DataStoreEnum.PASSTHROUGH_OPERATIONAL, optionsParamInQuery, topicParamInQuery, requestId);
     }
 
     @Override
     public Object getResourceDataPassThroughRunningForCmHandle(final String cmHandleId,
                                                                final String resourceIdentifier,
                                                                final String optionsParamInQuery,
-                                                               final String topicParamInQuery) {
+                                                               final String topicParamInQuery,
+                                                               final String requestId) {
         CpsValidator.validateNameCharacters(cmHandleId);
-        return validateTopicNameAndGetResourceData(cmHandleId, resourceIdentifier,
-                DmiOperations.DataStoreEnum.PASSTHROUGH_RUNNING, optionsParamInQuery, topicParamInQuery);
+        return getResourceDataResponse(cmHandleId, resourceIdentifier,
+                DmiOperations.DataStoreEnum.PASSTHROUGH_RUNNING, optionsParamInQuery, topicParamInQuery, requestId);
     }
 
     @Override
@@ -165,6 +160,20 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService
         return cpsAdminService.queryAnchorNames(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, moduleNames);
     }
 
+    @Override
+    public Set<String> queryCmHandles(final CmHandleQueryApiParameters cmHandleQueryApiParameters) {
+
+        cmHandleQueryApiParameters.getPublicProperties().forEach((key, value) -> {
+            if (Strings.isNullOrEmpty(key)) {
+                throw new DataValidationException("Invalid Query Parameter.",
+                    "Missing property name - please supply a valid name.");
+            }
+        });
+
+        return cpsAdminService.queryCmHandles(jsonObjectMapper.convertToValueType(cmHandleQueryApiParameters,
+                org.onap.cps.spi.model.CmHandleQueryParameters.class));
+    }
+
     /**
      * Retrieve cm handle details for a given cm handle.
      *
@@ -329,35 +338,14 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService
             yangModelCmHandle.getId());
     }
 
-    private static boolean hasTopicParameter(final String topicName) {
-        if (topicName == null) {
-            return false;
-        }
-        if (TOPIC_NAME_PATTERN.matcher(topicName).matches()) {
-            return true;
-        }
-        throw new InvalidTopicException("Topic name " + topicName + " is invalid", "invalid topic");
-    }
-
-    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 DmiOperations.DataStoreEnum dataStore,
-                                                       final String optionsParamInQuery,
-                                                       final String topicParamInQuery) {
-        final boolean processAsynchronously = hasTopicParameter(topicParamInQuery);
-        if (processAsynchronously) {
-            final String resourceDataRequestId = UUID.randomUUID().toString();
-            return ResponseEntity.status(HttpStatus.OK)
-                    .body(buildDmiResponse(resourceDataRequestId));
-        }
+    private Object getResourceDataResponse(final String cmHandleId,
+                                           final String resourceIdentifier,
+                                           final DmiOperations.DataStoreEnum dataStore,
+                                           final String optionsParamInQuery,
+                                           final String topicParamInQuery,
+                                           final String requestId) {
         final ResponseEntity<?> responseEntity = dmiDataOperations.getResourceDataFromDmi(
-                cmHandleId, resourceIdentifier, optionsParamInQuery, dataStore, NO_REQUEST_ID, NO_TOPIC);
+                cmHandleId, resourceIdentifier, optionsParamInQuery, dataStore, requestId, topicParamInQuery);
         return handleResponse(responseEntity, OperationEnum.READ);
     }
 }
\ No newline at end of file
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/CmHandleQueryApiParameters.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/CmHandleQueryApiParameters.java
new file mode 100644 (file)
index 0000000..3f584ed
--- /dev/null
@@ -0,0 +1,41 @@
+/*
+ *  ============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.models;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.Collections;
+import java.util.Map;
+import javax.validation.Valid;
+import lombok.Getter;
+import lombok.Setter;
+
+@Setter
+@Getter
+@JsonInclude(Include.NON_NULL)
+public class CmHandleQueryApiParameters {
+
+    @JsonProperty("publicCmHandleProperties")
+    @Valid
+    private Map<String, String> publicProperties = Collections.emptyMap();
+
+}
index 489c71c..2d01dba 100644 (file)
@@ -23,7 +23,6 @@
 package org.onap.cps.ncmp.api.impl
 
 import org.onap.cps.ncmp.api.impl.exception.HttpClientRequestException
-import org.onap.cps.ncmp.api.impl.exception.InvalidTopicException
 import org.onap.cps.ncmp.api.impl.operations.YangModelCmHandleRetriever
 import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle
 import spock.lang.Shared
@@ -119,7 +118,8 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
             def response = objectUnderTest.getResourceDataOperationalForCmHandle('testCmHandle',
                     'testResourceId',
                     OPTIONS_PARAM,
-                    NO_TOPIC)
+                    NO_TOPIC,
+                    NO_REQUEST_ID)
         then: 'DMI returns a json response'
             response == 'dmi-response'
     }
@@ -137,7 +137,8 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
             objectUnderTest.getResourceDataOperationalForCmHandle('testCmHandle',
                     'testResourceId',
                     OPTIONS_PARAM,
-                    NO_TOPIC)
+                    NO_TOPIC,
+                    NO_REQUEST_ID)
         then: 'exception is thrown with the expected response code and details'
             def exceptionThrown = thrown(HttpClientRequestException.class)
             exceptionThrown.details.contains('NOK-json')
@@ -160,7 +161,8 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
             objectUnderTest.getResourceDataOperationalForCmHandle('testCmHandle',
                     'testResourceId',
                     OPTIONS_PARAM,
-                    NO_TOPIC)
+                    NO_TOPIC,
+                    NO_REQUEST_ID)
         then: 'exception is thrown'
             def exceptionThrown = thrown(HttpClientRequestException.class)
         and: 'details contain the original response'
@@ -183,7 +185,8 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
             def response = objectUnderTest.getResourceDataPassThroughRunningForCmHandle('testCmHandle',
                     'testResourceId',
                     OPTIONS_PARAM,
-                    NO_TOPIC)
+                    NO_TOPIC,
+                    NO_REQUEST_ID)
         then: 'get resource data returns expected response'
             response == '{dmi-response}'
     }
@@ -204,7 +207,8 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
             objectUnderTest.getResourceDataPassThroughRunningForCmHandle('testCmHandle',
                     'testResourceId',
                     OPTIONS_PARAM,
-                    NO_TOPIC)
+                    NO_TOPIC,
+                    NO_REQUEST_ID)
         then: 'exception is thrown'
             def exceptionThrown = thrown(HttpClientRequestException.class)
         and: 'details contain the original response'
@@ -212,72 +216,6 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
             exceptionThrown.httpStatus == HttpStatus.NOT_FOUND.value()
     }
 
-    def 'DMI Operational data request with #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 data operational with blank topic'
-            def responseData = objectUnderTest.getResourceDataOperationalForCmHandle('', '',
-                    '', emptyTopic)
-        then: 'a invalid topic exception is thrown'
-            thrown(InvalidTopicException)
-        where: 'the following parameters are used'
-            scenario                               | emptyTopic
-            'no topic value in url'                | ''
-            'empty topic value in url'             | '\"\"'
-            'blank topic value in url'             | ' '
-            'invalid non-empty topic value in url' | '1_5_*_#'
-    }
-
-    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 'DMI pass through running data request with #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: 'a invalid topic exception is thrown'
-            thrown(InvalidTopicException)
-        where: 'the following parameters are used'
-            scenario                               | emptyTopic
-            'no topic value in url'                | ''
-            'empty topic value in url'             | '\"\"'
-            'blank topic value in url'             | ' '
-            'invalid non-empty topic value in url' | '1_5_*_#'
-    }
-
     def 'Getting Yang Resources.'() {
         when: 'yang resources is called'
             objectUnderTest.getYangResourcesModuleReferences('some-cm-handle')
index 50b2720..2e7bb7e 100755 (executable)
@@ -24,6 +24,7 @@ package org.onap.cps.spi.impl;
 
 import java.util.Collection;
 import java.util.List;
+import java.util.Set;
 import java.util.stream.Collectors;
 import javax.transaction.Transactional;
 import lombok.AllArgsConstructor;
@@ -36,8 +37,10 @@ import org.onap.cps.spi.exceptions.AlreadyDefinedException;
 import org.onap.cps.spi.exceptions.DataspaceInUseException;
 import org.onap.cps.spi.exceptions.ModuleNamesNotFoundException;
 import org.onap.cps.spi.model.Anchor;
+import org.onap.cps.spi.model.CmHandleQueryParameters;
 import org.onap.cps.spi.repository.AnchorRepository;
 import org.onap.cps.spi.repository.DataspaceRepository;
+import org.onap.cps.spi.repository.ModuleReferenceRepository;
 import org.onap.cps.spi.repository.SchemaSetRepository;
 import org.onap.cps.spi.repository.YangResourceRepository;
 import org.springframework.dao.DataIntegrityViolationException;
@@ -51,6 +54,7 @@ public class CpsAdminPersistenceServiceImpl implements CpsAdminPersistenceServic
     private final AnchorRepository anchorRepository;
     private final SchemaSetRepository schemaSetRepository;
     private final YangResourceRepository yangResourceRepository;
+    private final ModuleReferenceRepository moduleReferenceRepository;
 
     @Override
     public void createDataspace(final String dataspaceName) {
@@ -132,6 +136,11 @@ public class CpsAdminPersistenceServiceImpl implements CpsAdminPersistenceServic
         anchorRepository.delete(anchorEntity);
     }
 
+    @Override
+    public Set<String> queryCmHandles(final CmHandleQueryParameters cmHandleQueryParameters) {
+        return moduleReferenceRepository.queryCmHandles(cmHandleQueryParameters);
+    }
+
     private AnchorEntity getAnchorEntity(final String dataspaceName, final String anchorName) {
         final var dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
         return anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
index 6551937..4bc9dd9 100644 (file)
@@ -21,6 +21,8 @@
 package org.onap.cps.spi.repository;
 
 import java.util.Collection;
+import java.util.Set;
+import org.onap.cps.spi.model.CmHandleQueryParameters;
 import org.onap.cps.spi.model.ModuleReference;
 
 /**
@@ -31,4 +33,12 @@ public interface ModuleReferenceQuery {
     Collection<ModuleReference> identifyNewModuleReferences(
         final Collection<ModuleReference> moduleReferencesToCheck);
 
+    /**
+     * Query and return cm handles that match the given query parameters.
+     *
+     * @param cmHandleQueryParameters the cm handle query parameters
+     * @return collection of cm handle ids
+     */
+    Set<String> queryCmHandles(CmHandleQueryParameters cmHandleQueryParameters);
+
 }
index ce2bfe7..f70e218 100644 (file)
@@ -27,8 +27,7 @@ import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.stereotype.Repository;
 
 @Repository
-public interface ModuleReferenceRepository extends
-    JpaRepository<YangResourceEntity, Long>, ModuleReferenceQuery {
+public interface ModuleReferenceRepository extends JpaRepository<YangResourceEntity, Long>, ModuleReferenceQuery {
 
     Collection<ModuleReference> identifyNewModuleReferences(
         final Collection<ModuleReference> moduleReferencesToCheck);
index 0e79deb..40a93da 100644 (file)
@@ -24,21 +24,32 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 import java.util.UUID;
+import java.util.stream.Collectors;
 import javax.persistence.EntityManager;
 import javax.persistence.PersistenceContext;
+import lombok.AllArgsConstructor;
 import lombok.SneakyThrows;
 import lombok.extern.slf4j.Slf4j;
+import org.onap.cps.spi.CpsDataPersistenceService;
+import org.onap.cps.spi.FetchDescendantsOption;
+import org.onap.cps.spi.model.CmHandleQueryParameters;
+import org.onap.cps.spi.model.DataNode;
 import org.onap.cps.spi.model.ModuleReference;
 import org.springframework.transaction.annotation.Transactional;
 
 @Slf4j
 @Transactional
+@AllArgsConstructor
 public class ModuleReferenceRepositoryImpl implements ModuleReferenceQuery {
 
     @PersistenceContext
     private EntityManager entityManager;
 
+    private final CpsDataPersistenceService cpsDataPersistenceService;
+
     @Override
     @SneakyThrows
     public Collection<ModuleReference> identifyNewModuleReferences(
@@ -57,6 +68,56 @@ public class ModuleReferenceRepositoryImpl implements ModuleReferenceQuery {
         return identifyNewModuleReferencesForCmHandle(tempTableName);
     }
 
+    /**
+     * Query and return cm handles that match the given query parameters.
+     *
+     * @param cmHandleQueryParameters the cm handle query parameters
+     * @return collection of cm handle ids
+     */
+    @Override
+    public Set<String> queryCmHandles(final CmHandleQueryParameters cmHandleQueryParameters) {
+
+        if (cmHandleQueryParameters.getPublicProperties().entrySet().isEmpty()) {
+            return getAllCmHandles();
+        }
+
+        final Collection<DataNode> amalgamatedQueryResult = new ArrayList<>();
+        int queryConditionCounter = 0;
+        for (final Map.Entry<String, String> entry : cmHandleQueryParameters.getPublicProperties().entrySet()) {
+            final StringBuilder cmHandlePath = new StringBuilder();
+            cmHandlePath.append("//public-properties[@name='").append(entry.getKey()).append("' ");
+            cmHandlePath.append("and @value='").append(entry.getValue()).append("']");
+            cmHandlePath.append("/ancestor::cm-handles");
+
+            final Collection<DataNode> singleConditionQueryResult =
+                cpsDataPersistenceService.queryDataNodes("NCMP-Admin",
+                "ncmp-dmi-registry", String.valueOf(cmHandlePath), FetchDescendantsOption.OMIT_DESCENDANTS);
+            if (++queryConditionCounter == 1) {
+                amalgamatedQueryResult.addAll(singleConditionQueryResult);
+            } else {
+                amalgamatedQueryResult.retainAll(singleConditionQueryResult);
+            }
+
+            if (amalgamatedQueryResult.isEmpty()) {
+                break;
+            }
+        }
+
+        return extractCmHandleIds(amalgamatedQueryResult);
+    }
+
+    private Set<String> getAllCmHandles() {
+        final Collection<DataNode> cmHandles = cpsDataPersistenceService.queryDataNodes("NCMP-Admin",
+            "ncmp-dmi-registry", "//public-properties/ancestor::cm-handles",
+            FetchDescendantsOption.OMIT_DESCENDANTS);
+        return extractCmHandleIds(cmHandles);
+    }
+
+    private Set<String> extractCmHandleIds(final Collection<DataNode> cmHandles) {
+        return cmHandles.stream().map(cmHandle -> cmHandle.getLeaves().get("id").toString())
+            .collect(Collectors.toSet());
+    }
+
     private void createTemporaryTable(final String tempTableName) {
         final StringBuilder sqlStringBuilder = new StringBuilder("CREATE TEMPORARY TABLE " + tempTableName + "(");
         sqlStringBuilder.append(" id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,");
@@ -95,7 +156,7 @@ public class ModuleReferenceRepositoryImpl implements ModuleReferenceQuery {
                 + " WHERE yang_resource.module_name IS NULL;", tempTableName);
 
         final List<Object[]> resultsAsObjects =
-            entityManager.createNativeQuery(sql).getResultList();
+            (List<Object[]>) entityManager.createNativeQuery(sql).getResultList();
 
         final List<ModuleReference> resultsAsModuleReferences = new ArrayList<>(resultsAsObjects.size());
         for (final Object[] row : resultsAsObjects) {
index 063bd5b..f486cb7 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2021 Nordix Foundation
+ *  Copyright (C) 2021-2022 Nordix Foundation
  *  Modifications Copyright (C) 2021 Pantheon.tech
  *  Modifications Copyright (C) 2022 Bell Canada
  *  ================================================================================
@@ -22,6 +22,7 @@
 
 package org.onap.cps.spi.impl
 
+import org.mockito.Mock
 import org.onap.cps.spi.CpsAdminPersistenceService
 import org.onap.cps.spi.exceptions.AlreadyDefinedException
 import org.onap.cps.spi.exceptions.AnchorNotFoundException
@@ -30,15 +31,21 @@ import org.onap.cps.spi.exceptions.DataspaceNotFoundException
 import org.onap.cps.spi.exceptions.SchemaSetNotFoundException
 import org.onap.cps.spi.exceptions.ModuleNamesNotFoundException
 import org.onap.cps.spi.model.Anchor
+import org.onap.cps.spi.model.CmHandleQueryParameters
 import org.springframework.beans.factory.annotation.Autowired
 import org.springframework.test.context.jdbc.Sql
+import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper
 
 class CpsAdminPersistenceServiceSpec extends CpsPersistenceSpecBase {
 
     @Autowired
     CpsAdminPersistenceService objectUnderTest
 
+    @Mock
+    ObjectMapper objectMapper
+
     static final String SET_DATA = '/data/anchor.sql'
+    static final String SET_FRAGMENT_DATA = '/data/fragment.sql'
     static final String SAMPLE_DATA_FOR_ANCHORS_WITH_MODULES = '/data/anchors-schemaset-modules.sql'
     static final String DATASPACE_WITH_NO_DATA = 'DATASPACE-002-NO-DATA'
     static final Integer DELETED_ANCHOR_ID = 3002
@@ -219,4 +226,20 @@ class CpsAdminPersistenceServiceSpec extends CpsPersistenceSpecBase {
             'dataspace contains schemasets' | 'DATASPACE-003' || DataspaceInUseException    | 'contains 1 schemaset(s)'
     }
 
+    @Sql([CLEAR_DATA, SET_FRAGMENT_DATA])
+    def 'Retrieve cm handle ids when #scenario.'() {
+        when: 'the service is invoked'
+            def cmHandleQueryParameters = new CmHandleQueryParameters()
+            cmHandleQueryParameters.setPublicProperties(publicProperties)
+            def returnedCmHandles = objectUnderTest.queryCmHandles(cmHandleQueryParameters)
+        then: 'the correct expected cm handles are returned'
+            returnedCmHandles == expectedCmHandleIds
+        where: 'the following data is used'
+            scenario                                       | publicProperties                                                                              || expectedCmHandleIds
+            'single matching property'                     | ['Contact' : 'newemailforstore@bookstore.com']                                                || ['PNFDemo2', 'PNFDemo', 'PNFDemo4'] as Set
+            'public property dont match'                   | ['wont_match' : 'wont_match']                                                                 || [] as Set
+            '2 properties, only one match (and)'           | ['Contact' : 'newemailforstore@bookstore.com', 'Contact2': 'newemailforstore2@bookstore.com'] || ['PNFDemo4'] as Set
+            '2 properties, no match (and)'                 | ['Contact' : 'newemailforstore@bookstore.com', 'Contact2': '']                                || [] as Set
+            'No public properties - return all cm handles' | [ : ]                                                                                         || ['PNFDemo3', 'PNFDemo', 'PNFDemo2', 'PNFDemo4'] as Set
+    }
 }
index a27bb5f..fb4a5f7 100755 (executable)
@@ -1,6 +1,6 @@
 /*
    ============LICENSE_START=======================================================
-    Copyright (C) 2021 Nordix Foundation.
+    Copyright (C) 2021-2022 Nordix Foundation.
     Modifications Copyright (C) 2021 Pantheon.tech
     Modifications Copyright (C) 2021-2022 Bell Canada.
    ================================================================================
 */
 
 INSERT INTO DATASPACE (ID, NAME) VALUES
-    (1001, 'DATASPACE-001');
+    (1001, 'DATASPACE-001'),
+    (1002, 'NCMP-Admin');
 
 INSERT INTO SCHEMA_SET (ID, NAME, DATASPACE_ID) VALUES
     (2001, 'SCHEMA-SET-001', 1001);
 
 INSERT INTO ANCHOR (ID, NAME, DATASPACE_ID, SCHEMA_SET_ID) VALUES
     (3001, 'ANCHOR-001', 1001, 2001),
-    (3003, 'ANCHOR-003', 1001, 2001);
+    (3003, 'ANCHOR-003', 1001, 2001),
+    (3004, 'ncmp-dmi-registry', 1002, 2001);
 
 INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH) VALUES
     (4001, 1001, 3001, null, '/parent-1'),
@@ -67,4 +69,15 @@ INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH, ATTRIBUTES)
     (4230, 1001, 3003, 4227, '/parent-206/child-206/grand-child-206[@key="X"]', '{"key": "X"}'),
     (4231, 1001, 3003, null, '/parent-206[@key="A"]', '{"key": "A"}'),
     (4232, 1001, 3003, 4231, '/parent-206[@key="A"]/child-206', '{}'),
-    (4233, 1001, 3003, null, '/parent-206[@key="B"]', '{"key": "B"}');
\ No newline at end of file
+    (4233, 1001, 3003, null, '/parent-206[@key="B"]', '{"key": "B"}');
+
+INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH, ATTRIBUTES) VALUES
+    (5000, 1002, 3004, null, '/dmi-registry/cm-handles[@id="PNFDemo"]', '{"id": "PNFDemo", "dmi-service-name": "http://172.21.235.14:8783", "dmi-data-service-name": "", "dmi-model-service-name": ""}'),
+    (5001, 1002, 3004, null, '/dmi-registry/cm-handles[@id="PNFDemo2"]', '{"id": "PNFDemo2", "dmi-service-name": "http://172.26.46.68:8783", "dmi-data-service-name": "", "dmi-model-service-name": ""}'),
+    (5002, 1002, 3004, null, '/dmi-registry/cm-handles[@id="PNFDemo3"]', '{"id": "PNFDemo3", "dmi-service-name": "http://172.26.46.68:8783", "dmi-data-service-name": "", "dmi-model-service-name": ""}'),
+    (5003, 1002, 3004, null, '/dmi-registry/cm-handles[@id="PNFDemo4"]', '{"id": "PNFDemo4", "dmi-service-name": "http://172.26.46.68:8783", "dmi-data-service-name": "", "dmi-model-service-name": ""}'),
+    (5004, 1002, 3004, 5000, '/dmi-registry/cm-handles[@id="PNFDemo"]/public-properties[@name="Contact"]', '{"name": "Contact", "value": "newemailforstore@bookstore.com"}'),
+    (5005, 1002, 3004, 5001, '/dmi-registry/cm-handles[@id="PNFDemo2"]/public-properties[@name="Contact"]', '{"name": "Contact", "value": "newemailforstore@bookstore.com"}'),
+    (5006, 1002, 3004, 5002, '/dmi-registry/cm-handles[@id="PNFDemo3"]/public-properties[@name="Contact"]', '{"name": "Contact3", "value": "PNF3@bookstore.com"}'),
+    (5007, 1002, 3004, 5003, '/dmi-registry/cm-handles[@id="PNFDemo4"]/public-properties[@name="Contact"]', '{"name": "Contact", "value": "newemailforstore@bookstore.com"}'),
+    (5008, 1002, 3004, 5004, '/dmi-registry/cm-handles[@id="PNFDemo4"]/public-properties[@name="Contact2"]', '{"name": "Contact2", "value": "newemailforstore2@bookstore.com"}');
\ No newline at end of file
index 2cb01ac..2106f15 100755 (executable)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2020-2021 Nordix Foundation
+ *  Copyright (C) 2020-2022 Nordix Foundation
  *  Modifications Copyright (C) 2020-2022 Bell Canada.
  *  Modifications Copyright (C) 2021 Pantheon.tech
  *  ================================================================================
 package org.onap.cps.api;
 
 import java.util.Collection;
+import java.util.Set;
 import org.onap.cps.spi.exceptions.AlreadyDefinedException;
 import org.onap.cps.spi.exceptions.CpsException;
 import org.onap.cps.spi.model.Anchor;
+import org.onap.cps.spi.model.CmHandleQueryParameters;
 
 /**
  * CPS Admin Service.
@@ -100,4 +102,12 @@ public interface CpsAdminService {
      *         given module names
      */
     Collection<String> queryAnchorNames(String dataspaceName, Collection<String> moduleNames);
+
+    /**
+     * Query and return cm handles that match the given query parameters.
+     *
+     * @param cmHandleQueryParameters the cm handle query parameters
+     * @return collection of cm handle ids
+     */
+    Set<String> queryCmHandles(CmHandleQueryParameters cmHandleQueryParameters);
 }
index 7bec1e3..762754f 100755 (executable)
@@ -24,12 +24,14 @@ package org.onap.cps.api.impl;
 
 import java.time.OffsetDateTime;
 import java.util.Collection;
+import java.util.Set;
 import java.util.stream.Collectors;
 import lombok.AllArgsConstructor;
 import org.onap.cps.api.CpsAdminService;
 import org.onap.cps.api.CpsDataService;
 import org.onap.cps.spi.CpsAdminPersistenceService;
 import org.onap.cps.spi.model.Anchor;
+import org.onap.cps.spi.model.CmHandleQueryParameters;
 import org.onap.cps.utils.CpsValidator;
 import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Component;
@@ -91,4 +93,9 @@ public class CpsAdminServiceImpl implements CpsAdminService {
         final Collection<Anchor> anchors = cpsAdminPersistenceService.queryAnchors(dataspaceName, moduleNames);
         return anchors.stream().map(Anchor::getName).collect(Collectors.toList());
     }
+
+    @Override
+    public Set<String> queryCmHandles(final CmHandleQueryParameters cmHandleQueryParameters) {
+        return cpsAdminPersistenceService.queryCmHandles(cmHandleQueryParameters);
+    }
 }
index dd4059d..25167e8 100755 (executable)
@@ -1,6 +1,6 @@
 /*
  * ============LICENSE_START=======================================================
- *  Copyright (C) 2020 Nordix Foundation.
+ *  Copyright (C) 2020-2022 Nordix Foundation.
  *  Modifications Copyright (C) 2020-2022 Bell Canada.
  *  Modifications Copyright (C) 2021 Pantheon.tech
  * ================================================================================
 package org.onap.cps.spi;
 
 import java.util.Collection;
+import java.util.Set;
 import org.onap.cps.spi.exceptions.AlreadyDefinedException;
 import org.onap.cps.spi.model.Anchor;
+import org.onap.cps.spi.model.CmHandleQueryParameters;
 
 /*
     Service for handling CPS admin data.
@@ -99,4 +101,12 @@ public interface CpsAdminPersistenceService {
      * @param anchorName anchor name
      */
     void deleteAnchor(String dataspaceName, String anchorName);
+
+    /**
+     * Query and return cm handles that match the given query parameters.
+     *
+     * @param cmHandleQueryParameters the cm handle query parameters
+     * @return collection of cm handle ids
+     */
+    Set<String> queryCmHandles(CmHandleQueryParameters cmHandleQueryParameters);
 }
diff --git a/cps-service/src/main/java/org/onap/cps/spi/model/CmHandleQueryParameters.java b/cps-service/src/main/java/org/onap/cps/spi/model/CmHandleQueryParameters.java
new file mode 100644 (file)
index 0000000..ff4e627
--- /dev/null
@@ -0,0 +1,41 @@
+/*
+ *  ============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.spi.model;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.Collections;
+import java.util.Map;
+import javax.validation.Valid;
+import lombok.Getter;
+import lombok.Setter;
+
+@Setter
+@Getter
+@JsonInclude(Include.NON_NULL)
+public class CmHandleQueryParameters {
+
+    @JsonProperty("publicCmHandleProperties")
+    @Valid
+    private Map<String, String> publicProperties = Collections.emptyMap();
+
+}
index 55e7b99..43aa06b 100644 (file)
@@ -26,11 +26,13 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.Map;
 import lombok.AccessLevel;
+import lombok.EqualsAndHashCode;
 import lombok.Getter;
 import lombok.Setter;
 
 @Setter(AccessLevel.PROTECTED)
 @Getter
+@EqualsAndHashCode
 public class DataNode {
 
     DataNode() {    }
index dd16495..28b49c9 100644 (file)
@@ -22,6 +22,7 @@ package org.onap.cps.utils;
 
 import com.google.common.collect.Lists;
 import java.util.Collection;
+import java.util.regex.Pattern;
 import lombok.AccessLevel;
 import lombok.NoArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -32,6 +33,8 @@ import org.onap.cps.spi.exceptions.DataValidationException;
 public final class CpsValidator {
 
     private static final char[] UNSUPPORTED_NAME_CHARACTERS = "!\" #$%&'()*+,./\\:;<=>?@[]^`{|}~".toCharArray();
+    private static final Pattern TOPIC_NAME_PATTERN = Pattern.compile("^[a-zA-Z0-9]([._-](?![._-])|"
+            + "[a-zA-Z0-9]){0,120}[a-zA-Z0-9]$");
 
     /**
      * Validate characters in names within cps.
@@ -48,4 +51,12 @@ public final class CpsValidator {
             }
         }
     }
+
+    /**
+     * Validate kafka topic name pattern.
+     * @param topicName name of the topic to be validated
+     */
+    public static boolean validateTopicName(final String topicName) {
+        return topicName != null && TOPIC_NAME_PATTERN.matcher(topicName).matches();
+    }
 }
index bb122d1..cbe1ebb 100755 (executable)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2020 Nordix Foundation
+ *  Copyright (C) 2020-2022 Nordix Foundation
  *  Modifications Copyright (C) 2020-2022 Bell Canada.
  *  Modifications Copyright (C) 2021 Pantheon.tech
  *  ================================================================================
@@ -25,6 +25,7 @@ package org.onap.cps.api.impl
 import org.onap.cps.api.CpsDataService
 import org.onap.cps.spi.CpsAdminPersistenceService
 import org.onap.cps.spi.model.Anchor
+import org.onap.cps.spi.model.CmHandleQueryParameters
 import spock.lang.Specification
 import java.time.OffsetDateTime
 
@@ -95,4 +96,13 @@ class CpsAdminServiceImplSpec extends Specification {
             1 * mockCpsAdminPersistenceService.deleteDataspace('someDataspace')
     }
 
+    def 'Query CM Handles.'() {
+        given: 'a cm handle query'
+            def cmHandleQueryParameters = new CmHandleQueryParameters()
+        when: 'query cm handles is invoked'
+            objectUnderTest.queryCmHandles(cmHandleQueryParameters)
+        then: 'associated persistence service method is invoked with correct parameter'
+            1 * mockCpsAdminPersistenceService.queryCmHandles(cmHandleQueryParameters)
+    }
+
 }
index 191472c..ce728ef 100644 (file)
@@ -45,4 +45,18 @@ class CpsValidatorSpec extends Specification {
             'position 5' | 'name with spaces' || 'name with spaces invalid token encountered at position 5'
             'position 9' | 'nameWith Space'   || 'nameWith Space invalid token encountered at position 9'
     }
+
+    def 'Validating topic names.'() {
+        when: 'the topic name is validated'
+            def isValidTopicName = CpsValidator.validateTopicName(topicName)
+        then: 'boolean response will be returned for #scenario'
+            assert isValidTopicName == booleanResponse
+        where: 'the following names are used'
+            scenario                  | topicName       || booleanResponse
+            'valid topic'             | 'my-topic-name' || true
+            'empty topic'             | ''              || false
+            'blank topic'             | ' '             || false
+            'null topic'              | null            || false
+            'invalid non empty topic' | '1_5_*_#'       || false
+    }
 }
diff --git a/csit/data/cmHandleRegistration.json b/csit/data/cmHandleRegistration.json
deleted file mode 100644 (file)
index 0133148..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-{
-    "cmHandles": [
-        "PNFDemo"
-    ]
-}
\ No newline at end of file
index 8069bb7..d4615e7 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.
@@ -17,8 +17,8 @@
 # Test suites are relative paths under csit/tests/.
 # Place the suites in run order.
 actuator
-cps-model-sync
-ncmp-passthrough
 cps-admin
 cps-data
-
+cps-model-sync
+ncmp-passthrough
+public-properties-query
\ No newline at end of file
index 7de1f3a..ea082b5 100644 (file)
@@ -34,7 +34,7 @@ ${auth}                   Basic Y3BzdXNlcjpjcHNyMGNrcyE=
 ${ncmpInventoryBasePath}  /ncmpInventory
 ${ncmpBasePath}           /ncmp
 ${dmiUrl}                 http://${DMI_HOST}:${DMI_PORT}
-${jsonDataCreate}         {"dmiPlugin":"${dmiUrl}","dmiDataPlugin":"","dmiModelPlugin":"","createdCmHandles":[{"cmHandle":"PNFDemo","cmHandleProperties":{"Book1":"Sci-Fi Book"},"publicCmHandleProperties":{"Contact":"storeemail@bookstore.com"}}]}
+${jsonDataCreate}         {"dmiPlugin":"${dmiUrl}","dmiDataPlugin":"","dmiModelPlugin":"","createdCmHandles":[{"cmHandle":"PNFDemo","cmHandleProperties":{"Book1":"Sci-Fi Book"},"publicCmHandleProperties":{"Contact":"storeemail@bookstore.com", "Contact2":"storeemail2@bookstore.com"}}]}
 ${jsonDataUpdate}         {"dmiPlugin":"${dmiUrl}","dmiDataPlugin":"","dmiModelPlugin":"","updatedCmHandles":[{"cmHandle":"PNFDemo","cmHandleProperties":{"Book1":"Romance Book"},"publicCmHandleProperties":{"Contact":"newemailforstore@bookstore.com"}}]}
 
 *** Test Cases ***
diff --git a/csit/tests/public-properties-query/public-properties-query.robot b/csit/tests/public-properties-query/public-properties-query.robot
new file mode 100644 (file)
index 0000000..3a64087
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ *  ============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=========================================================
+ */
+
+*** Settings ***
+Documentation         Public Properties Query Test
+
+Library               Collections
+Library               OperatingSystem
+Library               RequestsLibrary
+Library               BuiltIn
+
+Suite Setup           Create Session      CPS_URL    http://${CPS_CORE_HOST}:${CPS_CORE_PORT}
+
+*** Variables ***
+
+${auth}                                     Basic Y3BzdXNlcjpjcHNyMGNrcyE=
+${ncmpBasePath}                             /ncmp/v1
+${jsonMatchingQueryParameters}              {"publicCmHandleProperties": {"Contact" : "newemailforstore@bookstore.com", "Contact2" : "storeemail2@bookstore.com"}}
+${jsonMissingPropertyQueryParameters}       {"publicCmHandleProperties": { "" : "doesnt matter"}}
+
+*** Test Cases ***
+Retrieve CM Handles where query parameters Match
+    ${uri}=              Set Variable       ${ncmpBasePath}/data/ch/searches
+    ${headers}=          Create Dictionary  Content-Type=application/json   Authorization=${auth}
+    ${response}=         POST On Session    CPS_URL   ${uri}   headers=${headers}   data=${jsonMatchingQueryParameters}
+    ${responseJson}=     Set Variable       ${response.json()}
+    Should Be Equal As Strings              ${response.status_code}   200
+    Should Contain       ${responseJson}    PNFDemo
+
+Throw 400 when Structure of Request is Incorrect
+    ${uri}=              Set Variable       ${ncmpBasePath}/data/ch/searches
+    ${headers}=          Create Dictionary  Content-Type=application/json   Authorization=${auth}
+    ${response}=         POST On Session    CPS_URL   ${uri}   headers=${headers}   data=${jsonMissingPropertyQueryParameters}    expected_status=400
+    Should Be Equal As Strings              ${response}   <Response [400]>