Merge "Condense Liquibase steps"
authorToine Siebelink <toine.siebelink@est.tech>
Tue, 26 Mar 2024 16:38:50 +0000 (16:38 +0000)
committerGerrit Code Review <gerrit@onap.org>
Tue, 26 Mar 2024 16:38:50 +0000 (16:38 +0000)
27 files changed:
cps-dependencies/pom.xml
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/controller/handlers/NcmpCachedResourceRequestHandler.java
cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/handlers/NcmpDatastoreRequestHandler.java
cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/handlers/NcmpPassthroughResourceRequestHandler.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/controller/handlers/NcmpDatastoreRequestHandlerSpec.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/events/cmsubscription/service/CmNotificationSubscriptionPersistenceService.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/service/CmNotificationSubscriptionPersistenceServiceImpl.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/utils/AlternateIdChecker.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/CmResourceAddress.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/events/cmsubscription/service/CmNotificationSubscriptionPersistenceServiceImplSpec.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/utils/AlternateIdCheckerSpec.groovy
cps-parent/pom.xml
docker-compose/docker-compose.yml
docker-compose/postgres-init.sql [new file with mode: 0644]
docs/deployment.rst
integration-test/pom.xml
integration-test/src/test/groovy/org/onap/cps/integration/base/CpsIntegrationSpecBase.groovy
integration-test/src/test/groovy/org/onap/cps/integration/functional/NcmpCmHandleCreateSpec.groovy
integration-test/src/test/groovy/org/onap/cps/integration/functional/NcmpCmNotificationSubscriptionSpec.groovy [new file with mode: 0644]
integration-test/src/test/java/org/onap/cps/integration/KafkaTestContainer.java [new file with mode: 0644]

index 69ea859..68f36fb 100644 (file)
@@ -2,7 +2,7 @@
 <!--
   ============LICENSE_START=======================================================
   Copyright (c) 2021 Linux Foundation.
-  Modifications Copyright (C) 2020-2023 Nordix Foundation
+  Modifications Copyright (C) 2020-2024 Nordix Foundation
   Modifications Copyright (C) 2022 Bell Canada.
   ================================================================================
   Licensed under the Apache License, Version 2.0 (the "License");
             <dependency>
                 <groupId>com.github.spotbugs</groupId>
                 <artifactId>spotbugs</artifactId>
-                <version>4.2.0</version>
+                <version>4.2.3</version>
             </dependency>
             <dependency>
                 <groupId>com.google.code.gson</groupId>
index 66c1591..93cbccf 100755 (executable)
@@ -44,6 +44,7 @@ import org.onap.cps.ncmp.api.impl.inventory.CompositeState;
 import org.onap.cps.ncmp.api.impl.operations.DatastoreType;
 import org.onap.cps.ncmp.api.impl.trustlevel.TrustLevel;
 import org.onap.cps.ncmp.api.models.CmHandleQueryApiParameters;
+import org.onap.cps.ncmp.api.models.CmResourceAddress;
 import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle;
 import org.onap.cps.ncmp.rest.api.NetworkCmProxyApi;
 import org.onap.cps.ncmp.rest.controller.handlers.NcmpCachedResourceRequestHandler;
@@ -107,8 +108,9 @@ public class NetworkCmProxyController implements NetworkCmProxyApi {
                                                              final Boolean includeDescendants,
                                                              final String authorization) {
         final NcmpDatastoreRequestHandler ncmpDatastoreRequestHandler = getNcmpDatastoreRequestHandler(datastoreName);
-        return ncmpDatastoreRequestHandler.executeRequest(datastoreName, cmHandle, resourceIdentifier,
-                optionsParamInQuery, topicParamInQuery, includeDescendants, authorization);
+        final CmResourceAddress cmResourceAddress = new CmResourceAddress(datastoreName, cmHandle, resourceIdentifier);
+        return ncmpDatastoreRequestHandler.executeRequest(cmResourceAddress, optionsParamInQuery, topicParamInQuery,
+            includeDescendants, authorization);
     }
 
     @Override
index 430c099..e6d6faf 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2022-2023 Nordix Foundation
+ *  Copyright (C) 2022-2024 Nordix Foundation
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@ package org.onap.cps.ncmp.rest.controller.handlers;
 import java.util.function.Supplier;
 import org.onap.cps.ncmp.api.NetworkCmProxyDataService;
 import org.onap.cps.ncmp.api.NetworkCmProxyQueryService;
+import org.onap.cps.ncmp.api.models.CmResourceAddress;
 import org.onap.cps.ncmp.rest.executor.CpsNcmpTaskExecutor;
 import org.onap.cps.spi.FetchDescendantsOption;
 import org.springframework.http.ResponseEntity;
@@ -68,9 +69,7 @@ public class NcmpCachedResourceRequestHandler extends NcmpDatastoreRequestHandle
     }
 
     @Override
-    protected Supplier<Object> getTaskSupplierForGetRequest(final String datastoreName,
-                                                  final String cmHandleId,
-                                                  final String resourceIdentifier,
+    protected Supplier<Object> getTaskSupplierForGetRequest(final CmResourceAddress cmResourceAddress,
                                                   final String optionsParamInQuery,
                                                   final String topicParamInQuery,
                                                   final String requestId,
@@ -79,8 +78,7 @@ public class NcmpCachedResourceRequestHandler extends NcmpDatastoreRequestHandle
 
         final FetchDescendantsOption fetchDescendantsOption = getFetchDescendantsOption(includeDescendants);
 
-        return () -> networkCmProxyDataService.getResourceDataForCmHandle(datastoreName, cmHandleId, resourceIdentifier,
-            fetchDescendantsOption);
+        return () -> networkCmProxyDataService.getResourceDataForCmHandle(cmResourceAddress, fetchDescendantsOption);
     }
 
     private Supplier<Object> getTaskSupplierForQueryRequest(final String cmHandleId,
index 65410d3..1ae1682 100644 (file)
@@ -25,6 +25,7 @@ import java.util.UUID;
 import java.util.function.Supplier;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.onap.cps.ncmp.api.models.CmResourceAddress;
 import org.onap.cps.ncmp.rest.executor.CpsNcmpTaskExecutor;
 import org.onap.cps.ncmp.rest.util.TopicValidator;
 import org.springframework.beans.factory.annotation.Value;
@@ -50,18 +51,14 @@ public abstract class NcmpDatastoreRequestHandler {
     /**
      * Executes synchronous/asynchronous get request for given cm handle.
      *
-     * @param datastoreName       the name of the datastore
-     * @param cmHandleId          the cm handle
-     * @param resourceIdentifier  the resource identifier
+     * @param cmResourceAddress   the name of the datastore, cm handle and resource identifier
      * @param optionsParamInQuery the options param in query
      * @param topicParamInQuery   the topic param in query
      * @param includeDescendants  whether include descendants
      * @param authorization       contents of Authorization header, or null if not present
      * @return the response entity
      */
-    public ResponseEntity<Object> executeRequest(final String datastoreName,
-                                                 final String cmHandleId,
-                                                 final String resourceIdentifier,
+    public ResponseEntity<Object> executeRequest(final CmResourceAddress cmResourceAddress,
                                                  final String optionsParamInQuery,
                                                  final String topicParamInQuery,
                                                  final boolean includeDescendants,
@@ -69,16 +66,16 @@ public abstract class NcmpDatastoreRequestHandler {
 
         final boolean asyncResponseRequested = topicParamInQuery != null;
         if (asyncResponseRequested && notificationFeatureEnabled) {
-            return executeAsyncTaskAndGetResponseEntity(datastoreName, cmHandleId, resourceIdentifier,
-                optionsParamInQuery, topicParamInQuery, includeDescendants, authorization);
+            return executeAsyncTaskAndGetResponseEntity(cmResourceAddress, optionsParamInQuery, topicParamInQuery,
+                includeDescendants, authorization);
         }
 
         if (asyncResponseRequested) {
             log.warn("Asynchronous request is unavailable as notification feature is currently disabled, "
                     + "will use synchronous operation.");
         }
-        final Supplier<Object> taskSupplier = getTaskSupplierForGetRequest(datastoreName, cmHandleId,
-                resourceIdentifier, optionsParamInQuery, NO_TOPIC, NO_REQUEST_ID, includeDescendants, authorization);
+        final Supplier<Object> taskSupplier = getTaskSupplierForGetRequest(cmResourceAddress, optionsParamInQuery,
+            NO_TOPIC, NO_REQUEST_ID, includeDescendants, authorization);
         return executeTaskSync(taskSupplier);
     }
 
@@ -96,23 +93,18 @@ public abstract class NcmpDatastoreRequestHandler {
         return ResponseEntity.ok(taskSupplier.get());
     }
 
-    private ResponseEntity<Object> executeAsyncTaskAndGetResponseEntity(final String datastoreName,
-                                                                        final String cmHandleId,
-                                                                        final String resourceIdentifier,
+    private ResponseEntity<Object> executeAsyncTaskAndGetResponseEntity(final CmResourceAddress cmResourceAddress,
                                                                         final String optionsParamInQuery,
                                                                         final String topicParamInQuery,
                                                                         final boolean includeDescendants,
                                                                         final String authorization) {
         final String requestId = UUID.randomUUID().toString();
-        final Supplier<Object> taskSupplier = getTaskSupplierForGetRequest(datastoreName, cmHandleId,
-                resourceIdentifier, optionsParamInQuery, topicParamInQuery, requestId, includeDescendants,
-                authorization);
+        final Supplier<Object> taskSupplier = getTaskSupplierForGetRequest(cmResourceAddress,
+            optionsParamInQuery, topicParamInQuery, requestId, includeDescendants, authorization);
         return executeTaskAsync(topicParamInQuery, requestId, taskSupplier);
     }
 
-    protected abstract Supplier<Object> getTaskSupplierForGetRequest(final String datastoreName,
-                                                  final String cmHandleId,
-                                                  final String resourceIdentifier,
+    protected abstract Supplier<Object> getTaskSupplierForGetRequest(final CmResourceAddress cmResourceAddress,
                                                   final String optionsParamInQuery,
                                                   final String topicParamInQuery,
                                                   final String requestId,
index 430b749..75112ca 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2022-2023 Nordix Foundation
+ *  Copyright (C) 2022-2024 Nordix Foundation
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -30,6 +30,7 @@ import org.onap.cps.ncmp.api.NetworkCmProxyDataService;
 import org.onap.cps.ncmp.api.impl.exception.InvalidDatastoreException;
 import org.onap.cps.ncmp.api.impl.operations.DatastoreType;
 import org.onap.cps.ncmp.api.impl.operations.OperationType;
+import org.onap.cps.ncmp.api.models.CmResourceAddress;
 import org.onap.cps.ncmp.api.models.DataOperationRequest;
 import org.onap.cps.ncmp.rest.exceptions.OperationNotSupportedException;
 import org.onap.cps.ncmp.rest.executor.CpsNcmpTaskExecutor;
@@ -77,18 +78,15 @@ public class NcmpPassthroughResourceRequestHandler extends NcmpDatastoreRequestH
     }
 
     @Override
-    protected Supplier<Object> getTaskSupplierForGetRequest(final String datastoreName,
-                                                            final String cmHandleId,
-                                                            final String resourceIdentifier,
+    protected Supplier<Object> getTaskSupplierForGetRequest(final CmResourceAddress cmResourceAddress,
                                                             final String optionsParamInQuery,
                                                             final String topicParamInQuery,
                                                             final String requestId,
                                                             final boolean includeDescendants,
                                                             final String authorization) {
 
-        return () -> networkCmProxyDataService.getResourceDataForCmHandle(
-            datastoreName, cmHandleId, resourceIdentifier, optionsParamInQuery, topicParamInQuery, requestId,
-            authorization);
+        return () -> networkCmProxyDataService.getResourceDataForCmHandle(cmResourceAddress, optionsParamInQuery,
+            topicParamInQuery, requestId, authorization);
     }
 
     private ResponseEntity<Object> getRequestIdAndSendDataOperationRequestToDmiService(
index 616492d..a5b1f05 100644 (file)
@@ -39,6 +39,7 @@ import org.onap.cps.ncmp.api.impl.inventory.DataStoreSyncState
 import org.onap.cps.ncmp.api.impl.inventory.LockReasonCategory
 import org.onap.cps.ncmp.api.impl.trustlevel.TrustLevel
 import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle
+import org.onap.cps.ncmp.api.models.CmResourceAddress
 import org.onap.cps.ncmp.rest.controller.handlers.NcmpCachedResourceRequestHandler
 import org.onap.cps.ncmp.rest.controller.handlers.NcmpPassthroughResourceRequestHandler
 import org.onap.cps.ncmp.rest.executor.CpsNcmpTaskExecutor
@@ -136,15 +137,15 @@ class NetworkCmProxyControllerSpec extends Specification {
     def NO_TOPIC = null
     def NO_REQUEST_ID = null
     def NO_AUTH_HEADER = null
-    def TIMOUT_FOR_TEST = 1234
+    def TIMEOUT_FOR_TEST = 1234
 
     def logger = Spy(ListAppender<ILoggingEvent>)
 
     def setup() {
         ncmpCachedResourceRequestHandler.notificationFeatureEnabled = true
-        ncmpCachedResourceRequestHandler.timeOutInMilliSeconds = TIMOUT_FOR_TEST
+        ncmpCachedResourceRequestHandler.timeOutInMilliSeconds = TIMEOUT_FOR_TEST
         ncmpPassthroughResourceRequestHandler.notificationFeatureEnabled = true
-        ncmpPassthroughResourceRequestHandler.timeOutInMilliSeconds = TIMOUT_FOR_TEST
+        ncmpPassthroughResourceRequestHandler.timeOutInMilliSeconds = TIMEOUT_FOR_TEST
         setupLogger()
     }
 
@@ -154,31 +155,28 @@ class NetworkCmProxyControllerSpec extends Specification {
 
     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)"
+            def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-operational?resourceIdentifier=parent/child&options=(a=1,b=2)"
+        and: 'the expected cm resource address'
+            def expectedCmResourceAddress = new CmResourceAddress(PASSTHROUGH_OPERATIONAL.datastoreName, 'testCmHandle', 'parent/child')
         when: 'get data resource request is performed'
-            def response = mvc.perform(
-                    get(getUrl)
-                            .contentType(MediaType.APPLICATION_JSON)
-            ).andReturn().response
-        then: 'the NCMP data service is called with getResourceDataOperationalForCmHandle'
-            1 * mockNetworkCmProxyDataService.getResourceDataForCmHandle(PASSTHROUGH_OPERATIONAL.datastoreName, 'testCmHandle',
-                    'parent/child','(a=1,b=2)', NO_TOPIC, NO_REQUEST_ID, NO_AUTH_HEADER)
+            def response = mvc.perform(get(getUrl).contentType(MediaType.APPLICATION_JSON)).andReturn().response
+        then: 'the NCMP data service is called with correct parameters'
+            1 * mockNetworkCmProxyDataService.getResourceDataForCmHandle(expectedCmResourceAddress, '(a=1,b=2)', NO_TOPIC, NO_REQUEST_ID, NO_AUTH_HEADER)
         and: 'response status is Ok'
-            response.status == HttpStatus.OK.value()
+            assert response.status == HttpStatus.OK.value()
     }
 
     def 'Get Resource Data from ncmp-datastore:operational (cached) parameters handling with #scenario.'() {
         given: 'resource data url'
-            def getUrl = "$ncmpBasePathV1/ch/h123/data/ds/ncmp-datastore:operational" +
-                    "?resourceIdentifier=parent/child${additionalUrlParam}"
+            def getUrl = "$ncmpBasePathV1/ch/h123/data/ds/ncmp-datastore:operational?resourceIdentifier=parent/child${additionalUrlParam}"
+        and: 'the expected cm resource address'
+            def expectedCmResourceAddress = new CmResourceAddress('ncmp-datastore:operational', 'h123', 'parent/child')
         when: 'get data resource request is performed'
-            def response = mvc.perform(
-                    get(getUrl).contentType(MediaType.APPLICATION_JSON)).andReturn().response
-        then: 'task executor is called appropriate number of times'
-            1 * mockNetworkCmProxyDataService.getResourceDataForCmHandle('ncmp-datastore:operational', 'h123', 'parent/child', expectedIncludeDescendants)
+            def response = mvc.perform(get(getUrl).contentType(MediaType.APPLICATION_JSON)).andReturn().response
+        then: 'the NCMP data service is called with correct parameters'
+            1 * mockNetworkCmProxyDataService.getResourceDataForCmHandle(expectedCmResourceAddress, expectedIncludeDescendants)
         and: 'response status is OK'
-            response.status == HttpStatus.OK.value()
+            assert response.status == HttpStatus.OK.value()
         where: 'the following parameters are used'
             scenario                    | additionalUrlParam           || expectedIncludeDescendants
             'no additional param'       | ''                           || OMIT_DESCENDANTS
@@ -192,15 +190,11 @@ class NetworkCmProxyControllerSpec extends Specification {
     def 'Execute (async) data operation to read data from dmi service.'() {
         given: 'data operation url'
             def getUrl = "$ncmpBasePathV1/data?topic=my-topic-name"
-            def dataOperationRequestJsonData = jsonObjectMapper.asJsonString(getDataOperationRequest("read", datastore.datastoreName))
+            def dataOperationRequestJsonData = jsonObjectMapper.asJsonString(getDataOperationRequest('read', datastore.datastoreName))
         when: 'post data operation request is performed'
-            def response = mvc.perform(
-                    post(getUrl)
-                            .contentType(MediaType.APPLICATION_JSON)
-                            .content(dataOperationRequestJsonData)
-            ).andReturn().response
+            def response = mvc.perform(post(getUrl).contentType(MediaType.APPLICATION_JSON).content(dataOperationRequestJsonData)).andReturn().response
         then: 'response status is Ok'
-            response.status == HttpStatus.OK.value()
+            assert response.status == HttpStatus.OK.value()
         and: 'async request id is generated'
             assert response.contentAsString.contains('requestId')
         then: 'the request is handled asynchronously'
@@ -212,80 +206,57 @@ class NetworkCmProxyControllerSpec extends Specification {
     def 'Execute (async) data operation with some validation error.'() {
         given: 'data operation url'
             def getUrl = "$ncmpBasePathV1/data?topic=my-topic-name"
-            def dataOperationRequestJsonData = jsonObjectMapper.asJsonString(
-                    getDataOperationRequest('read', 'invalid datastore'))
+            def dataOperationRequestJsonData = jsonObjectMapper.asJsonString(getDataOperationRequest('read', 'invalid datastore'))
         when: 'post data resource request is performed'
-            def response = mvc.perform(
-                    post(getUrl)
-                            .contentType(MediaType.APPLICATION_JSON)
-                            .content(dataOperationRequestJsonData)
-            ).andReturn().response
+            def response = mvc.perform(post(getUrl).contentType(MediaType.APPLICATION_JSON).content(dataOperationRequestJsonData)).andReturn().response
         then: 'response status is BAD_REQUEST'
-            response.status == HttpStatus.BAD_REQUEST.value()
+            assert response.status == HttpStatus.BAD_REQUEST.value()
     }
 
     def 'Get data operation resource data when notification feature is disabled for datastore: #datastore.'() {
         given: 'data operation url'
             def getUrl = "$ncmpBasePathV1/data?topic=my-topic-name"
-            def dataOperationRequestJsonData = jsonObjectMapper.asJsonString(
-                    getDataOperationRequest("read", PASSTHROUGH_RUNNING.datastoreName))
+            def dataOperationRequestJsonData = jsonObjectMapper.asJsonString(getDataOperationRequest("read", PASSTHROUGH_RUNNING.datastoreName))
             ncmpPassthroughResourceRequestHandler.notificationFeatureEnabled = false
         when: 'post data resource request is performed'
-            def response = mvc.perform(
-                    post(getUrl)
-                            .contentType(MediaType.APPLICATION_JSON)
-                            .content(dataOperationRequestJsonData)
+            def response = mvc.perform(post(getUrl).contentType(MediaType.APPLICATION_JSON).content(dataOperationRequestJsonData)
             ).andReturn().response
         then: 'response status is Ok'
-            response.status == HttpStatus.OK.value()
+            assert response.status == HttpStatus.OK.value()
         and: 'async request id is unavailable'
             assert response.contentAsString == '{"status":"Asynchronous request is unavailable as notification feature is currently disabled."}'
     }
 
     def 'Query Resource Data from operational.'() {
         given: 'the query resource data url'
-            def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:operational/query" +
-                    "?cps-path=/cps/path"
+            def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:operational/query?cps-path=/cps/path"
         when: 'the query data resource request is performed'
-            def response = mvc.perform(
-                    get(getUrl)
-                            .contentType(MediaType.APPLICATION_JSON)
-            ).andReturn().response
+            def response = mvc.perform(get(getUrl).contentType(MediaType.APPLICATION_JSON)).andReturn().response
         then: 'the NCMP query service is called with queryResourceDataOperationalForCmHandle'
-            1 * mockNetworkCmProxyQueryService.queryResourceDataOperational('testCmHandle',
-                    '/cps/path',
-                    FetchDescendantsOption.OMIT_DESCENDANTS)
+            1 * mockNetworkCmProxyQueryService.queryResourceDataOperational('testCmHandle','/cps/path',FetchDescendantsOption.OMIT_DESCENDANTS)
         and: 'response status is Ok'
-            response.status == HttpStatus.OK.value()
+            assert response.status == HttpStatus.OK.value()
     }
 
     def 'Query Resource Data with unsupported datastore'() {
         given: 'the query resource data url'
-            def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running/query" +
-                    "?cps-path=/cps/path"
+            def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running/query?cps-path=/cps/path"
         when: 'the query data resource request is performed'
-            def response = mvc.perform(
-                    get(getUrl)
-                            .contentType(MediaType.APPLICATION_JSON)
-            ).andReturn().response
+            def response = mvc.perform(get(getUrl).contentType(MediaType.APPLICATION_JSON)).andReturn().response
         then: 'a 400 BAD_REQUEST is returned for the unsupported datastore'
-            response.status == 400
+            assert response.status == 400
         and: 'the error message is that the datastore is not supported'
-            response.contentAsString.contains("ncmp-datastore:passthrough-running is not supported")
+            assert response.contentAsString.contains("ncmp-datastore:passthrough-running is not supported")
     }
 
     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)"
+            def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running?resourceIdentifier=$resourceIdentifier&options=(a=1,b=2)"
         and: 'ncmp service returns json object'
-            mockNetworkCmProxyDataService.getResourceDataForCmHandle(PASSTHROUGH_RUNNING.datastoreName, 'testCmHandle',
-                    resourceIdentifier,'(a=1,b=2)', NO_TOPIC, NO_REQUEST_ID, NO_AUTH_HEADER) >> '{valid-json}'
+            def expectedCmResourceAddress = new CmResourceAddress(PASSTHROUGH_RUNNING.datastoreName, 'testCmHandle', resourceIdentifier)
+            mockNetworkCmProxyDataService.getResourceDataForCmHandle(expectedCmResourceAddress,'(a=1,b=2)', NO_TOPIC, NO_REQUEST_ID, NO_AUTH_HEADER) >> '{valid-json}'
         when: 'get data resource request is performed'
-            def response = mvc.perform(
-                    get(getUrl)
-                            .contentType(MediaType.APPLICATION_JSON)
-            ).andReturn().response
+            def response = mvc.perform(get(getUrl).contentType(MediaType.APPLICATION_JSON)).andReturn().response
         then: 'response status is Ok'
             response.status == HttpStatus.OK.value()
         and: 'response contains valid object body'
@@ -302,34 +273,24 @@ class NetworkCmProxyControllerSpec extends Specification {
 
     def 'Update resource data from pass-through running.'() {
         given: 'update resource data url'
-            def updateUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running" +
-                    "?resourceIdentifier=parent/child"
+            def updateUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running?resourceIdentifier=parent/child"
         when: 'update data resource request is performed'
-            def response = mvc.perform(
-                    put(updateUrl)
-                            .contentType(MediaType.APPLICATION_JSON_VALUE).content(requestBody)
-            ).andReturn().response
+            def response = mvc.perform(put(updateUrl).contentType(MediaType.APPLICATION_JSON_VALUE).content(requestBody)).andReturn().response
         then: 'ncmp service method to update resource is called'
-            1 * mockNetworkCmProxyDataService.writeResourceDataPassThroughRunningForCmHandle('testCmHandle',
-                    'parent/child', UPDATE, requestBody, 'application/json;charset=UTF-8', NO_AUTH_HEADER)
+            1 * mockNetworkCmProxyDataService.writeResourceDataPassThroughRunningForCmHandle('testCmHandle','parent/child', UPDATE, requestBody, 'application/json;charset=UTF-8', NO_AUTH_HEADER)
         and: 'the response status is OK'
-            response.status == HttpStatus.OK.value()
+            assert response.status == HttpStatus.OK.value()
     }
 
     def 'Create Resource Data from pass-through running with #scenario.'() {
         given: 'resource data url'
-            def url = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running" +
-                    "?resourceIdentifier=parent/child"
+            def url = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running?resourceIdentifier=parent/child"
         when: 'create resource request is performed'
-            def response = mvc.perform(
-                    post(url)
-                            .contentType(MediaType.APPLICATION_JSON_VALUE).content(requestBody)
-            ).andReturn().response
+            def response = mvc.perform(post(url).contentType(MediaType.APPLICATION_JSON_VALUE).content(requestBody)).andReturn().response
         then: 'ncmp service method to create resource called'
-            1 * mockNetworkCmProxyDataService.writeResourceDataPassThroughRunningForCmHandle('testCmHandle',
-                    'parent/child', CREATE, requestBody, 'application/json;charset=UTF-8', NO_AUTH_HEADER)
+            1 * mockNetworkCmProxyDataService.writeResourceDataPassThroughRunningForCmHandle('testCmHandle', 'parent/child', CREATE, requestBody, 'application/json;charset=UTF-8', NO_AUTH_HEADER)
         and: 'resource is created'
-            response.status == HttpStatus.CREATED.value()
+            assert response.status == HttpStatus.CREATED.value()
     }
 
     def 'Get module references for the given dataspace and cm handle.'() {
@@ -338,12 +299,11 @@ class NetworkCmProxyControllerSpec extends Specification {
         when: 'get module resource request is performed'
             def response = mvc.perform(get(getUrl)).andReturn().response
         then: 'ncmp service method to get yang resource module references is called'
-            mockNetworkCmProxyDataService.getYangResourcesModuleReferences('some-cmhandle')
-                    >> [new ModuleReference(moduleName: 'some-name1', revision: '2021-10-03')]
+            mockNetworkCmProxyDataService.getYangResourcesModuleReferences('some-cmhandle') >> [new ModuleReference(moduleName: 'some-name1', revision: '2021-10-03')]
         and: 'response contains an array with the module name and revision'
             response.getContentAsString() == '[{"moduleName":"some-name1","revision":"2021-10-03"}]'
         and: 'response returns an OK http code'
-            response.status == HttpStatus.OK.value()
+            assert response.status == HttpStatus.OK.value()
     }
 
     def 'Retrieve cm handles.'() {
@@ -364,13 +324,11 @@ class NetworkCmProxyControllerSpec extends Specification {
         and: 'map for trust level per cmHandle has value for only one cm handle'
               trustLevelPerCmHandle.put('ch-1', TrustLevel.NONE)
         when: 'the searches api is invoked'
-            def response = mvc.perform(post(searchesEndpoint)
-                    .contentType(MediaType.APPLICATION_JSON)
-                    .content(jsonString)).andReturn().response
+            def response = mvc.perform(post(searchesEndpoint).contentType(MediaType.APPLICATION_JSON).content(jsonString)).andReturn().response
         then: 'response status returns OK'
-            response.status == HttpStatus.OK.value()
+            assert response.status == HttpStatus.OK.value()
         and: 'the expected response content is returned'
-            response.contentAsString == '[{"cmHandle":"ch-1","publicCmHandleProperties":[{"color":"yellow"}],"state":null,"trustLevel":"NONE","moduleSetTag":null,"alternateId":null,"dataProducerIdentifier":null},{"cmHandle":"ch-2","publicCmHandleProperties":[{"color":"green"}],"state":null,"trustLevel":null,"moduleSetTag":"someModuleSetTag","alternateId":"someAlternateId","dataProducerIdentifier":"someDataProducerIdentifier"}]'
+            assert response.contentAsString == '[{"cmHandle":"ch-1","publicCmHandleProperties":[{"color":"yellow"}],"state":null,"trustLevel":"NONE","moduleSetTag":null,"alternateId":null,"dataProducerIdentifier":null},{"cmHandle":"ch-2","publicCmHandleProperties":[{"color":"green"}],"state":null,"trustLevel":null,"moduleSetTag":"someModuleSetTag","alternateId":"someAlternateId","dataProducerIdentifier":"someDataProducerIdentifier"}]'
     }
 
     def 'Get complete Cm Handle details by Cm Handle id.'() {
@@ -396,7 +354,7 @@ class NetworkCmProxyControllerSpec extends Specification {
         and: 'the response contains the cm handle state'
             assertContainsState(response)
         and: 'the content does not contain dmi properties'
-            !response.contentAsString.contains("some DMI property")
+            assert !response.contentAsString.contains("some DMI property")
     }
 
     def 'Get Cm Handle public properties by Cm Handle id.'() {
@@ -405,13 +363,11 @@ class NetworkCmProxyControllerSpec extends Specification {
         and: 'some cm handle public properties'
             def publicProperties = ['public prop': 'some public property']
         and: 'the service method is invoked with the cm handle id returning the cm handle public properties'
-            1 * mockNetworkCmProxyDataService
-                    .getCmHandlePublicProperties('some-cm-handle') >> publicProperties
+            1 * mockNetworkCmProxyDataService.getCmHandlePublicProperties('some-cm-handle') >> publicProperties
         when: 'the cm handle properties api is invoked'
-            def response = mvc.perform(
-                    get(cmHandlePropertiesEndpoint)).andReturn().response
+            def response = mvc.perform(get(cmHandlePropertiesEndpoint)).andReturn().response
         then: 'the correct response is returned'
-            response.status == HttpStatus.OK.value()
+            assert response.status == HttpStatus.OK.value()
         and: 'the response contains the public properties'
             assertContainsPublicProperties(response)
     }
@@ -422,15 +378,13 @@ class NetworkCmProxyControllerSpec extends Specification {
         and: 'some cm handle composite state'
             def compositeState = compositeStateTestObject()
         and: 'the service method is invoked with the cm handle id returning the cm handle composite state'
-            1 * mockNetworkCmProxyDataService
-                    .getCmHandleCompositeState('some-cm-handle') >> compositeState
+            1 * mockNetworkCmProxyDataService.getCmHandleCompositeState('some-cm-handle') >> compositeState
         when: 'the cm handle state api is invoked'
-            def response = mvc.perform(
-                    get(cmHandlePropertiesEndpoint)).andReturn().response
+            def response = mvc.perform(get(cmHandlePropertiesEndpoint)).andReturn().response
         then: 'the correct response is returned'
             response.status == HttpStatus.OK.value()
         and: 'the response contains the cm handle state'
-            assertContainsState(response)
+            assert assertContainsState(response)
     }
 
     def 'Call execute cm handle searches with unrecognized condition name.'() {
@@ -449,12 +403,9 @@ class NetworkCmProxyControllerSpec extends Specification {
             trustLevelPerCmHandle.put('ch-1', TrustLevel.COMPLETE)
             trustLevelPerCmHandle.put('ch-2', TrustLevel.NONE)
         when: 'the searches api is invoked'
-            def response = mvc.perform(
-                    post(searchesEndpoint)
-                            .contentType(MediaType.APPLICATION_JSON)
-                            .content(jsonString)).andReturn().response
+            def response = mvc.perform(post(searchesEndpoint).contentType(MediaType.APPLICATION_JSON).content(jsonString)).andReturn().response
         then: 'an empty cm handle identifier is returned'
-            response.contentAsString == '[{"cmHandle":"ch-1","publicCmHandleProperties":[{"color":"yellow"}],"state":null,"trustLevel":"COMPLETE","moduleSetTag":null,"alternateId":null,"dataProducerIdentifier":null},{"cmHandle":"ch-2","publicCmHandleProperties":[{"color":"green"}],"state":null,"trustLevel":"NONE","moduleSetTag":null,"alternateId":null,"dataProducerIdentifier":null}]'
+            assert response.contentAsString == '[{"cmHandle":"ch-1","publicCmHandleProperties":[{"color":"yellow"}],"state":null,"trustLevel":"COMPLETE","moduleSetTag":null,"alternateId":null,"dataProducerIdentifier":null},{"cmHandle":"ch-2","publicCmHandleProperties":[{"color":"green"}],"state":null,"trustLevel":"NONE","moduleSetTag":null,"alternateId":null,"dataProducerIdentifier":null}]'
     }
 
     def 'Query for cm handles matching query parameters'() {
@@ -463,68 +414,47 @@ class NetworkCmProxyControllerSpec extends Specification {
         and: 'the service method is invoked with module names and returns cm handle ids'
             1 * mockNetworkCmProxyDataService.executeCmHandleIdSearch(_) >> ['ch-1', 'ch-2']
         when: 'the searches api is invoked'
-            def response = mvc.perform(
-                    post(searchesEndpoint)
-                            .contentType(MediaType.APPLICATION_JSON)
-                            .content('{}')).andReturn().response
+            def response = mvc.perform(post(searchesEndpoint).contentType(MediaType.APPLICATION_JSON).content('{}')).andReturn().response
         then: 'cm handle ids are returned'
-            response.contentAsString == '["ch-1","ch-2"]'
+            assert response.contentAsString == '["ch-1","ch-2"]'
     }
 
     def 'Query for cm handles with invalid request payload'() {
         when: 'the searches api is invoked'
             def searchesEndpoint = "$ncmpBasePathV1/ch/id-searches"
             def invalidInputData = '{invalidJson}'
-            def response = mvc.perform(
-                    post(searchesEndpoint)
-                            .contentType(MediaType.APPLICATION_JSON)
-                            .content(invalidInputData)).andReturn().response
+            def response = mvc.perform(post(searchesEndpoint).contentType(MediaType.APPLICATION_JSON).content(invalidInputData)).andReturn().response
         then: 'BAD_REQUEST is returned'
-            response.getStatus() == 400
+            assert 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" +
-                    "?resourceIdentifier=parent/child"
+            def url = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running?resourceIdentifier=parent/child"
         when: 'patch data resource request is performed'
-            def response = mvc.perform(
-                    patch(url)
-                            .contentType(MediaType.APPLICATION_JSON)
-                            .accept(MediaType.APPLICATION_JSON).content(requestBody)
-            ).andReturn().response
+            def response = mvc.perform(patch(url).contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON).content(requestBody)).andReturn().response
         then: 'ncmp service method to update resource is called'
-            1 * mockNetworkCmProxyDataService.writeResourceDataPassThroughRunningForCmHandle('testCmHandle',
-                    'parent/child', PATCH, requestBody, 'application/json;charset=UTF-8', NO_AUTH_HEADER)
+            1 * mockNetworkCmProxyDataService.writeResourceDataPassThroughRunningForCmHandle('testCmHandle', 'parent/child', PATCH, requestBody, 'application/json;charset=UTF-8', NO_AUTH_HEADER)
         and: 'the response status is OK'
-            response.status == HttpStatus.OK.value()
+            assert response.status == HttpStatus.OK.value()
     }
 
     def 'Delete resource data in pass-through running datastore.'() {
         given: 'delete resource data url'
-            def url = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running" +
-                    "?resourceIdentifier=parent/child"
+            def url = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:passthrough-running?resourceIdentifier=parent/child"
         when: 'delete data resource request is performed'
-            def response = mvc.perform(
-                    delete(url)
-                            .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)).andReturn().response
+            def response = mvc.perform(delete(url).contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)).andReturn().response
         then: 'the ncmp service method to delete resource is called (with null as body)'
-            1 * mockNetworkCmProxyDataService.writeResourceDataPassThroughRunningForCmHandle('testCmHandle',
-                    'parent/child', DELETE, null, 'application/json;charset=UTF-8', NO_AUTH_HEADER)
+            1 * mockNetworkCmProxyDataService.writeResourceDataPassThroughRunningForCmHandle('testCmHandle', 'parent/child', DELETE, null, 'application/json;charset=UTF-8', NO_AUTH_HEADER)
         and: 'the response is No Content'
-            response.status == HttpStatus.NO_CONTENT.value()
+            assert 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"
+            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
+            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'
@@ -535,17 +465,14 @@ class NetworkCmProxyControllerSpec extends Specification {
 
     def 'Getting module definitions for a module'() {
         when: 'get module definition request is performed with module name'
-            def response = mvc.perform(
-                get("$ncmpBasePathV1/ch/some-cmhandle/modules/definitions?module-name=sampleModuleName"))
-                .andReturn().response
+            def response = mvc.perform(get("$ncmpBasePathV1/ch/some-cmhandle/modules/definitions?module-name=sampleModuleName")).andReturn().response
         then: 'ncmp service method is invoked with correct parameters'
             mockNetworkCmProxyDataService.getModuleDefinitionsByCmHandleAndModule('some-cmhandle', 'sampleModuleName', _)
-                >> [new ModuleDefinition('sampleModuleName', '2021-10-03',
-                'module sampleModuleName{ sample module content }')]
+                >> [new ModuleDefinition('sampleModuleName', '2021-10-03','module sampleModuleName{ sample module content }')]
         and: 'response contains an array with the module name, revision and content'
             response.getContentAsString() == '[{"moduleName":"sampleModuleName","revision":"2021-10-03","content":"module sampleModuleName{ sample module content }"}]'
         and: 'response returns an OK http code'
-            response.status == HttpStatus.OK.value()
+            assert response.status == HttpStatus.OK.value()
     }
 
     def 'Getting module definitions filtering on #scenario'() {
@@ -590,17 +517,15 @@ class NetworkCmProxyControllerSpec extends Specification {
 
     def 'Get Resource Data from operational with or without descendants'() {
         given: 'resource data url with descendants #enabled'
-            def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:operational" +
-                    "?resourceIdentifier=parent/child&include-descendants=${booleanValue}"
+            def getUrl = "$ncmpBasePathV1/ch/testCmHandle/data/ds/ncmp-datastore:operational?resourceIdentifier=parent/child&include-descendants=${booleanValue}"
+        and: 'the expected target'
+            def expectedCmResourceAddress = new CmResourceAddress(OPERATIONAL.datastoreName, 'testCmHandle', 'parent/child')
         when: 'get data resource request is performed'
-            def response = mvc.perform(
-                    get(getUrl)
-                            .contentType(MediaType.APPLICATION_JSON)
-            ).andReturn().response
+            def response = mvc.perform(get(getUrl).contentType(MediaType.APPLICATION_JSON)).andReturn().response
         then: 'the NCMP data service is called with getResourceDataOperational with #descendantsOption'
-            1 * mockNetworkCmProxyDataService.getResourceDataForCmHandle(OPERATIONAL.datastoreName, 'testCmHandle', 'parent/child', descendantsOption)
+            1 * mockNetworkCmProxyDataService.getResourceDataForCmHandle(expectedCmResourceAddress, descendantsOption)
         and: 'response status is Ok'
-            response.status == HttpStatus.OK.value()
+            assert response.status == HttpStatus.OK.value()
         where: 'the following parameters are used'
             booleanValue | descendantsOption
             false        | OMIT_DESCENDANTS
index ddeac51..bdd0e71 100644 (file)
@@ -25,6 +25,7 @@ import org.onap.cps.ncmp.api.impl.exception.InvalidDatastoreException
 import org.onap.cps.ncmp.api.impl.exception.InvalidOperationException
 import org.onap.cps.ncmp.api.models.DataOperationDefinition
 import org.onap.cps.ncmp.api.models.DataOperationRequest
+import org.onap.cps.ncmp.api.models.CmResourceAddress
 import org.onap.cps.ncmp.rest.exceptions.OperationNotSupportedException
 import org.onap.cps.ncmp.rest.executor.CpsNcmpTaskExecutor
 import spock.lang.Specification
@@ -48,12 +49,13 @@ class NcmpDatastoreRequestHandlerSpec extends Specification {
             objectUnderTest.notificationFeatureEnabled = notificationFeatureEnabled
         and: 'a flag to track the network service call'
             def networkServiceMethodCalled = false
+        and: 'a CM resource address'
+            def cmResourceAddress = new CmResourceAddress('ds', 'ch1', 'resource1')
         and: 'the (mocked) service will use the flag to indicate if it is called'
-            mockNetworkCmProxyDataService.getResourceDataForCmHandle('ds', 'ch1', 'resource1', 'options', _, _, NO_AUTH_HEADER) >> {
-                networkServiceMethodCalled = true
-            }
+            mockNetworkCmProxyDataService.getResourceDataForCmHandle(cmResourceAddress, 'options', _, _, NO_AUTH_HEADER) >>
+                { networkServiceMethodCalled = true }
         when: 'get request is executed with topic = #topic'
-            objectUnderTest.executeRequest('ds', 'ch1', 'resource1', 'options', topic, false, NO_AUTH_HEADER)
+            objectUnderTest.executeRequest(cmResourceAddress, 'options', topic, false, NO_AUTH_HEADER)
         then: 'the task is executed in an async fashion or not'
             expectedCalls * spiedCpsNcmpTaskExecutor.executeTask(*_)
         and: 'the service request is invoked'
index 4230140..20545d7 100644 (file)
@@ -29,6 +29,7 @@ import org.onap.cps.ncmp.api.impl.inventory.CompositeState;
 import org.onap.cps.ncmp.api.impl.operations.OperationType;
 import org.onap.cps.ncmp.api.models.CmHandleQueryApiParameters;
 import org.onap.cps.ncmp.api.models.CmHandleQueryServiceParameters;
+import org.onap.cps.ncmp.api.models.CmResourceAddress;
 import org.onap.cps.ncmp.api.models.DataOperationRequest;
 import org.onap.cps.ncmp.api.models.DmiPluginRegistration;
 import org.onap.cps.ncmp.api.models.DmiPluginRegistrationResponse;
@@ -53,18 +54,14 @@ public interface NetworkCmProxyDataService {
     /**
      * Get resource data for given data store using dmi.
      *
-     * @param datastoreName       datastore name
-     * @param cmHandleId          cm handle identifier
-     * @param resourceIdentifier  resource identifier
+     * @param cmResourceAddress   target datastore, cm handle and resource identifier
      * @param optionsParamInQuery options query
      * @param topicParamInQuery   topic name for (triggering) async responses
      * @param requestId           unique requestId for async request
      * @param authorization       contents of Authorization header, or null if not present
      * @return {@code Object} resource data
      */
-    Object getResourceDataForCmHandle(String datastoreName,
-                                      String cmHandleId,
-                                      String resourceIdentifier,
+    Object getResourceDataForCmHandle(CmResourceAddress cmResourceAddress,
                                       String optionsParamInQuery,
                                       String topicParamInQuery,
                                       String requestId,
@@ -73,15 +70,11 @@ public interface NetworkCmProxyDataService {
     /**
      * Get resource data for operational.
      *
-     * @param datastoreName      datastore name
-     * @param cmHandleId         cm handle identifier
-     * @param resourceIdentifier resource identifier
+     * @param cmResourceAddress     target datastore, cm handle and resource identifier
      * @Link FetchDescendantsOption fetch descendants option
      * @return {@code Object} resource data
      */
-    Object getResourceDataForCmHandle(String datastoreName,
-                                      String cmHandleId,
-                                      String resourceIdentifier,
+    Object getResourceDataForCmHandle(CmResourceAddress cmResourceAddress,
                                       FetchDescendantsOption fetchDescendantsOption);
 
     /**
@@ -110,11 +103,11 @@ public interface NetworkCmProxyDataService {
      * @return {@code Object} return data
      */
     Object writeResourceDataPassThroughRunningForCmHandle(String cmHandleId,
-                                                        String resourceIdentifier,
-                                                        OperationType operationType,
-                                                        String requestBody,
-                                                        String contentType,
-                                                        String authorization);
+                                                          String resourceIdentifier,
+                                                          OperationType operationType,
+                                                          String requestBody,
+                                                          String contentType,
+                                                          String authorization);
 
     /**
      * Retrieve module references for the given cm handle.
index 6ab6eab..c15df9c 100755 (executable)
@@ -72,6 +72,7 @@ import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle;
 import org.onap.cps.ncmp.api.models.CmHandleQueryApiParameters;
 import org.onap.cps.ncmp.api.models.CmHandleQueryServiceParameters;
 import org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse;
+import org.onap.cps.ncmp.api.models.CmResourceAddress;
 import org.onap.cps.ncmp.api.models.DataOperationRequest;
 import org.onap.cps.ncmp.api.models.DmiPluginRegistration;
 import org.onap.cps.ncmp.api.models.DmiPluginRegistrationResponse;
@@ -127,15 +128,12 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService
     }
 
     @Override
-    public Object getResourceDataForCmHandle(final String datastoreName,
-                                             final String cmHandleId,
-                                             final String resourceIdentifier,
+    public Object getResourceDataForCmHandle(final CmResourceAddress cmResourceAddress,
                                              final String optionsParamInQuery,
                                              final String topicParamInQuery,
                                              final String requestId,
                                              final String authorization) {
-        final ResponseEntity<?> responseEntity = dmiDataOperations.getResourceDataFromDmi(datastoreName, cmHandleId,
-            resourceIdentifier,
+        final ResponseEntity<?> responseEntity = dmiDataOperations.getResourceDataFromDmi(cmResourceAddress,
             optionsParamInQuery,
             topicParamInQuery,
             requestId,
@@ -144,12 +142,12 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService
     }
 
     @Override
-    public Object getResourceDataForCmHandle(final String datastoreName,
-                                             final String cmHandleId,
-                                             final String resourceIdentifier,
+    public Object getResourceDataForCmHandle(final CmResourceAddress cmResourceAddress,
                                              final FetchDescendantsOption fetchDescendantsOption) {
-        return cpsDataService.getDataNodes(datastoreName, cmHandleId, resourceIdentifier,
-            fetchDescendantsOption).iterator().next();
+        return cpsDataService.getDataNodes(cmResourceAddress.datastoreName(),
+                                           cmResourceAddress.cmHandleId(),
+                                           cmResourceAddress.resourceIdentifier(),
+                                           fetchDescendantsOption).iterator().next();
     }
 
     @Override
index 38f3db9..6b02adb 100644 (file)
@@ -57,4 +57,15 @@ public interface CmNotificationSubscriptionPersistenceService {
      */
     Collection<String> getOngoingCmNotificationSubscriptionIds(final DatastoreType datastoreType,
             final String cmHandleId, final String xpath);
+
+    /**
+     * Add or update cm notification subscription.
+     *
+     * @param datastoreType valid datastore type
+     * @param cmHandle cmhandle id
+     * @param xpath valid xpath
+     * @param newSubscriptionId subscription Id to be added
+     */
+    void addOrUpdateCmNotificationSubscription(final DatastoreType datastoreType, final String cmHandle,
+                                               final String xpath, final String newSubscriptionId);
 }
index 6e4997a..5eca5e8 100644 (file)
 
 package org.onap.cps.ncmp.api.impl.events.cmsubscription.service;
 
+import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.PASSTHROUGH_RUNNING;
+import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS;
+
+import java.io.Serializable;
+import java.time.OffsetDateTime;
+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 lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.onap.cps.api.CpsDataService;
 import org.onap.cps.api.CpsQueryService;
+import org.onap.cps.cpspath.parser.CpsPathUtil;
 import org.onap.cps.ncmp.api.impl.operations.DatastoreType;
-import org.onap.cps.spi.FetchDescendantsOption;
 import org.onap.cps.spi.model.DataNode;
+import org.onap.cps.utils.ContentType;
+import org.onap.cps.utils.JsonObjectMapper;
 import org.springframework.stereotype.Service;
 
 @Slf4j
@@ -37,14 +48,16 @@ import org.springframework.stereotype.Service;
 public class CmNotificationSubscriptionPersistenceServiceImpl implements CmNotificationSubscriptionPersistenceService {
 
     private static final String SUBSCRIPTION_ANCHOR_NAME = "cm-data-subscriptions";
-    private static final String IS_ONGOING_CM_SUBSCRIPTION_CPS_PATH_QUERY = """
+    private static final String CM_SUBSCRIPTION_CPS_PATH_QUERY = """
             /datastores/datastore[@name='%s']/cm-handles/cm-handle[@id='%s']/filters/filter[@xpath='%s']
             """.trim();
     private static final String SUBSCRIPTION_IDS_CPS_PATH_QUERY = """
             //filter/subscriptionIds[text()='%s']
             """.trim();
 
+    private final JsonObjectMapper jsonObjectMapper;
     private final CpsQueryService cpsQueryService;
+    private final CpsDataService cpsDataService;
 
     @Override
     public boolean isOngoingCmNotificationSubscription(final DatastoreType datastoreType, final String cmHandleId,
@@ -56,7 +69,7 @@ public class CmNotificationSubscriptionPersistenceServiceImpl implements CmNotif
     public boolean isUniqueSubscriptionId(final String subscriptionId) {
         return cpsQueryService.queryDataNodes(NCMP_DATASPACE_NAME, SUBSCRIPTION_ANCHOR_NAME,
                 SUBSCRIPTION_IDS_CPS_PATH_QUERY.formatted(subscriptionId),
-                FetchDescendantsOption.OMIT_DESCENDANTS).isEmpty();
+                OMIT_DESCENDANTS).isEmpty();
     }
 
     @Override
@@ -64,17 +77,65 @@ public class CmNotificationSubscriptionPersistenceServiceImpl implements CmNotif
             final String cmHandleId, final String xpath) {
 
         final String isOngoingCmSubscriptionCpsPathQuery =
-                IS_ONGOING_CM_SUBSCRIPTION_CPS_PATH_QUERY.formatted(datastoreType.getDatastoreName(), cmHandleId,
+                CM_SUBSCRIPTION_CPS_PATH_QUERY.formatted(datastoreType.getDatastoreName(), cmHandleId,
                         escapeQuotesByDoublingThem(xpath));
         final Collection<DataNode> existingNodes =
                 cpsQueryService.queryDataNodes(NCMP_DATASPACE_NAME, CM_SUBSCRIPTIONS_ANCHOR_NAME,
-                        isOngoingCmSubscriptionCpsPathQuery, FetchDescendantsOption.OMIT_DESCENDANTS);
+                        isOngoingCmSubscriptionCpsPathQuery, OMIT_DESCENDANTS);
         if (existingNodes.isEmpty()) {
             return Collections.emptyList();
         }
         return (List<String>) existingNodes.iterator().next().getLeaves().get("subscriptionIds");
     }
 
+    @Override
+    public void addOrUpdateCmNotificationSubscription(final DatastoreType datastoreType, final String cmHandleId,
+                                                      final String xpath, final String newSubscriptionId) {
+        if (isOngoingCmNotificationSubscription(datastoreType, cmHandleId, xpath)) {
+            final DataNode existingFilterNode =
+                    cpsQueryService.queryDataNodes(NCMP_DATASPACE_NAME, SUBSCRIPTION_ANCHOR_NAME,
+                            CM_SUBSCRIPTION_CPS_PATH_QUERY.formatted(datastoreType.getDatastoreName(), cmHandleId,
+                                    escapeQuotesByDoublingThem(xpath)),
+                            OMIT_DESCENDANTS).iterator().next();
+            final Collection<String> existingSubscriptionIds = getOngoingCmNotificationSubscriptionIds(datastoreType,
+                    cmHandleId, xpath);
+            if (!existingSubscriptionIds.contains(newSubscriptionId)) {
+                updateListOfSubscribers(existingSubscriptionIds, newSubscriptionId, existingFilterNode);
+            }
+        } else {
+            addNewSubscriptionViaDatastore(datastoreType, cmHandleId, xpath, newSubscriptionId);
+        }
+    }
+
+    private void addNewSubscriptionViaDatastore(final DatastoreType datastoreType, final String cmHandleId,
+                                                final String xpath, final String newSubscriptionId) {
+        final String parentXpathFormat = "/datastores/datastore[@name='%s']/cm-handles";
+        String parentXpath = "";
+        if (datastoreType == PASSTHROUGH_RUNNING) {
+            parentXpath = parentXpathFormat.formatted("ncmp-datastore:passthrough-running");
+        } else {
+            parentXpath = parentXpathFormat.formatted("ncmp-datastore:passthrough-operational");
+        }
+
+        final String updatedJson = String.format("{\"cm-handle\":[{\"id\":\"%s\",\"filters\":{\"filter\":"
+                + "[{\"xpath\":\"%s\",\"subscriptionIds\":[\"%s\"]}]}}]}", cmHandleId, xpath, newSubscriptionId);
+        cpsDataService.saveData(NCMP_DATASPACE_NAME, SUBSCRIPTION_ANCHOR_NAME, parentXpath, updatedJson,
+                OffsetDateTime.now(), ContentType.JSON);
+    }
+
+    private void updateListOfSubscribers(final Collection<String> existingSubscriptionIds,
+                                         final String newSubscriptionId, final DataNode existingFilterNode) {
+        final String parentXpath = CpsPathUtil.getNormalizedParentXpath(existingFilterNode.getXpath());
+        final List<String> updatedSubscribers = new ArrayList<>(existingSubscriptionIds);
+        updatedSubscribers.add(newSubscriptionId);
+        final Map<String, Serializable> updatedLeaves = new HashMap<>();
+        updatedLeaves.put("xpath", existingFilterNode.getLeaves().get("xpath"));
+        updatedLeaves.put("subscriptionIds", (Serializable) updatedSubscribers);
+        final String updatedJson = "{\"filter\":[" + jsonObjectMapper.asJsonString(updatedLeaves) + "]}";
+        cpsDataService.updateNodeLeaves(NCMP_DATASPACE_NAME, SUBSCRIPTION_ANCHOR_NAME, parentXpath, updatedJson,
+                OffsetDateTime.now());
+    }
+
     private static String escapeQuotesByDoublingThem(final String inputXpath) {
         return inputXpath.replace("'", "''");
     }
index 2a4bcec..a9ec124 100644 (file)
@@ -42,6 +42,7 @@ import org.onap.cps.ncmp.api.impl.inventory.InventoryPersistence;
 import org.onap.cps.ncmp.api.impl.utils.DmiServiceUrlBuilder;
 import org.onap.cps.ncmp.api.impl.utils.data.operation.ResourceDataOperationRequestUtils;
 import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle;
+import org.onap.cps.ncmp.api.models.CmResourceAddress;
 import org.onap.cps.ncmp.api.models.DataOperationRequest;
 import org.onap.cps.spi.exceptions.CpsException;
 import org.onap.cps.utils.JsonObjectMapper;
@@ -70,9 +71,7 @@ public class DmiDataOperations extends DmiOperations {
      * This method fetches the resource data from operational data store for given cm handle
      * identifier on given resource using dmi client.
      *
-     * @param dataStoreName       name of data store
-     * @param cmHandleId          network resource identifier
-     * @param resourceId          resource identifier
+     * @param cmResourceAddress   target datastore, cm handle and resource identifier
      * @param optionsParamInQuery options query
      * @param topicParamInQuery   topic name for (triggering) async responses
      * @param requestId           requestId for async responses
@@ -82,19 +81,17 @@ public class DmiDataOperations extends DmiOperations {
     @Timed(value = "cps.ncmp.dmi.get",
             description = "Time taken to fetch the resource data from operational data store for given cm handle "
                     + "identifier on given resource using dmi client")
-    public ResponseEntity<Object> getResourceDataFromDmi(final String dataStoreName,
-                                                         final String cmHandleId,
-                                                         final String resourceId,
+    public ResponseEntity<Object> getResourceDataFromDmi(final CmResourceAddress cmResourceAddress,
                                                          final String optionsParamInQuery,
                                                          final String topicParamInQuery,
                                                          final String requestId,
                                                          final String authorization) {
-        final YangModelCmHandle yangModelCmHandle = getYangModelCmHandle(cmHandleId);
+        final YangModelCmHandle yangModelCmHandle = getYangModelCmHandle(cmResourceAddress.cmHandleId());
         final CmHandleState cmHandleState = yangModelCmHandle.getCompositeState().getCmHandleState();
         validateIfCmHandleStateReady(yangModelCmHandle, cmHandleState);
-        final String jsonRequestBody = getDmiRequestBody(READ, requestId, null, null,
-                yangModelCmHandle);
-        final String dmiResourceDataUrl = getDmiRequestUrl(dataStoreName, cmHandleId, resourceId, optionsParamInQuery,
+        final String jsonRequestBody = getDmiRequestBody(READ, requestId, null, null, yangModelCmHandle);
+        final String dmiResourceDataUrl = getDmiRequestUrl(cmResourceAddress.datastoreName(),
+            cmResourceAddress.cmHandleId(), cmResourceAddress.resourceIdentifier(), optionsParamInQuery,
                 topicParamInQuery, yangModelCmHandle.resolveDmiServiceName(RequiredDmiService.DATA));
         return dmiRestClient.postOperationWithJsonData(dmiResourceDataUrl, jsonRequestBody, READ, authorization);
     }
index f14439f..60f39fc 100644 (file)
@@ -115,23 +115,7 @@ public class AlternateIdChecker {
         for (final NcmpServiceCmHandle ncmpServiceCmHandle : newNcmpServiceCmHandles) {
             final String cmHandleId = ncmpServiceCmHandle.getCmHandleId();
             final String proposedAlternateId = ncmpServiceCmHandle.getAlternateId();
-            final boolean isAcceptable;
-            if (StringUtils.isEmpty(proposedAlternateId)) {
-                isAcceptable = true;
-            } else {
-                if (acceptedAlternateIds.contains(proposedAlternateId)) {
-                    isAcceptable = false;
-                    log.warn("Alternate id update ignored, cannot update cm handle {}, alternate id is already "
-                        + "assigned to a different cm handle (in this batch)", cmHandleId);
-                } else {
-                    if (Operation.CREATE.equals(operation)) {
-                        isAcceptable = canApplyAlternateId(cmHandleId, NO_CURRENT_ALTERNATE_ID, proposedAlternateId);
-                    } else {
-                        isAcceptable = canApplyAlternateId(cmHandleId, proposedAlternateId);
-                    }
-                }
-            }
-            if (isAcceptable) {
+            if (isProposedAlternateIdAcceptable(proposedAlternateId, operation, acceptedAlternateIds, cmHandleId)) {
                 acceptedAlternateIds.add(proposedAlternateId);
             } else {
                 rejectedCmHandleIds.add(cmHandleId);
@@ -140,6 +124,22 @@ public class AlternateIdChecker {
         return rejectedCmHandleIds;
     }
 
+    private boolean isProposedAlternateIdAcceptable(final String proposedAlternateId, final Operation operation,
+                                                    final Set<String> acceptedAlternateIds, final String cmHandleId) {
+        if (StringUtils.isEmpty(proposedAlternateId)) {
+            return true;
+        }
+        if (acceptedAlternateIds.contains(proposedAlternateId)) {
+            log.warn("Alternate id update ignored, cannot update cm handle {}, alternate id is already "
+                + "assigned to a different cm handle (in this batch)", cmHandleId);
+            return false;
+        }
+        if (Operation.CREATE.equals(operation)) {
+            return canApplyAlternateId(cmHandleId, NO_CURRENT_ALTERNATE_ID, proposedAlternateId);
+        }
+        return canApplyAlternateId(cmHandleId, proposedAlternateId);
+    }
+
     private boolean alternateIdAlreadyInDb(final String alternateId) {
         try {
             inventoryPersistence.getCmHandleDataNodeByAlternateId(alternateId);
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/CmResourceAddress.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/CmResourceAddress.java
new file mode 100644 (file)
index 0000000..21d82fc
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2024 Nordix Foundation
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.ncmp.api.models;
+
+public record CmResourceAddress(String datastoreName, String cmHandleId, String resourceIdentifier) {
+
+}
index 74016e4..d47be6c 100644 (file)
@@ -24,6 +24,7 @@
 package org.onap.cps.ncmp.api.impl
 
 import org.onap.cps.ncmp.api.models.DmiPluginRegistrationResponse
+import org.onap.cps.ncmp.api.models.CmResourceAddress
 
 import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME
 import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DATASPACE_NAME
@@ -121,35 +122,27 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
                 >> { new ResponseEntity<>(HttpStatus.CREATED) }
     }
 
-    def 'Get resource data for pass-through operational from DMI.'() {
+    def 'Get resource data for from DMI.'() {
         given: 'cpsDataService returns valid data node'
             mockDataNode()
+        and: 'some cm resource address'
+            def cmResourceAddress = new CmResourceAddress('some datastore','some CM Handle', 'some resource Id')
         and: 'get resource data from DMI is called'
-            mockDmiDataOperations.getResourceDataFromDmi(PASSTHROUGH_OPERATIONAL.datastoreName,'testCmHandle', 'testResourceId', OPTIONS_PARAM, NO_TOPIC, NO_REQUEST_ID, NO_AUTH_HEADER) >>
+            mockDmiDataOperations.getResourceDataFromDmi(cmResourceAddress, OPTIONS_PARAM, NO_TOPIC, NO_REQUEST_ID, NO_AUTH_HEADER) >>
                     new ResponseEntity<>('dmi-response', HttpStatus.OK)
-        when: 'get resource data operational for cm-handle is called'
-            def response = objectUnderTest.getResourceDataForCmHandle(PASSTHROUGH_OPERATIONAL.datastoreName, 'testCmHandle', 'testResourceId', OPTIONS_PARAM, NO_TOPIC, NO_REQUEST_ID, NO_AUTH_HEADER)
+        when: 'get resource data operational for the given cm resource address is called'
+            def response = objectUnderTest.getResourceDataForCmHandle(cmResourceAddress, OPTIONS_PARAM, NO_TOPIC, NO_REQUEST_ID, NO_AUTH_HEADER)
         then: 'DMI returns a json response'
             assert response == 'dmi-response'
     }
 
-    def 'Get resource data for pass-through running from DMI.'() {
-        given: 'cpsDataService returns valid data node'
-            mockDataNode()
-        and: 'DMI returns valid response and data'
-            mockDmiDataOperations.getResourceDataFromDmi(PASSTHROUGH_RUNNING.datastoreName, 'testCmHandle', 'testResourceId', OPTIONS_PARAM, NO_TOPIC, NO_REQUEST_ID, NO_AUTH_HEADER) >>
-                    new ResponseEntity<>('{dmi-response}', HttpStatus.OK)
-        when: 'get resource data is called'
-            def response = objectUnderTest.getResourceDataForCmHandle(PASSTHROUGH_RUNNING.datastoreName, 'testCmHandle', 'testResourceId', OPTIONS_PARAM, NO_TOPIC, NO_REQUEST_ID, NO_AUTH_HEADER)
-        then: 'get resource data returns expected response'
-            assert response == '{dmi-response}'
-    }
-
     def 'Get resource data for operational (cached) data.'() {
         given: 'CPS Data service returns some object(s)'
             mockCpsDataService.getDataNodes(OPERATIONAL.datastoreName, 'testCmHandle', 'testResourceId', FetchDescendantsOption.OMIT_DESCENDANTS) >> ['First Object', 'other Object']
+        and: 'a cm resource address for the same datastore, cm handle and resource id'
+            def cmResourceAddress = new CmResourceAddress(OPERATIONAL.datastoreName, 'testCmHandle', 'testResourceId')
         when: 'get resource data is called'
-            def response = objectUnderTest.getResourceDataForCmHandle(OPERATIONAL.datastoreName, 'testCmHandle', 'testResourceId', FetchDescendantsOption.OMIT_DESCENDANTS)
+            def response = objectUnderTest.getResourceDataForCmHandle(cmResourceAddress, FetchDescendantsOption.OMIT_DESCENDANTS)
         then: 'get resource data returns teh first object from the data service'
             assert response == 'First Object'
     }
index eb0e110..19ebc3d 100644 (file)
 
 package org.onap.cps.ncmp.api.impl.events.cmsubscription.service
 
-
+import org.onap.cps.api.CpsDataService
 import org.onap.cps.api.CpsQueryService
 import org.onap.cps.ncmp.api.impl.operations.DatastoreType
 import org.onap.cps.spi.FetchDescendantsOption
 import org.onap.cps.spi.model.DataNode
+import org.onap.cps.utils.JsonObjectMapper
+import com.fasterxml.jackson.databind.ObjectMapper
 import spock.lang.Specification
 
 class CmNotificationSubscriptionPersistenceServiceImplSpec extends Specification {
 
+    def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
     def mockCpsQueryService = Mock(CpsQueryService)
+    def mockCpsDataService = Mock(CpsDataService)
 
-    def objectUnderTest = new CmNotificationSubscriptionPersistenceServiceImpl(mockCpsQueryService)
+    def objectUnderTest = new CmNotificationSubscriptionPersistenceServiceImpl(jsonObjectMapper, mockCpsQueryService, mockCpsDataService)
 
     def 'Check ongoing cm subscription #scenario'() {
         given: 'a valid cm subscription query'
@@ -64,4 +68,43 @@ class CmNotificationSubscriptionPersistenceServiceImplSpec extends Specification
             'datanodes present'    | [new DataNode()] || false
             'no datanodes present' | []               || true
     }
+
+    def 'Add new subscriber to an ongoing cm notification subscription'() {
+        given: 'a valid cm subscription path query'
+            def cpsPathQuery = objectUnderTest.CM_SUBSCRIPTION_CPS_PATH_QUERY.formatted('ncmp-datastore:passthrough-running', 'ch-1', '/x/y');
+        and: 'a dataNode exists for the given cps path query'
+             mockCpsQueryService.queryDataNodes('NCMP-Admin', 'cm-data-subscriptions',
+                cpsPathQuery, FetchDescendantsOption.OMIT_DESCENDANTS) >> [new DataNode(xpath: cpsPathQuery, leaves: ['xpath': '/x/y','subscriptionIds': ['sub-1']])]
+        when: 'the method to add/update cm notification subscription is called'
+            objectUnderTest.addOrUpdateCmNotificationSubscription(DatastoreType.PASSTHROUGH_RUNNING, 'ch-1','/x/y', 'newSubId')
+        then: 'data service method to update list of subscribers is called once'
+            1 * mockCpsDataService.updateNodeLeaves(
+                'NCMP-Admin',
+                'cm-data-subscriptions',
+                '/datastores/datastore[@name=\'ncmp-datastore:passthrough-running\']/cm-handles/cm-handle[@id=\'ch-1\']/filters',
+                '{"filter":[{"xpath":"/x/y","subscriptionIds":["sub-1","newSubId"]}]}', _)
+    }
+
+    def 'Add new cm notification subscription for #datastoreType'() {
+        given: 'a valid cm subscription path query'
+            def cpsPathQuery = objectUnderTest.CM_SUBSCRIPTION_CPS_PATH_QUERY.formatted(datastoreName, 'ch-1', '/x/y')
+        and: 'a parent node xpath for given path above'
+            def parentNodeXpath = '/datastores/datastore[@name=\'%s\']/cm-handles'
+        and: 'a datanode does not exist for the given cps path query'
+            mockCpsQueryService.queryDataNodes('NCMP-Admin', 'cm-data-subscriptions',
+                cpsPathQuery.formatted(datastoreName),
+                FetchDescendantsOption.OMIT_DESCENDANTS) >> []
+        when: 'the method to add/update cm notification subscription is called'
+            objectUnderTest.addOrUpdateCmNotificationSubscription(datastoreType, 'ch-1','/x/y', 'newSubId')
+        then: 'data service method to update list of subscribers is called once with the correct parameters'
+            1 * mockCpsDataService.saveData(
+                'NCMP-Admin',
+                'cm-data-subscriptions',
+                parentNodeXpath.formatted(datastoreName),
+                '{"cm-handle":[{"id":"ch-1","filters":{"filter":[{"xpath":"/x/y","subscriptionIds":["newSubId"]}]}}]}', _,_)
+        where:
+            scenario                  | datastoreType                          || datastoreName
+            'passthrough_running'     | DatastoreType.PASSTHROUGH_RUNNING      || "ncmp-datastore:passthrough-running"
+            'passthrough_operational' | DatastoreType.PASSTHROUGH_OPERATIONAL  || "ncmp-datastore:passthrough-operational"
+    }
 }
\ No newline at end of file
index e154588..eb6c7a0 100644 (file)
@@ -28,6 +28,7 @@ import org.onap.cps.ncmp.api.impl.exception.HttpClientRequestException
 import org.onap.cps.ncmp.api.impl.utils.DmiServiceUrlBuilder
 import org.onap.cps.ncmp.api.impl.utils.context.CpsApplicationContext
 import org.onap.cps.ncmp.api.models.DataOperationRequest
+import org.onap.cps.ncmp.api.models.CmResourceAddress
 import org.onap.cps.ncmp.events.async1_0_0.DataOperationEvent
 import org.onap.cps.ncmp.utils.TestUtils
 import org.onap.cps.utils.JsonObjectMapper
@@ -81,8 +82,8 @@ class DmiDataOperationsSpec extends DmiOperationsBaseSpec {
             mockDmiRestClient.postOperationWithJsonData(expectedUrl, expectedJson, READ, NO_AUTH_HEADER) >> responseFromDmi
             dmiServiceUrlBuilder.getDmiDatastoreUrl(_, _) >> expectedUrl
         when: 'get resource data is invoked'
-            def result = objectUnderTest.getResourceDataFromDmi(dataStore.datastoreName, cmHandleId, resourceIdentifier,
-                    options, NO_TOPIC, NO_REQUEST_ID, NO_AUTH_HEADER)
+            def cmResourceAddress = new CmResourceAddress(dataStore.datastoreName, cmHandleId, resourceIdentifier)
+            def result = objectUnderTest.getResourceDataFromDmi(cmResourceAddress, options, NO_TOPIC, NO_REQUEST_ID, NO_AUTH_HEADER)
         then: 'the result is the response from the DMI service'
             assert result == responseFromDmi
         where: 'the following parameters are used'
index 0eabaa1..1e84367 100644 (file)
@@ -80,7 +80,8 @@ class AlternateIdCheckerSpec extends Specification {
             assert result == expectedRejectedCmHandleIds
         where: 'the following alternate ids are used'
             scenario                          | alt1   | alt2   | altAlreadyInDb  || expectedRejectedCmHandleIds
-            'no alternate ids'                | ''     | ''     | ['dont matter'] || []
+            'blank alternate ids'             | ''     | ''     | ['dont matter'] || []
+            'null alternate ids'              | null   | null   | ['dont matter'] || []
             'new alternate ids'               | 'fdn1' | 'fdn2' | ['other fdn']   || []
             'one already used alternate id'   | 'fdn1' | 'fdn2' | ['fdn1']        || ['ch-1']
             'duplicate alternate id in batch' | 'fdn1' | 'fdn1' | ['dont matter'] || ['ch-2']
index 699bf3c..b6e12c0 100644 (file)
                         <dependency>
                             <groupId>com.github.spotbugs</groupId>
                             <artifactId>spotbugs</artifactId>
-                            <version>4.2.0</version>
+                            <version>4.2.3</version>
                         </dependency>
                         <dependency>
                             <groupId>${project.groupId}</groupId>
index de427af..a604b06 100644 (file)
@@ -31,6 +31,8 @@ services:
       POSTGRES_DB: cpsdb
       POSTGRES_USER: ${DB_USERNAME:-cps}
       POSTGRES_PASSWORD: ${DB_PASSWORD:-cps}
+    volumes:
+      - ./postgres-init.sql:/docker-entrypoint-initdb.d/postgres-init.sql
     deploy:
       resources:
         reservations:
diff --git a/docker-compose/postgres-init.sql b/docker-compose/postgres-init.sql
new file mode 100644 (file)
index 0000000..0c96de5
--- /dev/null
@@ -0,0 +1 @@
+ALTER SYSTEM SET shared_buffers = '512MB';
index de276ce..ba8fcd9 100644 (file)
@@ -12,6 +12,13 @@ CPS Deployment
 .. contents::
     :depth: 2
 
+Database configuration
+======================
+CPS uses PostgreSQL database. As per the `PostgreSQL documentation on resource consumption
+<https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-SHARED-BUFFERS>`_, the *shared_buffers*
+parameter should be set between 25% and 40% of total memory. It has a default value of 128 megabytes, so this should be
+set appropriately. For example, given a database with 2GB of memory, 512MB is a recommended value.
+
 CPS OOM Charts
 ==============
 The CPS kubernetes chart is located in the `OOM repository <https://github.com/onap/oom/tree/master/kubernetes/cps>`_.
index b379e9f..ca2b26d 100644 (file)
             <artifactId>spock-core</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-web</artifactId>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>org.spockframework</groupId>
             <artifactId>spock-spring</artifactId>
             <artifactId>spring-boot-starter-test</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.springframework.kafka</groupId>
+            <artifactId>spring-kafka-test</artifactId>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>org.testcontainers</groupId>
             <artifactId>postgresql</artifactId>
@@ -88,8 +98,8 @@
             <scope>test</scope>
         </dependency>
         <dependency>
-            <groupId>org.springframework</groupId>
-            <artifactId>spring-web</artifactId>
+            <groupId>org.testcontainers</groupId>
+            <artifactId>kafka</artifactId>
             <scope>test</scope>
         </dependency>
     </dependencies>
index 33945a6..2603c48 100644 (file)
 package org.onap.cps.integration.base
 
 import java.time.OffsetDateTime
+import java.time.format.DateTimeFormatter
 import org.onap.cps.api.CpsAnchorService
 import org.onap.cps.api.CpsDataService
 import org.onap.cps.api.CpsDataspaceService
 import org.onap.cps.api.CpsModuleService
 import org.onap.cps.api.CpsQueryService
 import org.onap.cps.integration.DatabaseTestContainer
+import org.onap.cps.integration.KafkaTestContainer
 import org.onap.cps.ncmp.api.NetworkCmProxyCmHandleQueryService
 import org.onap.cps.ncmp.api.NetworkCmProxyDataService
 import org.onap.cps.ncmp.api.NetworkCmProxyQueryService
@@ -38,6 +40,7 @@ import org.onap.cps.spi.exceptions.DataspaceNotFoundException
 import org.onap.cps.spi.model.DataNode
 import org.onap.cps.spi.repository.DataspaceRepository
 import org.onap.cps.spi.utils.SessionManager
+import org.onap.cps.utils.JsonObjectMapper
 import org.springframework.beans.factory.annotation.Autowired
 import org.springframework.boot.autoconfigure.EnableAutoConfiguration
 import org.springframework.boot.autoconfigure.domain.EntityScan
@@ -55,13 +58,11 @@ import spock.lang.Shared
 import spock.lang.Specification
 import spock.util.concurrent.PollingConditions
 
-import java.time.format.DateTimeFormatter
-
+import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DATASPACE_NAME
+import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DMI_REGISTRY_ANCHOR
+import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DMI_REGISTRY_PARENT
 import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo
 import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus
-import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DATASPACE_NAME;
-import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DMI_REGISTRY_ANCHOR;
-import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NCMP_DMI_REGISTRY_PARENT;
 
 @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK, classes = [CpsDataspaceService])
 @Testcontainers
@@ -75,8 +76,11 @@ abstract class CpsIntegrationSpecBase extends Specification {
     @Shared
     DatabaseTestContainer databaseTestContainer = DatabaseTestContainer.getInstance()
 
+    @Shared
+    KafkaTestContainer kafkaTestContainer = KafkaTestContainer.getInstance()
+
     @Autowired
-    MockMvc mvc;
+    MockMvc mvc
 
     @Autowired
     CpsDataspaceService cpsDataspaceService
@@ -111,6 +115,9 @@ abstract class CpsIntegrationSpecBase extends Specification {
     @Autowired
     ModuleSyncWatchdog moduleSyncWatchdog
 
+    @Autowired
+    JsonObjectMapper jsonObjectMapper
+
     MockRestServiceServer mockDmiServer = null
 
     static final DMI_URL = 'http://mock-dmi-server'
index 6b6f62e..f03872d 100644 (file)
 
 package org.onap.cps.integration.functional
 
+import java.time.Duration
 import java.time.OffsetDateTime
+import org.apache.kafka.common.TopicPartition
+import org.apache.kafka.common.serialization.StringDeserializer
+import org.onap.cps.integration.KafkaTestContainer
 import org.onap.cps.integration.base.CpsIntegrationSpecBase
 import org.onap.cps.ncmp.api.NetworkCmProxyDataService
 import org.onap.cps.ncmp.api.impl.inventory.CmHandleState
@@ -28,12 +32,15 @@ import org.onap.cps.ncmp.api.impl.inventory.LockReasonCategory
 import org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse
 import org.onap.cps.ncmp.api.models.DmiPluginRegistration
 import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle
+import org.onap.cps.ncmp.events.lcm.v1.LcmEvent
 import spock.util.concurrent.PollingConditions
 
 class NcmpCmHandleCreateSpec extends CpsIntegrationSpecBase {
 
     NetworkCmProxyDataService objectUnderTest
 
+    def kafkaConsumer = KafkaTestContainer.getConsumer('ncmp-group', StringDeserializer.class)
+
     static final MODULE_REFERENCES_RESPONSE_A = readResourceDataFile('mock-dmi-responses/bookStoreAWithModules_M1_M2_Response.json')
     static final MODULE_RESOURCES_RESPONSE_A = readResourceDataFile('mock-dmi-responses/bookStoreAWithModules_M1_M2_ResourcesResponse.json')
     static final MODULE_REFERENCES_RESPONSE_B = readResourceDataFile('mock-dmi-responses/bookStoreBWithModules_M1_M3_Response.json')
@@ -47,6 +54,9 @@ class NcmpCmHandleCreateSpec extends CpsIntegrationSpecBase {
         given: 'DMI will return modules when requested'
             mockDmiResponsesForModuleSync(DMI_URL, 'ch-1', MODULE_REFERENCES_RESPONSE_A, MODULE_RESOURCES_RESPONSE_A)
 
+        and: 'consumer subscribed to topic'
+            kafkaConsumer.subscribe(['ncmp-events'])
+
         when: 'a CM-handle is registered for creation'
             def cmHandleToCreate = new NcmpServiceCmHandle(cmHandleId: 'ch-1')
             def dmiPluginRegistration = new DmiPluginRegistration(dmiPlugin: DMI_URL, createdCmHandles: [cmHandleToCreate])
@@ -66,6 +76,14 @@ class NcmpCmHandleCreateSpec extends CpsIntegrationSpecBase {
                 assert CmHandleState.READY == objectUnderTest.getCmHandleCompositeState('ch-1').cmHandleState
             })
 
+        and: 'the messages is polled'
+            def message = kafkaConsumer.poll(Duration.ofMillis(10000))
+            def records = message.records(new TopicPartition('ncmp-events', 0))
+
+        and: 'the newest lcm event notification is received with READY state'
+            def notificationMessage = jsonObjectMapper.convertJsonString(records.last().value().toString(), LcmEvent)
+            assert notificationMessage.event.newValues.cmHandleState.value() == 'READY'
+
         and: 'the CM-handle has expected modules'
             assert ['M1', 'M2'] == objectUnderTest.getYangResourcesModuleReferences('ch-1').moduleName.sort()
 
diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/functional/NcmpCmNotificationSubscriptionSpec.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/functional/NcmpCmNotificationSubscriptionSpec.groovy
new file mode 100644 (file)
index 0000000..df74a05
--- /dev/null
@@ -0,0 +1,46 @@
+package org.onap.cps.integration.functional
+
+import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.PASSTHROUGH_RUNNING;
+import org.onap.cps.integration.base.CpsIntegrationSpecBase;
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.service.CmNotificationSubscriptionPersistenceService;
+import org.springframework.beans.factory.annotation.Autowired;
+
+class NcmpCmNotificationSubscriptionSpec extends CpsIntegrationSpecBase {
+
+    @Autowired
+    CmNotificationSubscriptionPersistenceService cmNotificationSubscriptionPersistenceService;
+
+    def 'Adding a new cm notification subscription'() {
+        given: 'there is no ongoing cm subscription for the following'
+            def datastoreType = PASSTHROUGH_RUNNING
+            def cmHandleId = 'ch-1'
+            def xpath = '/x/y'
+            assert cmNotificationSubscriptionPersistenceService.
+                getOngoingCmNotificationSubscriptionIds(datastoreType,cmHandleId,xpath).size() == 0
+        when: 'we add a new cm notification subscription'
+            cmNotificationSubscriptionPersistenceService.addOrUpdateCmNotificationSubscription(datastoreType,cmHandleId,xpath,
+                'subId-1')
+        then: 'there is an ongoing cm subscription for that CM handle and xpath'
+            assert cmNotificationSubscriptionPersistenceService.isOngoingCmNotificationSubscription(datastoreType,cmHandleId,xpath)
+        and: 'only one subscription id is related to now ongoing cm subscription'
+            assert cmNotificationSubscriptionPersistenceService.
+                getOngoingCmNotificationSubscriptionIds(datastoreType,cmHandleId,xpath).size() == 1
+    }
+
+    def 'Adding a cm notification subscription to an already existing'() {
+        given: 'an ongoing cm subscription'
+            def datastoreType = PASSTHROUGH_RUNNING
+            def cmHandleId = 'ch-1'
+            def xpath = '/x/y'
+            cmNotificationSubscriptionPersistenceService.addOrUpdateCmNotificationSubscription(datastoreType,cmHandleId,xpath,
+                'subId-1')
+        when: 'a new cm notification subscription is made for the SAME CM handle and xpath'
+            cmNotificationSubscriptionPersistenceService.addOrUpdateCmNotificationSubscription(datastoreType,cmHandleId,xpath,
+                'subId-2')
+        then: 'it is added to the ongoing list of subscription ids'
+            def subscriptionIds = cmNotificationSubscriptionPersistenceService.getOngoingCmNotificationSubscriptionIds(datastoreType,cmHandleId,xpath)
+            assert subscriptionIds.size() == 2
+        and: 'both subscription ids exists for the CM handle and xpath'
+            assert subscriptionIds.contains("subId-1") && subscriptionIds.contains("subId-2")
+    }
+}
diff --git a/integration-test/src/test/java/org/onap/cps/integration/KafkaTestContainer.java b/integration-test/src/test/java/org/onap/cps/integration/KafkaTestContainer.java
new file mode 100644 (file)
index 0000000..d41f752
--- /dev/null
@@ -0,0 +1,86 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2024 Nordix Foundation.
+ *  ================================================================================
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ *  SPDX-License-Identifier: Apache-2.0
+ *  ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.integration;
+
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.kafka.clients.consumer.ConsumerConfig;
+import org.apache.kafka.clients.consumer.KafkaConsumer;
+import org.apache.kafka.common.serialization.StringDeserializer;
+import org.testcontainers.containers.KafkaContainer;
+import org.testcontainers.utility.DockerImageName;
+
+/**
+ * The Apache Kafka test container wrapper.
+ * Allow to use specific image and version with Singleton design pattern.
+ * This ensures only one instance of Kafka container across the integration tests.
+ * Avoid unnecessary resource and time consumption.
+ */
+public class KafkaTestContainer extends KafkaContainer {
+
+    private static final String IMAGE_NAME_AND_VERSION = "registry.nordix.org/onaptest/confluentinc/cp-kafka:6.2.1";
+
+    private static KafkaTestContainer kafkaTestContainer;
+
+    private KafkaTestContainer() {
+        super(DockerImageName.parse(IMAGE_NAME_AND_VERSION).asCompatibleSubstituteFor("confluentinc/cp-kafka"));
+    }
+
+    /**
+     * Provides an instance of Kafka test container wrapper.
+     * This will allow to initialize Kafka messaging support before any integration test run.
+     *
+     * @return KafkaTestContainer the unique Kafka instance
+     */
+    public static KafkaTestContainer getInstance() {
+        if (kafkaTestContainer == null) {
+            kafkaTestContainer = new KafkaTestContainer();
+            Runtime.getRuntime().addShutdownHook(new Thread(kafkaTestContainer::close));
+        }
+        return kafkaTestContainer;
+    }
+
+    public static KafkaConsumer getConsumer(final String consumerGroupId, final Object valueDeserializer) {
+        return new KafkaConsumer<>(consumerProperties(consumerGroupId, valueDeserializer));
+    }
+
+    @Override
+    public void start() {
+        super.start();
+        System.setProperty("spring.kafka.properties.bootstrap.servers", kafkaTestContainer.getBootstrapServers());
+    }
+
+    @Override
+    public void stop() {
+        // Method intentionally left blank
+    }
+
+    private static Map<String, Object> consumerProperties(final String consumerGroupId,
+                                                          final Object valueDeserializer) {
+        final Map<String, Object> configProps = new HashMap<>();
+        configProps.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, kafkaTestContainer.getBootstrapServers());
+        configProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
+        configProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, valueDeserializer);
+        configProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
+        configProps.put(ConsumerConfig.GROUP_ID_CONFIG, consumerGroupId);
+        return configProps;
+    }
+
+}