Merge "[BUG] Correctly parse observedTimestamp" master
authorLee Anjella Macabuhay <lee.anjella.macabuhay@est.tech>
Fri, 3 May 2024 14:55:32 +0000 (14:55 +0000)
committerGerrit Code Review <gerrit@onap.org>
Fri, 3 May 2024 14:55:32 +0000 (14:55 +0000)
111 files changed:
checkstyle/pom.xml
cps-application/pom.xml
cps-application/src/main/resources/application.yml
cps-bom/pom.xml
cps-dependencies/pom.xml
cps-events/pom.xml
cps-events/src/main/resources/schemas/updatenode/cps-data-updated-event-schema-1.0.0.json
cps-ncmp-events/pom.xml
cps-ncmp-rest-stub/cps-ncmp-rest-stub-app/pom.xml
cps-ncmp-rest-stub/cps-ncmp-rest-stub-service/pom.xml
cps-ncmp-rest-stub/pom.xml
cps-ncmp-rest/pom.xml
cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyInventoryController.java
cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/handlers/NcmpPassthroughResourceRequestHandler.java
cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/executor/CpsNcmpTaskExecutor.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/NetworkCmProxyInventoryControllerSpec.groovy
cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/handlers/NcmpDatastoreRequestHandlerSpec.groovy
cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/executor/CpsNcmpTaskExecutorSpec.groovy
cps-ncmp-service/pom.xml
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/config/HttpClientConfiguration.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/NcmpEvent.java [moved from cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/NcmpCloudEventBuilder.java with 83% similarity]
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avc/ncmptoclient/AvcEventPublisher.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/CmNotificationSubscriptionEventsHandler.java [new file with mode: 0644]
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/CmNotificationSubscriptionMappersHandler.java [new file with mode: 0644]
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/CmNotificationSubscriptionNcmpOutEventPublishingTask.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/DmiCmNotificationSubscriptionCacheHandler.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/consumer/CmNotificationSubscriptionDmiOutEventConsumer.java [moved from cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/CmNotificationSubscriptionDmiOutEventConsumer.java with 96% similarity]
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/consumer/CmNotificationSubscriptionNcmpInEventConsumer.java [moved from cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/CmNotificationSubscriptionNcmpInEventConsumer.java with 97% similarity]
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/producer/CmNotificationSubscriptionDmiInEventProducer.java [moved from cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/CmNotificationSubscriptionDmiInEventProducer.java with 98% similarity]
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/producer/CmNotificationSubscriptionNcmpOutEventProducer.java [moved from cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/CmNotificationSubscriptionNcmpOutEventProducer.java with 72% similarity]
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/service/CmNotificationSubscriptionHandlerServiceImpl.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/exception/NoAlternateIdParentFoundException.java [new file with mode: 0644]
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/inventory/InventoryPersistence.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/inventory/InventoryPersistenceImpl.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/DmiServiceUrlBuilder.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/data/operation/DataOperationEventCreator.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/data/operation/ResourceDataOperationRequestUtils.java
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/config/NcmpConfigurationSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/CmNotificationSubscriptionDmiInEventProducerSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/CmNotificationSubscriptionDmiOutEventConsumerSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/CmNotificationSubscriptionEventsHandlerSpec.groovy [new file with mode: 0644]
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/CmNotificationSubscriptionMappersHandlerSpec.groovy [new file with mode: 0644]
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/CmNotificationSubscriptionNcmpInEventConsumerSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/CmNotificationSubscriptionNcmpOutEventProducerSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/DmiCmNotificationSubscriptionCacheHandlerSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/service/CmNotificationSubscriptionHandlerServiceImplSpec.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/inventory/InventoryPersistenceImplSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/data/operation/ResourceDataOperationRequestUtilsSpec.groovy
cps-ncmp-service/src/test/resources/application.yml
cps-ncmp-service/src/test/resources/cmSubscription/cmNotificationSubscriptionNcmpInEvent.json
cps-parent/pom.xml
cps-path-parser/pom.xml
cps-rest/pom.xml
cps-rest/src/test/java/org/onap/cps/utils/DateTimeUtility.java
cps-ri/pom.xml
cps-ri/src/main/java/org/onap/cps/spi/impl/CpsAdminPersistenceServiceImpl.java
cps-ri/src/main/java/org/onap/cps/spi/repository/AnchorRepository.java
cps-ri/src/main/java/org/onap/cps/spi/repository/YangResourceRepository.java
cps-service/pom.xml
cps-service/src/main/java/org/onap/cps/api/impl/CpsAnchorServiceImpl.java
cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java
cps-service/src/main/java/org/onap/cps/events/CpsDataUpdateEventsService.java [new file with mode: 0644]
cps-service/src/main/java/org/onap/cps/events/CpsEvent.java [new file with mode: 0644]
cps-service/src/main/java/org/onap/cps/spi/CpsAdminPersistenceService.java
cps-service/src/main/java/org/onap/cps/utils/DateTimeUtility.java [moved from cps-service/src/test/java/org/onap/cps/utils/DateTimeUtility.java with 79% similarity]
cps-service/src/test/groovy/org/onap/cps/api/impl/CpsAnchorServiceImplSpec.groovy
cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy
cps-service/src/test/groovy/org/onap/cps/api/impl/E2ENetworkSliceSpec.groovy
cps-service/src/test/groovy/org/onap/cps/events/CpsDataUpdateEventsServiceSpec.groovy [new file with mode: 0644]
dmi-plugin-demo-and-csit-stub/dmi-plugin-demo-and-csit-stub-app/pom.xml
dmi-plugin-demo-and-csit-stub/dmi-plugin-demo-and-csit-stub-service/pom.xml
dmi-plugin-demo-and-csit-stub/pom.xml
docs/api/swagger/ncmp/openapi-inventory.yaml
docs/api/swagger/ncmp/openapi.yaml
docs/cps-ncmp-message-status-codes.rst
docs/release-notes.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/base/DmiDispatcher.groovy [new file with mode: 0644]
integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsAnchorServiceIntegrationSpec.groovy
integration-test/src/test/groovy/org/onap/cps/integration/functional/NcmpBearerTokenPassthroughSpec.groovy
integration-test/src/test/groovy/org/onap/cps/integration/functional/NcmpCmHandleCreateSpec.groovy
integration-test/src/test/groovy/org/onap/cps/integration/functional/NcmpCmHandleUpgradeSpec.groovy
integration-test/src/test/groovy/org/onap/cps/integration/functional/NcmpCmNotificationSubscriptionSpec.groovy
integration-test/src/test/groovy/org/onap/cps/integration/functional/NcmpRestApiSpec.groovy
integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/CpsDataServiceLimitsPerfTest.groovy
integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/DeletePerfTest.groovy
integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/GetPerfTest.groovy
integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/ModuleQueryPerfTest.groovy [new file with mode: 0644]
integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/QueryPerfTest.groovy
integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/UpdatePerfTest.groovy
integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/WritePerfTest.groovy
integration-test/src/test/groovy/org/onap/cps/integration/performance/ncmp/CmDataSubscriptionsPerfTest.groovy
integration-test/src/test/groovy/org/onap/cps/integration/performance/ncmp/CmHandleQueryPerfTest.groovy
integration-test/src/test/resources/application.yml
integration-test/src/test/resources/data/mock-dmi-responses/bookStoreAWithModules_M1_M2_ResourcesResponse.json [deleted file]
integration-test/src/test/resources/data/mock-dmi-responses/bookStoreAWithModules_M1_M2_Response.json [deleted file]
integration-test/src/test/resources/data/mock-dmi-responses/bookStoreBWithModules_M1_M3_ResourcesResponse.json [deleted file]
integration-test/src/test/resources/data/mock-dmi-responses/bookStoreBWithModules_M1_M3_Response.json [deleted file]
integration-test/src/test/resources/data/mock-dmi-responses/moduleReferencesTemplate.json [new file with mode: 0644]
integration-test/src/test/resources/data/mock-dmi-responses/moduleResourcesTemplate.json [new file with mode: 0644]
jacoco-report/pom.xml
pom.xml
releases/3.4.8-container.yaml [new file with mode: 0644]
releases/3.4.8.yaml [new file with mode: 0644]
spotbugs/pom.xml
version.properties

index b1c4379..cddf5ce 100644 (file)
@@ -26,7 +26,7 @@
     <modelVersion>4.0.0</modelVersion>
     <groupId>org.onap.cps</groupId>
     <artifactId>checkstyle</artifactId>
-    <version>3.4.8-SNAPSHOT</version>
+    <version>3.4.9-SNAPSHOT</version>
 
     <profiles>
         <profile>
index e6e2f9a..9939e0b 100644 (file)
@@ -28,7 +28,7 @@
     <parent>
         <groupId>org.onap.cps</groupId>
         <artifactId>cps-parent</artifactId>
-        <version>3.4.8-SNAPSHOT</version>
+        <version>3.4.9-SNAPSHOT</version>
         <relativePath>../cps-parent/pom.xml</relativePath>
     </parent>
 
index 3c263e3..68dd31b 100644 (file)
@@ -2,6 +2,7 @@
 #  Copyright (C) 2021 Pantheon.tech
 #  Modifications Copyright (C) 2021-2022 Bell Canada
 #  Modifications Copyright (C) 2021-2024 Nordix Foundation
+#  Modifications Copyright (C) 2024 TechMahindra Ltd
 #  ================================================================================
 #  Licensed under the Apache License, Version 2.0 (the "License");
 #  you may not use this file except in compliance with the License.
@@ -52,7 +53,7 @@ spring:
             minimumIdle: 5
             maximumPoolSize: 80
             idleTimeout: 60000
-            connectionTimeout: 120000
+            connectionTimeout: 30000
             leakDetectionThreshold: 30000
             pool-name: CpsDatabasePool
 
@@ -113,7 +114,9 @@ app:
             topic: ${DMI_CM_EVENTS_TOPIC:dmi-cm-events}
         device-heartbeat:
             topic: ${DMI_DEVICE_HEARTBEAT_TOPIC:dmi-device-heartbeat}
-
+    cps:
+        data-updated:
+            topic: ${CPS_CHANGE_EVENT_TOPIC:cps-data-updated-events}
 
 notification:
     enabled: true
@@ -138,14 +141,12 @@ springdoc:
             - name: cps-ncmp-inventory
               url: /api-docs/cps-ncmp/openapi-inventory.yaml
 
-
-
 security:
     # comma-separated uri patterns which do not require authorization
     permit-uri: /actuator/**,/swagger-ui.html,/swagger-ui/**,/swagger-resources/**,/api-docs/**,/v3/api-docs/**
     auth:
-        username: ${CPS_USERNAME}
-        password: ${CPS_PASSWORD}
+        username: ${CPS_USERNAME:cpsuser}
+        password: ${CPS_PASSWORD:cpsr0cks!}
 
 # Actuator
 management:
@@ -170,13 +171,13 @@ logging:
 ncmp:
     dmi:
         httpclient:
-            connectionTimeoutInSeconds: 180
+            connectionTimeoutInSeconds: 30
             maximumConnectionsPerRoute: 50
             maximumConnectionsTotal: 100
             idleConnectionEvictionThresholdInSeconds: 5
         auth:
-            username: ${DMI_USERNAME}
-            password: ${DMI_PASSWORD}
+            username: ${DMI_USERNAME:cpsuser}
+            password: ${DMI_PASSWORD:cpsr0cks!}
             enabled: ${DMI_AUTH_ENABLED:true}
         api:
             base-path: dmi
index 366d4c7..c2f68b4 100644 (file)
@@ -25,7 +25,7 @@
     <modelVersion>4.0.0</modelVersion>
     <groupId>org.onap.cps</groupId>
     <artifactId>cps-bom</artifactId>
-    <version>3.4.8-SNAPSHOT</version>
+    <version>3.4.9-SNAPSHOT</version>
     <packaging>pom</packaging>
 
     <description>This artifact contains dependencyManagement declarations of all published CPS components.</description>
index b7ec70b..34d45b7 100644 (file)
@@ -27,7 +27,7 @@
     <modelVersion>4.0.0</modelVersion>
     <groupId>org.onap.cps</groupId>
     <artifactId>cps-dependencies</artifactId>
-    <version>3.4.8-SNAPSHOT</version>
+    <version>3.4.9-SNAPSHOT</version>
     <packaging>pom</packaging>
 
     <name>${project.groupId}:${project.artifactId}</name>
                 <artifactId>hazelcast-spring</artifactId>
                 <version>5.3.1</version>
             </dependency>
+            <dependency>
+                <groupId>com.squareup.okhttp3</groupId>
+                <artifactId>mockwebserver</artifactId>
+                <version>4.12.0</version>
+                <scope>test</scope>
+            </dependency>
             <dependency>
                 <groupId>com.tngtech.archunit</groupId>
                 <artifactId>archunit-junit5</artifactId>
index 680d0bb..d74e735 100644 (file)
@@ -24,7 +24,7 @@
     <parent>
         <groupId>org.onap.cps</groupId>
         <artifactId>cps-parent</artifactId>
-        <version>3.4.8-SNAPSHOT</version>
+        <version>3.4.9-SNAPSHOT</version>
         <relativePath>../cps-parent/pom.xml</relativePath>
     </parent>
 
index 18f83cc..a3eaf63 100644 (file)
@@ -12,7 +12,7 @@
           "type": "object",
           "properties": {
             "observedTimestamp": {
-              "description": "The timestamp when the data has been observed. The expected format is 'yyyy-MM-dd'T'HH:mm:ss.SSSZ'. Ex: '2020-12-01T00:00:00.000+0000'  ",
+              "description": "The timestamp when the data has been observed. The expected format is 'yyyy-MM-dd'T'HH:mm:ss.SSSZ'. Ex: '2024-02-12T09:35:46.143+0530'  ",
               "type": "string"
             },
             "dataspaceName": {
index e12ab3b..7b43ea1 100644 (file)
@@ -23,7 +23,7 @@
     <parent>
         <groupId>org.onap.cps</groupId>
         <artifactId>cps-parent</artifactId>
-        <version>3.4.8-SNAPSHOT</version>
+        <version>3.4.9-SNAPSHOT</version>
         <relativePath>../cps-parent/pom.xml</relativePath>
     </parent>
 
index ae755a3..81f80a9 100644 (file)
@@ -22,7 +22,7 @@
     <parent>
         <groupId>org.onap.cps</groupId>
         <artifactId>cps-ncmp-rest-stub</artifactId>
-        <version>3.4.8-SNAPSHOT</version>
+        <version>3.4.9-SNAPSHOT</version>
     </parent>
 
     <artifactId>cps-ncmp-rest-stub-app</artifactId>
index b6c461c..a82f2c0 100644 (file)
@@ -21,7 +21,7 @@
     <parent>
         <groupId>org.onap.cps</groupId>
         <artifactId>cps-ncmp-rest-stub</artifactId>
-        <version>3.4.8-SNAPSHOT</version>
+        <version>3.4.9-SNAPSHOT</version>
     </parent>
     <artifactId>cps-ncmp-rest-stub-service</artifactId>
 
index c2c789e..d4eec0d 100644 (file)
@@ -22,7 +22,7 @@
     <parent>
         <groupId>org.onap.cps</groupId>
         <artifactId>cps-parent</artifactId>
-        <version>3.4.8-SNAPSHOT</version>
+        <version>3.4.9-SNAPSHOT</version>
         <relativePath>../cps-parent/pom.xml</relativePath>
     </parent>
 
index 453da8f..d538b98 100644 (file)
@@ -27,7 +27,7 @@
     <parent>
         <groupId>org.onap.cps</groupId>
         <artifactId>cps-parent</artifactId>
-        <version>3.4.8-SNAPSHOT</version>
+        <version>3.4.9-SNAPSHOT</version>
         <relativePath>../cps-parent/pom.xml</relativePath>
     </parent>
 
index 453abca..5467eef 100755 (executable)
@@ -96,8 +96,9 @@ public class NetworkCmProxyInventoryController implements NetworkCmProxyInventor
     private boolean allRegistrationsSuccessful(
         final DmiPluginRegistrationErrorResponse dmiPluginRegistrationErrorResponse) {
         return dmiPluginRegistrationErrorResponse.getFailedCreatedCmHandles().isEmpty()
-            && dmiPluginRegistrationErrorResponse.getFailedUpdatedCmHandles().isEmpty()
-            && dmiPluginRegistrationErrorResponse.getFailedRemovedCmHandles().isEmpty();
+                && dmiPluginRegistrationErrorResponse.getFailedUpdatedCmHandles().isEmpty()
+                && dmiPluginRegistrationErrorResponse.getFailedRemovedCmHandles().isEmpty()
+                && dmiPluginRegistrationErrorResponse.getFailedUpgradeCmHandles().isEmpty();
     }
 
     private DmiPluginRegistrationErrorResponse getFailureRegistrationResponse(
index eca7ebf..1f87865 100644 (file)
@@ -25,11 +25,13 @@ import static org.onap.cps.ncmp.api.impl.operations.OperationType.READ;
 
 import java.util.Map;
 import java.util.UUID;
+import java.util.function.BiConsumer;
 import java.util.function.Supplier;
 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.impl.utils.data.operation.ResourceDataOperationRequestUtils;
 import org.onap.cps.ncmp.api.models.CmResourceAddress;
 import org.onap.cps.ncmp.api.models.DataOperationRequest;
 import org.onap.cps.ncmp.rest.exceptions.OperationNotSupportedException;
@@ -46,7 +48,7 @@ public class NcmpPassthroughResourceRequestHandler extends NcmpDatastoreRequestH
 
     private static final Object noReturn = null;
 
-    private static final int MAXIMUM_CM_HANDLES_PER_OPERATION = 50;
+    private static final int MAXIMUM_CM_HANDLES_PER_OPERATION = 50000;
 
     private static final String PAYLOAD_TOO_LARGE_TEMPLATE = "Operation '%s' affects too many (%d) cm handles";
 
@@ -99,8 +101,9 @@ public class NcmpPassthroughResourceRequestHandler extends NcmpDatastoreRequestH
             final DataOperationRequest dataOperationRequest,
             final String authorization) {
         final String requestId = UUID.randomUUID().toString();
-        cpsNcmpTaskExecutor.executeTask(
+        cpsNcmpTaskExecutor.executeTaskWithErrorHandling(
             getTaskSupplierForDataOperationRequest(topicParamInQuery, dataOperationRequest, requestId, authorization),
+            getTaskCompletionHandlerForDataOperationRequest(topicParamInQuery, dataOperationRequest, requestId),
             timeOutInMilliSeconds);
         return ResponseEntity.ok(Map.of("requestId", requestId));
     }
@@ -139,4 +142,13 @@ public class NcmpPassthroughResourceRequestHandler extends NcmpDatastoreRequestH
         };
     }
 
+    private static BiConsumer<Object, Throwable> getTaskCompletionHandlerForDataOperationRequest(
+            final String topicParamInQuery,
+            final DataOperationRequest dataOperationRequest,
+            final String requestId) {
+        return (result, throwable) ->
+                ResourceDataOperationRequestUtils.handleAsyncTaskCompletionForDataOperationsRequest(topicParamInQuery,
+                        requestId, dataOperationRequest, throwable);
+    }
+
 }
index ba68d5b..2601c7a 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.executor;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 
 import java.util.concurrent.CompletableFuture;
+import java.util.function.BiConsumer;
 import java.util.function.Supplier;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
@@ -31,16 +32,30 @@ import org.springframework.stereotype.Service;
 @Service
 public class CpsNcmpTaskExecutor {
 
+    /**
+     * Execute a task asynchronously, and invoke completion handler when done.
+     *
+     * @param taskSupplier functional method is get() task needed to be executed asynchronously
+     * @param taskCompletionHandler the action to perform on task completion or error
+     * @param timeOutInMillis the time-out value in milliseconds
+     */
+    public void executeTaskWithErrorHandling(final Supplier<Object> taskSupplier,
+                                             final BiConsumer<Object, Throwable> taskCompletionHandler,
+                                             final long timeOutInMillis) {
+        CompletableFuture.supplyAsync(taskSupplier)
+                .orTimeout(timeOutInMillis, MILLISECONDS)
+                .whenCompleteAsync(taskCompletionHandler);
+    }
+
     /**
      * Execute a task asynchronously.
      *
-     * @param taskSupplier functional method is get() task need to executed asynchronously
+     * @param taskSupplier functional method is get() task needed to be executed asynchronously
      * @param timeOutInMillis the time-out value in milliseconds
      */
     public void executeTask(final Supplier<Object> taskSupplier, final long timeOutInMillis) {
-        CompletableFuture.supplyAsync(taskSupplier::get)
-            .orTimeout(timeOutInMillis, MILLISECONDS)
-            .whenCompleteAsync((taskResult, throwable) -> handleTaskCompletion(throwable));
+        executeTaskWithErrorHandling(taskSupplier, (taskResult, throwable) -> handleTaskCompletion(throwable),
+                timeOutInMillis);
     }
 
     private void handleTaskCompletion(final Throwable throwable) {
index a5b1f05..2d7e9b2 100644 (file)
@@ -198,7 +198,7 @@ class NetworkCmProxyControllerSpec extends Specification {
         and: 'async request id is generated'
             assert response.contentAsString.contains('requestId')
         then: 'the request is handled asynchronously'
-            1 * mockCpsTaskExecutor.executeTask(*_)
+            1 * mockCpsTaskExecutor.executeTaskWithErrorHandling(*_)
         where: 'the following data stores are used'
             datastore << [PASSTHROUGH_RUNNING, PASSTHROUGH_OPERATIONAL]
     }
index 1d03be1..7b850a7 100644 (file)
@@ -201,7 +201,8 @@ class NetworkCmProxyInventoryControllerSpec extends Specification {
             def dmiRegistrationResponse = new DmiPluginRegistrationResponse(
                 createdCmHandles: [createCmHandleResponse],
                 updatedCmHandles: [updateCmHandleResponse],
-                removedCmHandles: [removeCmHandleResponse]
+                removedCmHandles: [removeCmHandleResponse],
+                upgradedCmHandles: [upgradeCmHandleResponse]
             )
             mockNetworkCmProxyDataService.updateDmiRegistrationAndSyncModule(*_) >> dmiRegistrationResponse
         when: 'registration endpoint is invoked'
@@ -218,15 +219,18 @@ class NetworkCmProxyInventoryControllerSpec extends Specification {
             responseBody.getFailedCreatedCmHandles() == expectedFailedCreatedCmHandle
             responseBody.getFailedUpdatedCmHandles() == expectedFailedUpdateCmHandle
             responseBody.getFailedRemovedCmHandles() == expectedFailedRemovedCmHandle
+            responseBody.getFailedUpgradeCmHandles() == expectedFailedUpgradedCmHandle
         where:
-        scenario               | createCmHandleResponse                 | updateCmHandleResponse                 | removeCmHandleResponse                 || expectedFailedCreatedCmHandle                 | expectedFailedUpdateCmHandle                  | expectedFailedRemovedCmHandle
-        'only create failed'   | expectedFailedResponse('cm-handle-1')  | expectedSuccessResponse('cm-handle-2') | expectedSuccessResponse('cm-handle-3') || [expectedUnknownErrorResponse('cm-handle-1')] | []                                            | []
-        'only update failed'   | expectedSuccessResponse('cm-handle-1') | expectedFailedResponse('cm-handle-2')  | expectedSuccessResponse('cm-handle-3') || []                                            | [expectedUnknownErrorResponse('cm-handle-2')] | []
-        'only delete failed'   | expectedSuccessResponse('cm-handle-1') | expectedSuccessResponse('cm-handle-2') | expectedFailedResponse('cm-handle-3')  || []                                            | []                                            | [expectedUnknownErrorResponse('cm-handle-3')]
-        'all three failed'     | expectedFailedResponse('cm-handle-1')  | expectedFailedResponse('cm-handle-2')  | expectedFailedResponse('cm-handle-3')  || [expectedUnknownErrorResponse('cm-handle-1')] | [expectedUnknownErrorResponse('cm-handle-2')] | [expectedUnknownErrorResponse('cm-handle-3')]
-        'create update failed' | expectedFailedResponse('cm-handle-1')  | expectedFailedResponse('cm-handle-2')  | expectedSuccessResponse('cm-handle-3') || [expectedUnknownErrorResponse('cm-handle-1')] | [expectedUnknownErrorResponse('cm-handle-2')] | []
-        'create delete failed' | expectedFailedResponse('cm-handle-1')  | expectedSuccessResponse('cm-handle-2') | expectedFailedResponse('cm-handle-3')  || [expectedUnknownErrorResponse('cm-handle-1')] | []                                            | [expectedUnknownErrorResponse('cm-handle-3')]
-        'update delete failed' | expectedSuccessResponse('cm-handle-1') | expectedFailedResponse('cm-handle-2')  | expectedFailedResponse('cm-handle-3')  || []                                            | [expectedUnknownErrorResponse('cm-handle-2')] | [expectedUnknownErrorResponse('cm-handle-3')]
+            scenario                | createCmHandleResponse                 | updateCmHandleResponse                 | removeCmHandleResponse                 | upgradeCmHandleResponse                || expectedFailedCreatedCmHandle                 | expectedFailedUpdateCmHandle                  | expectedFailedRemovedCmHandle                 | expectedFailedUpgradedCmHandle
+            'only create failed'    | expectedFailedResponse('cm-handle-1')  | expectedSuccessResponse('cm-handle-2') | expectedSuccessResponse('cm-handle-3') | expectedSuccessResponse('cm-handle-4') || [expectedUnknownErrorResponse('cm-handle-1')] | []                                            | []                                            | []
+            'only update failed'    | expectedSuccessResponse('cm-handle-1') | expectedFailedResponse('cm-handle-2')  | expectedSuccessResponse('cm-handle-3') | expectedSuccessResponse('cm-handle-4') || []                                            | [expectedUnknownErrorResponse('cm-handle-2')] | []                                            | []
+            'only delete failed'    | expectedSuccessResponse('cm-handle-1') | expectedSuccessResponse('cm-handle-2') | expectedFailedResponse('cm-handle-3')  | expectedSuccessResponse('cm-handle-4') || []                                            | []                                            | [expectedUnknownErrorResponse('cm-handle-3')] | []
+            'only upgrade failed'   | expectedSuccessResponse('cm-handle-1') | expectedSuccessResponse('cm-handle-2') | expectedSuccessResponse('cm-handle-3') | expectedFailedResponse('cm-handle-4')  || []                                            | []                                            | []                                            | [expectedUnknownErrorResponse('cm-handle-4')]
+            'all four failed'       | expectedFailedResponse('cm-handle-1')  | expectedFailedResponse('cm-handle-2')  | expectedFailedResponse('cm-handle-3')  | expectedFailedResponse('cm-handle-4')  || [expectedUnknownErrorResponse('cm-handle-1')] | [expectedUnknownErrorResponse('cm-handle-2')] | [expectedUnknownErrorResponse('cm-handle-3')] | [expectedUnknownErrorResponse('cm-handle-4')]
+            'create update failed'  | expectedFailedResponse('cm-handle-1')  | expectedFailedResponse('cm-handle-2')  | expectedSuccessResponse('cm-handle-3') | expectedSuccessResponse('cm-handle-4') || [expectedUnknownErrorResponse('cm-handle-1')] | [expectedUnknownErrorResponse('cm-handle-2')] | []                                            | []
+            'create delete failed'  | expectedFailedResponse('cm-handle-1')  | expectedSuccessResponse('cm-handle-2') | expectedFailedResponse('cm-handle-3')  | expectedSuccessResponse('cm-handle-4') || [expectedUnknownErrorResponse('cm-handle-1')] | []                                            | [expectedUnknownErrorResponse('cm-handle-3')] | []
+            'update delete failed'  | expectedSuccessResponse('cm-handle-1') | expectedFailedResponse('cm-handle-2')  | expectedFailedResponse('cm-handle-3')  | expectedSuccessResponse('cm-handle-4') || []                                            | [expectedUnknownErrorResponse('cm-handle-2')] | [expectedUnknownErrorResponse('cm-handle-3')] | []
+            'delete upgrade failed' | expectedSuccessResponse('cm-handle-1') | expectedSuccessResponse('cm-handle-2') | expectedFailedResponse('cm-handle-3')  | expectedFailedResponse('cm-handle-4')  || []                                            | []                                            | [expectedUnknownErrorResponse('cm-handle-3')] | [expectedUnknownErrorResponse('cm-handle-4')]
     }
 
     def 'Get all cm handle IDs by DMI plugin identifier.'() {
index aef37c9..641715d 100644 (file)
@@ -79,7 +79,7 @@ class NcmpDatastoreRequestHandlerSpec extends Specification {
         when: 'data operation request is executed'
             objectUnderTest.executeRequest('someTopic', new DataOperationRequest(), NO_AUTH_HEADER)
         then: 'the task is executed in an async fashion or not'
-            expectedCalls * spiedCpsNcmpTaskExecutor.executeTask(*_)
+            expectedCalls * spiedCpsNcmpTaskExecutor.executeTaskWithErrorHandling(*_)
         where: 'the following parameters are used'
             scenario | notificationFeatureEnabled || expectedCalls
             'on'     | true                       || 1
@@ -101,7 +101,7 @@ class NcmpDatastoreRequestHandlerSpec extends Specification {
         when: 'data operation request is executed'
             objectUnderTest.executeRequest('myTopic', dataOperationRequest, NO_AUTH_HEADER)
         then: 'the task is executed in an async fashion'
-            1 * spiedCpsNcmpTaskExecutor.executeTask(*_)
+            1 * spiedCpsNcmpTaskExecutor.executeTaskWithErrorHandling(*_)
         and: 'the network service is invoked'
             new PollingConditions().within(1) {
                 assert networkServiceMethodCalled == true
@@ -143,14 +143,15 @@ class NcmpDatastoreRequestHandlerSpec extends Specification {
 
     def 'Attempt to execute async data operation request with too many cm handles.'() {
         given: 'a data operation definition with too many cm handles'
-            def cmHandleIds = new String[51]
+            def tooMany = objectUnderTest.MAXIMUM_CM_HANDLES_PER_OPERATION+1
+            def cmHandleIds = new String[tooMany]
             def dataOperationDefinition = new DataOperationDefinition(operationId: 'abc', operation: 'read', datastore: 'ncmp-datastore:passthrough-running', cmHandleIds: cmHandleIds)
         when: 'data operation request is executed'
             objectUnderTest.executeRequest('someTopic', new DataOperationRequest(dataOperationDefinitions:[dataOperationDefinition]), NO_AUTH_HEADER)
         then: 'a payload too large exception is thrown'
             def exceptionThrown = thrown(PayloadTooLargeException)
         and: 'the error message contains the offending number of cm handles'
-            assert exceptionThrown.message == "Operation 'abc' affects too many (51) cm handles"
+            assert exceptionThrown.message == "Operation 'abc' affects too many (${tooMany}) cm handles"
     }
 
 }
index 010eda9..4c8c40f 100644 (file)
@@ -33,6 +33,7 @@ class CpsNcmpTaskExecutorSpec extends Specification {
     def objectUnderTest = new CpsNcmpTaskExecutor()
     def logger = Spy(ListAppender<ILoggingEvent>)
     def enoughTime = 100
+    def notEnoughTime = 10
 
     void setup() {
         ((Logger) LoggerFactory.getLogger(CpsNcmpTaskExecutor.class)).addAppender(logger)
@@ -67,6 +68,18 @@ class CpsNcmpTaskExecutorSpec extends Specification {
             assert loggingEvent.formattedMessage.contains('original exception message')
     }
 
+    def 'Task times out.'() {
+        when: 'task is executed without enough time to complete'
+            objectUnderTest.executeTask(taskSupplierForLongRunningTask(), notEnoughTime)
+        then: 'an event is logged with level ERROR'
+            new PollingConditions().within(1) {
+                def loggingEvent = getLoggingEvent()
+                assert loggingEvent.level == Level.ERROR
+            }
+        and: 'a timeout error message is logged'
+            assert loggingEvent.formattedMessage.contains('java.util.concurrent.TimeoutException')
+    }
+
     def taskSupplier() {
         return () -> 'hello world'
     }
@@ -75,6 +88,10 @@ class CpsNcmpTaskExecutorSpec extends Specification {
         return () -> { throw new RuntimeException('original exception message') }
     }
 
+    def taskSupplierForLongRunningTask() {
+        return () -> { sleep(enoughTime) }
+    }
+
     def getLoggingEvent() {
         return logger.list[0]
     }
index 1510be9..ff0654a 100644 (file)
@@ -27,7 +27,7 @@
     <parent>
         <groupId>org.onap.cps</groupId>
         <artifactId>cps-parent</artifactId>
-        <version>3.4.8-SNAPSHOT</version>
+        <version>3.4.9-SNAPSHOT</version>
         <relativePath>../cps-parent/pom.xml</relativePath>
     </parent>
 
index 729930e..d547e31 100644 (file)
@@ -1,4 +1,4 @@
-/*-
+/*
  * ============LICENSE_START=======================================================
  *  Copyright (C) 2023 Nordix Foundation.
  * ================================================================================
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2023 Nordix Foundation
+ *  Copyright (C) 2023-2024 Nordix Foundation
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -31,30 +31,32 @@ import org.onap.cps.ncmp.api.impl.utils.EventDateTimeFormatter;
 import org.onap.cps.ncmp.api.impl.utils.context.CpsApplicationContext;
 import org.onap.cps.utils.JsonObjectMapper;
 
-@Builder(buildMethodName = "setCloudEvent")
-public class NcmpCloudEventBuilder {
+@Builder
+public class NcmpEvent {
 
-    private Object event;
+    private Object data;
     private Map<String, String> extensions;
     private String type;
     @Builder.Default
-    private static final String EVENT_SPEC_VERSION_V1 = "1.0.0";
+    private static final String CLOUD_EVENT_SPEC_VERSION_V1 = "1.0.0";
+    @Builder.Default
+    private static final String CLOUD_EVENT_SOURCE = "NCMP";
 
     /**
      * Creates ncmp cloud event with provided attributes.
      *
      * @return Cloud Event
      */
-    public CloudEvent build() {
+    public CloudEvent asCloudEvent() {
         final JsonObjectMapper jsonObjectMapper = CpsApplicationContext.getCpsBean(JsonObjectMapper.class);
         final CloudEventBuilder cloudEventBuilder = CloudEventBuilder.v1()
                 .withId(UUID.randomUUID().toString())
-                .withSource(URI.create("NCMP"))
+                .withSource(URI.create(CLOUD_EVENT_SOURCE))
                 .withType(type)
-                .withDataSchema(URI.create("urn:cps:" + type + ":" + EVENT_SPEC_VERSION_V1))
+                .withDataSchema(URI.create("urn:cps:" + type + ":" + CLOUD_EVENT_SPEC_VERSION_V1))
                 .withTime(EventDateTimeFormatter.toIsoOffsetDateTime(
                         EventDateTimeFormatter.getCurrentIsoFormattedDateTime()))
-                .withData(jsonObjectMapper.asJsonBytes(event));
+                .withData(jsonObjectMapper.asJsonBytes(data));
         extensions.entrySet().stream()
                 .filter(extensionEntry -> StringUtils.isNotBlank(extensionEntry.getValue()))
                 .forEach(extensionEntry ->
index 9bd1119..7afe606 100644 (file)
@@ -26,7 +26,7 @@ import java.util.HashMap;
 import java.util.Map;
 import lombok.RequiredArgsConstructor;
 import org.onap.cps.events.EventsPublisher;
-import org.onap.cps.ncmp.api.impl.events.NcmpCloudEventBuilder;
+import org.onap.cps.ncmp.api.impl.events.NcmpEvent;
 import org.onap.cps.ncmp.events.avc.ncmp_to_client.Avc;
 import org.onap.cps.ncmp.events.avc.ncmp_to_client.AvcEvent;
 import org.onap.cps.ncmp.events.avc.ncmp_to_client.Data;
@@ -53,8 +53,8 @@ public class AvcEventPublisher {
 
         final Map<String, String> extensions = createAvcEventExtensions(eventKey);
         final CloudEvent avcCloudEvent =
-            NcmpCloudEventBuilder.builder().type(AvcEvent.class.getTypeName())
-            .event(avcEvent).extensions(extensions).setCloudEvent().build();
+            NcmpEvent.builder().type(AvcEvent.class.getTypeName())
+            .data(avcEvent).extensions(extensions).build().asCloudEvent();
 
         eventsPublisher.publishCloudEvent(avcTopic, eventKey, avcCloudEvent);
     }
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/CmNotificationSubscriptionEventsHandler.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/CmNotificationSubscriptionEventsHandler.java
new file mode 100644 (file)
index 0000000..50a5df5
--- /dev/null
@@ -0,0 +1,70 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (c) 2024 Nordix Foundation.
+ *  ================================================================================
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an 'AS IS' BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ *  SPDX-License-Identifier: Apache-2.0
+ *  ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.ncmp.api.impl.events.cmsubscription;
+
+import lombok.RequiredArgsConstructor;
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.producer.CmNotificationSubscriptionDmiInEventProducer;
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.producer.CmNotificationSubscriptionNcmpOutEventProducer;
+import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.ncmp_to_dmi.CmNotificationSubscriptionDmiInEvent;
+import org.onap.cps.ncmp.events.cmsubscription_merge1_0_0.ncmp_to_client.CmNotificationSubscriptionNcmpOutEvent;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class CmNotificationSubscriptionEventsHandler {
+    private final CmNotificationSubscriptionNcmpOutEventProducer cmNotificationSubscriptionNcmpOutEventProducer;
+    private final CmNotificationSubscriptionDmiInEventProducer cmNotificationSubscriptionDmiInEventProducer;
+
+    /**
+     * Publish the event to the client who requested the subscription with key as subscription id and event is Cloud
+     * Event compliant.
+     *
+     * @param subscriptionId                         Cm Subscription id
+     * @param eventType                              Type of event
+     * @param cmNotificationSubscriptionNcmpOutEvent Cm Notification Subscription Event for the
+     *                                               client
+     * @param isScheduledEvent                       Determines if the event is to be scheduled
+     *                                               or published now
+     */
+    public void publishCmNotificationSubscriptionNcmpOutEvent(final String subscriptionId, final String eventType,
+                                                              final CmNotificationSubscriptionNcmpOutEvent
+                                                                      cmNotificationSubscriptionNcmpOutEvent,
+                                                              final boolean isScheduledEvent) {
+        cmNotificationSubscriptionNcmpOutEventProducer.publishCmNotificationSubscriptionNcmpOutEvent(subscriptionId,
+                eventType, cmNotificationSubscriptionNcmpOutEvent, isScheduledEvent);
+    }
+
+    /**
+     * Publish the event to the provided dmi plugin with key as subscription id and the event is in Cloud Event format.
+     *
+     * @param subscriptionId                       Cm Subscription id
+     * @param dmiPluginName                        Dmi Plugin Name
+     * @param eventType                            Type of event
+     * @param cmNotificationSubscriptionDmiInEvent Cm Notification Subscription event for Dmi
+     */
+    public void publishCmNotificationSubscriptionDmiInEvent(final String subscriptionId, final String dmiPluginName,
+                                                            final String eventType,
+                                                            final CmNotificationSubscriptionDmiInEvent
+                                                                    cmNotificationSubscriptionDmiInEvent) {
+        cmNotificationSubscriptionDmiInEventProducer.publishCmNotificationSubscriptionDmiInEvent(subscriptionId,
+                dmiPluginName, eventType, cmNotificationSubscriptionDmiInEvent);
+    }
+}
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/CmNotificationSubscriptionMappersHandler.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/cmsubscription/CmNotificationSubscriptionMappersHandler.java
new file mode 100644 (file)
index 0000000..73f9563
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2024 Nordix Foundation
+ *  ================================================================================
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ *  SPDX-License-Identifier: Apache-2.0
+ *  ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.ncmp.api.impl.events.cmsubscription;
+
+import java.util.List;
+import java.util.Map;
+import lombok.RequiredArgsConstructor;
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.mapper.CmNotificationSubscriptionDmiInEventMapper;
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.mapper.CmNotificationSubscriptionNcmpOutEventMapper;
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.model.DmiCmNotificationSubscriptionDetails;
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.model.DmiCmNotificationSubscriptionPredicate;
+import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.ncmp_to_dmi.CmNotificationSubscriptionDmiInEvent;
+import org.onap.cps.ncmp.events.cmsubscription_merge1_0_0.ncmp_to_client.CmNotificationSubscriptionNcmpOutEvent;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class CmNotificationSubscriptionMappersHandler {
+
+    private final CmNotificationSubscriptionDmiInEventMapper cmNotificationSubscriptionDmiInEventMapper;
+    private final CmNotificationSubscriptionNcmpOutEventMapper cmNotificationSubscriptionNcmpOutEventMapper;
+
+    /**
+     * Mapper to form a request for the DMI Plugin for the Cm Notification Subscription.
+     *
+     * @param dmiCmNotificationSubscriptionPredicates Collection of Cm Notification Subscription predicates
+     * @return cm notification subscription dmi in event
+     */
+    public CmNotificationSubscriptionDmiInEvent toCmNotificationSubscriptionDmiInEvent(
+            final List<DmiCmNotificationSubscriptionPredicate> dmiCmNotificationSubscriptionPredicates) {
+        return cmNotificationSubscriptionDmiInEventMapper.toCmNotificationSubscriptionDmiInEvent(
+                dmiCmNotificationSubscriptionPredicates);
+    }
+
+    /**
+     * Mapper to form a response for the client for the Cm Notification Subscription.
+     *
+     * @param subscriptionId                          Cm Notification Subscription id
+     * @param dmiCmNotificationSubscriptionDetailsMap contains CmNotificationSubscriptionDetails per dmi plugin
+     * @return CmNotificationSubscriptionNcmpOutEvent to sent back to the client
+     */
+    public CmNotificationSubscriptionNcmpOutEvent toCmNotificationSubscriptionNcmpOutEvent(final String subscriptionId,
+         final Map<String, DmiCmNotificationSubscriptionDetails> dmiCmNotificationSubscriptionDetailsMap) {
+        return cmNotificationSubscriptionNcmpOutEventMapper.toCmNotificationSubscriptionNcmpOutEvent(subscriptionId,
+                dmiCmNotificationSubscriptionDetailsMap);
+    }
+
+    /**
+     * Mapper to form a rejected response for the client for the Cm Notification Subscription Request.
+     *
+     * @param subscriptionId subscription id
+     * @param rejectedTargetFilters list of rejected target filters for the subscription request
+     * @return to sent back to the client
+     */
+    public CmNotificationSubscriptionNcmpOutEvent toCmNotificationSubscriptionNcmpOutEventForRejectedRequest(
+            final String subscriptionId, final List<String> rejectedTargetFilters) {
+        return cmNotificationSubscriptionNcmpOutEventMapper.toCmNotificationSubscriptionNcmpOutEventForRejectedRequest(
+                subscriptionId, rejectedTargetFilters);
+    }
+}
index f7ea4a4..f7dd51e 100644 (file)
 
 package org.onap.cps.ncmp.api.impl.events.cmsubscription;
 
-import static org.onap.cps.ncmp.api.impl.events.cmsubscription.CmNotificationSubscriptionNcmpOutEventProducer.buildAndGetCmNotificationNcmpOutEventAsCloudEvent;
+import static org.onap.cps.ncmp.api.impl.events.cmsubscription.producer.CmNotificationSubscriptionNcmpOutEventProducer.buildAndGetCmNotificationNcmpOutEventAsCloudEvent;
 
 import io.cloudevents.CloudEvent;
 import java.util.Map;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.onap.cps.events.EventsPublisher;
-import org.onap.cps.ncmp.api.impl.events.cmsubscription.mapper.CmNotificationSubscriptionNcmpOutEventMapper;
 import org.onap.cps.ncmp.api.impl.events.cmsubscription.model.DmiCmNotificationSubscriptionDetails;
 import org.onap.cps.ncmp.events.cmsubscription_merge1_0_0.ncmp_to_client.CmNotificationSubscriptionNcmpOutEvent;
 import org.onap.cps.utils.JsonObjectMapper;
@@ -36,14 +35,13 @@ import org.onap.cps.utils.JsonObjectMapper;
 @RequiredArgsConstructor
 public class CmNotificationSubscriptionNcmpOutEventPublishingTask implements Runnable {
 
-
     private final String topicName;
     private final String subscriptionId;
     private final String eventType;
     private final EventsPublisher<CloudEvent> eventsPublisher;
     private final JsonObjectMapper jsonObjectMapper;
-    private final Map<String, Map<String, DmiCmNotificationSubscriptionDetails>> cmNotificationSubscriptionCache;
-    private final CmNotificationSubscriptionNcmpOutEventMapper cmNotificationSubscriptionNcmpOutEventMapper;
+    private final CmNotificationSubscriptionMappersHandler cmNotificationSubscriptionMappersHandler;
+    private final DmiCmNotificationSubscriptionCacheHandler dmiCmNotificationSubscriptionCacheHandler;
 
     /**
      * Delegating the responsibility of publishing CmNotificationSubscriptionNcmpOutEvent as a separate task which will
@@ -52,13 +50,14 @@ public class CmNotificationSubscriptionNcmpOutEventPublishingTask implements Run
     @Override
     public void run() {
         final Map<String, DmiCmNotificationSubscriptionDetails> dmiCmNotificationSubscriptionDetailsMap =
-                cmNotificationSubscriptionCache.get(subscriptionId);
+                dmiCmNotificationSubscriptionCacheHandler.get(subscriptionId);
         final CmNotificationSubscriptionNcmpOutEvent cmNotificationSubscriptionNcmpOutEvent =
-                cmNotificationSubscriptionNcmpOutEventMapper.toCmNotificationSubscriptionNcmpOutEvent(subscriptionId,
+                cmNotificationSubscriptionMappersHandler.toCmNotificationSubscriptionNcmpOutEvent(subscriptionId,
                         dmiCmNotificationSubscriptionDetailsMap);
         eventsPublisher.publishCloudEvent(topicName, subscriptionId,
                 buildAndGetCmNotificationNcmpOutEventAsCloudEvent(jsonObjectMapper, subscriptionId, eventType,
                         cmNotificationSubscriptionNcmpOutEvent));
+        dmiCmNotificationSubscriptionCacheHandler
+                .removeAcceptedAndRejectedDmiCmNotificationSubscriptionEntries(subscriptionId);
     }
-
 }
index 8c1cac3..34ffb5e 100644 (file)
@@ -29,6 +29,7 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.stream.Collectors;
 import lombok.RequiredArgsConstructor;
 import org.onap.cps.ncmp.api.impl.events.cmsubscription.model.CmNotificationSubscriptionStatus;
 import org.onap.cps.ncmp.api.impl.events.cmsubscription.model.DmiCmNotificationSubscriptionDetails;
@@ -51,13 +52,40 @@ public class DmiCmNotificationSubscriptionCacheHandler {
     /**
      * Adds new subscription to the subscription cache.
      *
-     * @param subscriptionId    subscription Id
+     * @param subscriptionId    subscription id
      * @param predicates        subscription request predicates
      */
     public void add(final String subscriptionId, final List<Predicate> predicates) {
         cmNotificationSubscriptionCache.put(subscriptionId, createDmiCmNotificationSubscriptionsPerDmi(predicates));
     }
 
+    /**
+     * Get cm notification subscription cache entry via subscription id.
+     *
+     * @param subscriptionId    subscription id
+     * @return map of dmi cm notification subscriptions per dmi
+     */
+    public Map<String, DmiCmNotificationSubscriptionDetails> get(final String subscriptionId) {
+        return cmNotificationSubscriptionCache.get(subscriptionId);
+    }
+
+
+    /**
+     * Remove cache entries with CmNotificationSubscriptionStatus ACCEPTED/REJECTED via subscription id.
+     *
+     * @param subscriptionId subscription id as key in CM notification Subscription cache.
+     */
+    public void removeAcceptedAndRejectedDmiCmNotificationSubscriptionEntries(final String subscriptionId) {
+        final Map<String, DmiCmNotificationSubscriptionDetails> dmiCmNotificationSubscriptionsPerDmi =
+                cmNotificationSubscriptionCache.get(subscriptionId);
+        final Map<String, DmiCmNotificationSubscriptionDetails> updatedDmiCmNotificationSubscriptionsPerDmi =
+                dmiCmNotificationSubscriptionsPerDmi.entrySet().stream().filter(
+                                dmiCmNotificationSubscription ->
+                                        !isAcceptedOrRejected(dmiCmNotificationSubscription.getValue()))
+                        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+        cmNotificationSubscriptionCache.put(subscriptionId, updatedDmiCmNotificationSubscriptionsPerDmi);
+    }
+
     /**
      *  Creates map of subscription details per DMI.
      *
@@ -95,9 +123,9 @@ public class DmiCmNotificationSubscriptionCacheHandler {
      *
      */
     public void updateDmiCmNotificationSubscriptionStatusPerDmi(
-        final String subscriptionId, final String dmiServiceName, final CmNotificationSubscriptionStatus status) {
+            final String subscriptionId, final String dmiServiceName, final CmNotificationSubscriptionStatus status) {
         cmNotificationSubscriptionCache.get(subscriptionId).get(dmiServiceName)
-            .setCmNotificationSubscriptionStatus(status);
+                .setCmNotificationSubscriptionStatus(status);
     }
 
     /**
@@ -109,18 +137,18 @@ public class DmiCmNotificationSubscriptionCacheHandler {
      */
     public void persistIntoDatabasePerDmi(final String subscriptionId, final String dmiServiceName) {
         final List<DmiCmNotificationSubscriptionPredicate> dmiCmNotificationSubscriptionPredicateList =
-            cmNotificationSubscriptionCache.get(subscriptionId).get(dmiServiceName)
-            .getDmiCmNotificationSubscriptionPredicates();
+                cmNotificationSubscriptionCache.get(subscriptionId).get(dmiServiceName)
+                        .getDmiCmNotificationSubscriptionPredicates();
         for (final DmiCmNotificationSubscriptionPredicate dmiCmNotificationSubscriptionPredicate:
-            dmiCmNotificationSubscriptionPredicateList) {
+                dmiCmNotificationSubscriptionPredicateList) {
             final DatastoreType datastoreType = dmiCmNotificationSubscriptionPredicate.getDatastoreType();
             final Set<String> cmHandles = dmiCmNotificationSubscriptionPredicate.getTargetCmHandleIds();
             final Set<String> xpaths = dmiCmNotificationSubscriptionPredicate.getXpaths();
 
             for (final String cmHandle: cmHandles) {
                 for (final String xpath: xpaths) {
-                    cmNotificationSubscriptionPersistenceService.addOrUpdateCmNotificationSubscription(datastoreType,
-                        cmHandle, xpath, subscriptionId);
+                    cmNotificationSubscriptionPersistenceService.addCmNotificationSubscription(datastoreType, cmHandle,
+                            xpath, subscriptionId);
                 }
             }
         }
@@ -153,4 +181,10 @@ public class DmiCmNotificationSubscriptionCacheHandler {
         }
         return targetCmHandlesByDmiServiceNames;
     }
+
+    private boolean isAcceptedOrRejected(
+            final DmiCmNotificationSubscriptionDetails dmiCmNotificationSubscription) {
+        return dmiCmNotificationSubscription.getCmNotificationSubscriptionStatus().toString().equals("ACCEPTED")
+                || dmiCmNotificationSubscription.getCmNotificationSubscriptionStatus().toString().equals("REJECTED");
+    }
 }
\ No newline at end of file
@@ -18,7 +18,7 @@
  *  ============LICENSE_END=========================================================
  */
 
-package org.onap.cps.ncmp.api.impl.events.cmsubscription;
+package org.onap.cps.ncmp.api.impl.events.cmsubscription.consumer;
 
 import static org.onap.cps.ncmp.api.impl.events.mapper.CloudEventMapper.toTargetEvent;
 
@@ -26,6 +26,7 @@ import io.cloudevents.CloudEvent;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.kafka.clients.consumer.ConsumerRecord;
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.DmiCmNotificationSubscriptionCacheHandler;
 import org.onap.cps.ncmp.api.impl.events.cmsubscription.model.CmNotificationSubscriptionStatus;
 import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.dmi_to_ncmp.CmNotificationSubscriptionDmiOutEvent;
 import org.springframework.kafka.annotation.KafkaListener;
@@ -18,7 +18,7 @@
  *  ============LICENSE_END=========================================================
  */
 
-package org.onap.cps.ncmp.api.impl.events.cmsubscription;
+package org.onap.cps.ncmp.api.impl.events.cmsubscription.producer;
 
 import io.cloudevents.CloudEvent;
 import io.cloudevents.core.builder.CloudEventBuilder;
@@ -33,8 +33,9 @@ import java.util.concurrent.TimeUnit;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.onap.cps.events.EventsPublisher;
-import org.onap.cps.ncmp.api.impl.events.cmsubscription.mapper.CmNotificationSubscriptionNcmpOutEventMapper;
-import org.onap.cps.ncmp.api.impl.events.cmsubscription.model.DmiCmNotificationSubscriptionDetails;
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.CmNotificationSubscriptionMappersHandler;
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.CmNotificationSubscriptionNcmpOutEventPublishingTask;
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.DmiCmNotificationSubscriptionCacheHandler;
 import org.onap.cps.ncmp.events.cmsubscription_merge1_0_0.ncmp_to_client.CmNotificationSubscriptionNcmpOutEvent;
 import org.onap.cps.utils.JsonObjectMapper;
 import org.springframework.beans.factory.annotation.Value;
@@ -55,8 +56,8 @@ public class CmNotificationSubscriptionNcmpOutEventProducer {
 
     private final EventsPublisher<CloudEvent> eventsPublisher;
     private final JsonObjectMapper jsonObjectMapper;
-    private final Map<String, Map<String, DmiCmNotificationSubscriptionDetails>> cmNotificationSubscriptionCache;
-    private final CmNotificationSubscriptionNcmpOutEventMapper cmNotificationSubscriptionNcmpOutEventMapper;
+    private final CmNotificationSubscriptionMappersHandler cmNotificationSubscriptionMappersHandler;
+    private final DmiCmNotificationSubscriptionCacheHandler dmiCmNotificationSubscriptionCacheHandler;
     private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
     private static final Map<String, ScheduledFuture<?>> scheduledTasksPerSubscriptionId = new ConcurrentHashMap<>();
 
@@ -72,8 +73,9 @@ public class CmNotificationSubscriptionNcmpOutEventProducer {
      *                                               or published now
      */
     public void publishCmNotificationSubscriptionNcmpOutEvent(final String subscriptionId, final String eventType,
-            final CmNotificationSubscriptionNcmpOutEvent cmNotificationSubscriptionNcmpOutEvent,
-            final boolean isScheduledEvent) {
+                                                              final CmNotificationSubscriptionNcmpOutEvent
+                                                                      cmNotificationSubscriptionNcmpOutEvent,
+                                                              final boolean isScheduledEvent) {
 
         if (isScheduledEvent && !scheduledTasksPerSubscriptionId.containsKey(subscriptionId)) {
             final ScheduledFuture<?> scheduledFuture =
@@ -86,16 +88,15 @@ public class CmNotificationSubscriptionNcmpOutEventProducer {
                     cmNotificationSubscriptionNcmpOutEvent);
             log.info("Published CmNotificationSubscriptionEvent on demand for subscriptionId : {}", subscriptionId);
         }
-
     }
 
     private ScheduledFuture<?> scheduleAndPublishCmNotificationSubscriptionNcmpOutEvent(final String subscriptionId,
-            final String eventType) {
+                                                                                        final String eventType) {
         final CmNotificationSubscriptionNcmpOutEventPublishingTask
                 cmNotificationSubscriptionNcmpOutEventPublishingTask =
                 new CmNotificationSubscriptionNcmpOutEventPublishingTask(cmNotificationSubscriptionNcmpOutEventTopic,
-                        subscriptionId, eventType, eventsPublisher, jsonObjectMapper, cmNotificationSubscriptionCache,
-                        cmNotificationSubscriptionNcmpOutEventMapper);
+                        subscriptionId, eventType, eventsPublisher, jsonObjectMapper,
+                        cmNotificationSubscriptionMappersHandler, dmiCmNotificationSubscriptionCacheHandler);
         return scheduledExecutorService.schedule(cmNotificationSubscriptionNcmpOutEventPublishingTask,
                 cmNotificationSubscriptionDmiOutEventTimeoutInMs, TimeUnit.MILLISECONDS);
     }
@@ -112,22 +113,34 @@ public class CmNotificationSubscriptionNcmpOutEventProducer {
 
 
     private void publishCmNotificationSubscriptionNcmpOutEventNow(final String subscriptionId, final String eventType,
-            final CmNotificationSubscriptionNcmpOutEvent cmNotificationSubscriptionNcmpOutEvent) {
+                                                                  final CmNotificationSubscriptionNcmpOutEvent
+                                                                          cmNotificationSubscriptionNcmpOutEvent) {
         final CloudEvent cmNotificationSubscriptionNcmpOutEventAsCloudEvent =
                 buildAndGetCmNotificationNcmpOutEventAsCloudEvent(jsonObjectMapper, subscriptionId, eventType,
                         cmNotificationSubscriptionNcmpOutEvent);
         eventsPublisher.publishCloudEvent(cmNotificationSubscriptionNcmpOutEventTopic, subscriptionId,
                 cmNotificationSubscriptionNcmpOutEventAsCloudEvent);
+        dmiCmNotificationSubscriptionCacheHandler
+                .removeAcceptedAndRejectedDmiCmNotificationSubscriptionEntries(subscriptionId);
     }
 
-    protected static CloudEvent buildAndGetCmNotificationNcmpOutEventAsCloudEvent(
+    /**
+     * Get an NCMP out event as cloud event.
+     *
+     * @param jsonObjectMapper  JSON object mapper
+     * @param subscriptionId subscription id
+     * @param eventType event type
+     * @param cmNotificationSubscriptionNcmpOutEvent cm notification subscription NCMP out event
+     * @return cm notification subscription NCMP out event as cloud event
+     */
+    public static CloudEvent buildAndGetCmNotificationNcmpOutEventAsCloudEvent(
             final JsonObjectMapper jsonObjectMapper, final String subscriptionId, final String eventType,
             final CmNotificationSubscriptionNcmpOutEvent cmNotificationSubscriptionNcmpOutEvent) {
 
         return CloudEventBuilder.v1().withId(UUID.randomUUID().toString()).withType(eventType)
-                       .withSource(URI.create("NCMP")).withDataSchema(URI.create("org.onap.ncmp.cm.subscription:1.0.0"))
-                       .withExtension("correlationid", subscriptionId)
-                       .withData(jsonObjectMapper.asJsonBytes(cmNotificationSubscriptionNcmpOutEvent)).build();
+                .withSource(URI.create("NCMP")).withDataSchema(URI.create("org.onap.ncmp.cm.subscription:1.0.0"))
+                .withExtension("correlationid", subscriptionId)
+                .withData(jsonObjectMapper.asJsonBytes(cmNotificationSubscriptionNcmpOutEvent)).build();
     }
 
 }
index 8204f05..395c511 100644 (file)
@@ -22,14 +22,19 @@ package org.onap.cps.ncmp.api.impl.events.cmsubscription.service;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.stream.Collectors;
 import lombok.RequiredArgsConstructor;
-import org.onap.cps.ncmp.api.impl.events.cmsubscription.CmNotificationSubscriptionNcmpOutEventProducer;
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.CmNotificationSubscriptionDelta;
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.CmNotificationSubscriptionEventsHandler;
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.CmNotificationSubscriptionMappersHandler;
 import org.onap.cps.ncmp.api.impl.events.cmsubscription.DmiCmNotificationSubscriptionCacheHandler;
-import org.onap.cps.ncmp.api.impl.events.cmsubscription.mapper.CmNotificationSubscriptionNcmpOutEventMapper;
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.model.DmiCmNotificationSubscriptionDetails;
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.model.DmiCmNotificationSubscriptionPredicate;
 import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.client_to_ncmp.CmNotificationSubscriptionNcmpInEvent;
 import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.client_to_ncmp.Predicate;
+import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.ncmp_to_dmi.CmNotificationSubscriptionDmiInEvent;
 import org.onap.cps.ncmp.events.cmsubscription_merge1_0_0.ncmp_to_client.CmNotificationSubscriptionNcmpOutEvent;
 import org.springframework.stereotype.Service;
 
@@ -38,21 +43,23 @@ import org.springframework.stereotype.Service;
 public class CmNotificationSubscriptionHandlerServiceImpl implements CmNotificationSubscriptionHandlerService {
 
     private final CmNotificationSubscriptionPersistenceService cmNotificationSubscriptionPersistenceService;
-    private final CmNotificationSubscriptionNcmpOutEventMapper cmNotificationSubscriptionNcmpOutEventMapper;
-    private final CmNotificationSubscriptionNcmpOutEventProducer cmNotificationSubscriptionNcmpOutEventProducer;
+    private final CmNotificationSubscriptionDelta cmNotificationSubscriptionDelta;
+    private final CmNotificationSubscriptionMappersHandler cmNotificationSubscriptionMappersHandler;
+    private final CmNotificationSubscriptionEventsHandler cmNotificationSubscriptionEventsHandler;
     private final DmiCmNotificationSubscriptionCacheHandler dmiCmNotificationSubscriptionCacheHandler;
 
     @Override
     public void processSubscriptionCreateRequest(
-        final CmNotificationSubscriptionNcmpInEvent cmNotificationSubscriptionNcmpInEvent) {
+            final CmNotificationSubscriptionNcmpInEvent cmNotificationSubscriptionNcmpInEvent) {
         final String subscriptionId = cmNotificationSubscriptionNcmpInEvent.getData().getSubscriptionId();
         final List<Predicate> predicates = cmNotificationSubscriptionNcmpInEvent.getData().getPredicates();
 
         if (cmNotificationSubscriptionPersistenceService.isUniqueSubscriptionId(subscriptionId)) {
             dmiCmNotificationSubscriptionCacheHandler.add(subscriptionId, predicates);
+            sendSubscriptionCreateRequestToDmi(subscriptionId);
         } else {
             final Set<String> subscriptionTargetFilters = predicates.stream().flatMap(
-                predicate -> predicate.getTargetFilter().stream()).collect(Collectors.toSet());
+                    predicate -> predicate.getTargetFilter().stream()).collect(Collectors.toSet());
             rejectAndPublishCmNotificationSubscriptionCreateRequest(subscriptionId,
                     new ArrayList<>(subscriptionTargetFilters));
         }
@@ -61,10 +68,25 @@ public class CmNotificationSubscriptionHandlerServiceImpl implements CmNotificat
     private void rejectAndPublishCmNotificationSubscriptionCreateRequest(final String subscriptionId,
                                                                          final List<String> subscriptionTargetFilters) {
         final CmNotificationSubscriptionNcmpOutEvent cmNotificationSubscriptionNcmpOutEvent =
-            cmNotificationSubscriptionNcmpOutEventMapper
-                .toCmNotificationSubscriptionNcmpOutEventForRejectedRequest(subscriptionId,
-                    subscriptionTargetFilters);
-        cmNotificationSubscriptionNcmpOutEventProducer.publishCmNotificationSubscriptionNcmpOutEvent(subscriptionId,
-            "subscriptionCreateResponse", cmNotificationSubscriptionNcmpOutEvent, false);
+                cmNotificationSubscriptionMappersHandler
+                        .toCmNotificationSubscriptionNcmpOutEventForRejectedRequest(subscriptionId,
+                                subscriptionTargetFilters);
+        cmNotificationSubscriptionEventsHandler.publishCmNotificationSubscriptionNcmpOutEvent(subscriptionId,
+                "subscriptionCreateResponse", cmNotificationSubscriptionNcmpOutEvent, false);
     }
-}
+
+    private void sendSubscriptionCreateRequestToDmi(final String subscriptionId) {
+        final Map<String, DmiCmNotificationSubscriptionDetails> dmiCmNotificationSubscriptionDetailsMap =
+                dmiCmNotificationSubscriptionCacheHandler.get(subscriptionId);
+        dmiCmNotificationSubscriptionDetailsMap.forEach((dmiPluginName, dmiCmNotificationSubscriptionDetails) -> {
+            final List<DmiCmNotificationSubscriptionPredicate> dmiCmNotificationSubscriptionPredicates =
+                    cmNotificationSubscriptionDelta.getDelta(
+                            dmiCmNotificationSubscriptionDetails.getDmiCmNotificationSubscriptionPredicates());
+            final CmNotificationSubscriptionDmiInEvent cmNotificationSubscriptionDmiInEvent =
+                    cmNotificationSubscriptionMappersHandler.toCmNotificationSubscriptionDmiInEvent(
+                            dmiCmNotificationSubscriptionPredicates);
+            cmNotificationSubscriptionEventsHandler.publishCmNotificationSubscriptionDmiInEvent(subscriptionId,
+                    dmiPluginName, "subscriptionCreateRequest", cmNotificationSubscriptionDmiInEvent);
+        });
+    }
+}
\ No newline at end of file
index 6b02adb..3bb40c3 100644 (file)
@@ -31,9 +31,9 @@ public interface CmNotificationSubscriptionPersistenceService {
     /**
      * Check if we have an ongoing cm subscription based on the parameters.
      *
-     * @param datastoreType valid datastore type
-     * @param cmHandleId    cmhandle id
-     * @param xpath         valid xpath
+     * @param datastoreType the susbcription target datastore type
+     * @param cmHandleId the id of the cm handle for the susbcription
+     * @param xpath the target xpath
      * @return true for ongoing cmsubscription , otherwise false
      */
     boolean isOngoingCmNotificationSubscription(final DatastoreType datastoreType, final String cmHandleId,
@@ -50,22 +50,35 @@ public interface CmNotificationSubscriptionPersistenceService {
     /**
      * Get all ongoing cm notification subscription based on the parameters.
      *
-     * @param datastoreType valid datastore type
-     * @param cmHandleId    cmhandle id
-     * @param xpath         valid xpath
+     * @param datastoreType the susbcription target datastore type
+     * @param cmHandleId the id of the cm handle for the susbcription
+     * @param xpath the target xpath
      * @return collection of subscription ids of ongoing cm notification subscription
      */
     Collection<String> getOngoingCmNotificationSubscriptionIds(final DatastoreType datastoreType,
             final String cmHandleId, final String xpath);
 
     /**
-     * Add or update cm notification subscription.
+     * Add cm notification subscription.
      *
-     * @param datastoreType valid datastore type
-     * @param cmHandle cmhandle id
-     * @param xpath valid xpath
-     * @param newSubscriptionId subscription Id to be added
+     * @param datastoreType the susbcription target datastore type
+     * @param cmHandleId the id of the cm handle for the susbcription
+     * @param xpath the target xpath
+     * @param newSubscriptionId subscription id to be added
      */
-    void addOrUpdateCmNotificationSubscription(final DatastoreType datastoreType, final String cmHandle,
-                                               final String xpath, final String newSubscriptionId);
+    void addCmNotificationSubscription(final DatastoreType datastoreType, final String cmHandleId,
+                                       final String xpath, final String newSubscriptionId);
+
+    /**
+     * Remove cm notification Subscription.
+     *
+     * @param datastoreType the susbcription target datastore type
+     * @param cmHandleId the id of the cm handle for the susbcription
+     * @param xpath the target xpath
+     * @param subscriptionId subscription id to remove
+     */
+    void removeCmNotificationSubscription(final DatastoreType datastoreType, final String cmHandleId,
+                                          final String xpath, final String subscriptionId);
+
 }
+
index 2efd321..92f3459 100644 (file)
@@ -24,7 +24,6 @@ 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;
@@ -60,7 +59,7 @@ public class CmNotificationSubscriptionPersistenceServiceImpl implements CmNotif
 
     @Override
     public boolean isOngoingCmNotificationSubscription(final DatastoreType datastoreType, final String cmHandleId,
-            final String xpath) {
+                                                       final String xpath) {
         return !getOngoingCmNotificationSubscriptionIds(datastoreType, cmHandleId, xpath).isEmpty();
     }
 
@@ -73,7 +72,7 @@ public class CmNotificationSubscriptionPersistenceServiceImpl implements CmNotif
 
     @Override
     public Collection<String> getOngoingCmNotificationSubscriptionIds(final DatastoreType datastoreType,
-            final String cmHandleId, final String xpath) {
+                                                                      final String cmHandleId, final String xpath) {
 
         final String isOngoingCmSubscriptionCpsPathQuery =
                 CM_SUBSCRIPTION_CPS_PATH_QUERY.formatted(datastoreType.getDatastoreName(), cmHandleId,
@@ -88,45 +87,77 @@ public class CmNotificationSubscriptionPersistenceServiceImpl implements CmNotif
     }
 
     @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,
+    public void addCmNotificationSubscription(final DatastoreType datastoreType, final String cmHandleId,
+                                              final String xpath, final String subscriptionId) {
+        if (isOngoingCmNotificationSubscription(datastoreType, cmHandleId, xpath)
+                && (!getOngoingCmNotificationSubscriptionIds(datastoreType, cmHandleId, xpath)
+                .contains(subscriptionId))) {
+            final DataNode subscriptionAsDataNode = getSubscriptionAsDataNode(datastoreType, cmHandleId, xpath);
+            final Collection<String> subscriptionIds = getOngoingCmNotificationSubscriptionIds(datastoreType,
                     cmHandleId, xpath);
-            if (!existingSubscriptionIds.contains(newSubscriptionId)) {
-                updateListOfSubscribers(existingSubscriptionIds, newSubscriptionId, existingFilterNode);
-            }
+            subscriptionIds.add(subscriptionId);
+            saveSubscriptionDetails(subscriptionAsDataNode, subscriptionIds);
+        } else {
+            addNewSubscriptionViaDatastore(datastoreType, cmHandleId, xpath, subscriptionId);
+        }
+    }
+
+    @Override
+    public void removeCmNotificationSubscription(final DatastoreType datastoreType, final String cmHandleId,
+                                                 final String xpath, final String subscriptionId) {
+        final DataNode subscriptionAsDataNode = getSubscriptionAsDataNode(datastoreType, cmHandleId, xpath);
+        final Collection<String> subscriptionIds = getOngoingCmNotificationSubscriptionIds(datastoreType,
+                cmHandleId, xpath);
+        subscriptionIds.remove(subscriptionId);
+        saveSubscriptionDetails(subscriptionAsDataNode, subscriptionIds);
+        if (isOngoingCmNotificationSubscription(datastoreType, cmHandleId, xpath)) {
+            log.info("There are subscribers left for the following cps path {} :",
+                    CM_SUBSCRIPTION_CPS_PATH_QUERY.formatted(datastoreType.getDatastoreName(), cmHandleId,
+                            escapeQuotesByDoublingThem(xpath)));
         } else {
-            addNewSubscriptionViaDatastore(datastoreType, cmHandleId, xpath, newSubscriptionId);
+            log.info("No subscribers left for the following cps path {} :",
+                    CM_SUBSCRIPTION_CPS_PATH_QUERY.formatted(datastoreType.getDatastoreName(), cmHandleId,
+                            escapeQuotesByDoublingThem(xpath)));
+            deleteListOfSubscriptionsFor(datastoreType, cmHandleId, xpath);
         }
     }
 
+    private void deleteListOfSubscriptionsFor(final DatastoreType datastoreType, final String cmHandleId,
+                                              final String xpath) {
+        cpsDataService.deleteDataNode(NCMP_DATASPACE_NAME, SUBSCRIPTION_ANCHOR_NAME,
+                CM_SUBSCRIPTION_CPS_PATH_QUERY.formatted(datastoreType.getDatastoreName(), cmHandleId,
+                        escapeQuotesByDoublingThem(xpath)),
+                OffsetDateTime.now());
+    }
+
+    private DataNode getSubscriptionAsDataNode(final DatastoreType datastoreType, final String cmHandleId,
+                                               final String xpath) {
+        return cpsQueryService.queryDataNodes(NCMP_DATASPACE_NAME, SUBSCRIPTION_ANCHOR_NAME,
+                CM_SUBSCRIPTION_CPS_PATH_QUERY.formatted(datastoreType.getDatastoreName(), cmHandleId,
+                        escapeQuotesByDoublingThem(xpath)),
+                OMIT_DESCENDANTS).iterator().next();
+    }
+
     private void addNewSubscriptionViaDatastore(final DatastoreType datastoreType, final String cmHandleId,
                                                 final String xpath, final String newSubscriptionId) {
         final String parentXpath = "/datastores/datastore[@name='%s']/cm-handles"
                 .formatted(datastoreType.getDatastoreName());
-        final String updatedJson = String.format("{\"cm-handle\":[{\"id\":\"%s\",\"filters\":{\"filter\":"
+        final String subscriptionAsJson = 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,
+        cpsDataService.saveData(NCMP_DATASPACE_NAME, SUBSCRIPTION_ANCHOR_NAME, parentXpath, subscriptionAsJson,
                 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 void saveSubscriptionDetails(final DataNode subscriptionDetailsAsDataNode,
+                                         final  Collection<String> subscriptionIds) {
+        final Map<String, Serializable> subscriptionDetailsAsMap = new HashMap<>();
+        subscriptionDetailsAsMap.put("xpath", subscriptionDetailsAsDataNode.getLeaves().get("xpath"));
+        subscriptionDetailsAsMap.put("subscriptionIds", (Serializable) subscriptionIds);
+        final String parentXpath = CpsPathUtil.getNormalizedParentXpath(subscriptionDetailsAsDataNode.getXpath());
+        final String subscriptionDetailsAsJson = "{\"filter\":["
+                + jsonObjectMapper.asJsonString(subscriptionDetailsAsMap).replace("'", "\"") + "]}";
+        cpsDataService.updateNodeLeaves(NCMP_DATASPACE_NAME, SUBSCRIPTION_ANCHOR_NAME, parentXpath,
+                subscriptionDetailsAsJson, OffsetDateTime.now());
     }
 
     private static String escapeQuotesByDoublingThem(final String inputXpath) {
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/exception/NoAlternateIdParentFoundException.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/exception/NoAlternateIdParentFoundException.java
new file mode 100644 (file)
index 0000000..2e6cd33
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2024 Nordix Foundation
+ *  ================================================================================
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ *  SPDX-License-Identifier: Apache-2.0
+ *  ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.ncmp.api.impl.exception;
+
+import java.io.Serial;
+
+public class NoAlternateIdParentFoundException extends NcmpException {
+
+    @Serial
+    private static final long serialVersionUID = -2412915490233422945L;
+    private static final String ALTERNATE_ID_NOT_FOUND = "No matching (parent) cm handle found using alternate ids";
+
+    /**
+     * Constructor.
+     *
+     * @param cpsPath datanode cpsPath
+     */
+    public NoAlternateIdParentFoundException(final String cpsPath) {
+        super(ALTERNATE_ID_NOT_FOUND, String.format("cannot find a datanode with alternate id %s", cpsPath));
+    }
+}
index e230b3f..184b125 100644 (file)
@@ -129,6 +129,16 @@ public interface InventoryPersistence extends NcmpPersistence {
      */
     DataNode getCmHandleDataNodeByAlternateId(String alternateId);
 
+    /**
+     * Get data node that matches longest alternate id by removing elements (as defined by the separator string)
+     * from right to left.
+     *
+     * @param alternateId alternate ID
+     * @param separator   a string that separates each element from the next.
+     * @return data node
+     */
+    DataNode getCmHandleDataNodeByLongestMatchAlternateId(final String alternateId, final String separator);
+
     /**
      * Get collection of data nodes of given cm handles.
      *
index 3ae3dd9..bf54fe5 100644 (file)
@@ -33,9 +33,11 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
 import org.onap.cps.api.CpsAnchorService;
 import org.onap.cps.api.CpsDataService;
 import org.onap.cps.api.CpsModuleService;
+import org.onap.cps.ncmp.api.impl.exception.NoAlternateIdParentFoundException;
 import org.onap.cps.ncmp.api.impl.utils.YangDataConverter;
 import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle;
 import org.onap.cps.spi.FetchDescendantsOption;
@@ -169,16 +171,29 @@ public class InventoryPersistenceImpl extends NcmpPersistenceImpl implements Inv
 
     @Override
     public DataNode getCmHandleDataNodeByAlternateId(final String alternateId) {
-        final String xPathForCmHandleByAlternateId = getXPathForCmHandleByAlternateId(alternateId);
+        final String cpsPathForCmHandleByAlternateId = getCpsPathForCmHandleByAlternateId(alternateId);
         final Collection<DataNode> dataNodes = cmHandleQueries
-            .queryNcmpRegistryByCpsPath(xPathForCmHandleByAlternateId, OMIT_DESCENDANTS);
+            .queryNcmpRegistryByCpsPath(cpsPathForCmHandleByAlternateId, OMIT_DESCENDANTS);
         if (dataNodes.isEmpty()) {
             throw new DataNodeNotFoundException(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,
-                xPathForCmHandleByAlternateId);
+                cpsPathForCmHandleByAlternateId);
         }
         return dataNodes.iterator().next();
     }
 
+    @Override
+    public DataNode getCmHandleDataNodeByLongestMatchAlternateId(final String alternateId, final String separator) {
+        String bestMatch = alternateId;
+        while (StringUtils.isNotEmpty(bestMatch)) {
+            try {
+                return getCmHandleDataNodeByAlternateId(bestMatch);
+            } catch (final DataNodeNotFoundException ignored) {
+                bestMatch = getParentPath(bestMatch, separator);
+            }
+        }
+        throw new NoAlternateIdParentFoundException(alternateId);
+    }
+
     @Override
     public Collection<DataNode> getCmHandleDataNodes(final Collection<String> cmHandleIds) {
         final Collection<String> xpaths = new ArrayList<>(cmHandleIds.size());
@@ -195,7 +210,7 @@ public class InventoryPersistenceImpl extends NcmpPersistenceImpl implements Inv
         return NCMP_DMI_REGISTRY_PARENT + "/cm-handles[@id='" + cmHandleId + "']";
     }
 
-    private static String getXPathForCmHandleByAlternateId(final String alternateId) {
+    private static String getCpsPathForCmHandleByAlternateId(final String alternateId) {
         return NCMP_DMI_REGISTRY_PARENT + "/cm-handles[@alternate-id='" + alternateId + "']";
     }
 
@@ -206,4 +221,9 @@ public class InventoryPersistenceImpl extends NcmpPersistenceImpl implements Inv
     private String createCmHandlesJsonData(final List<YangModelCmHandle> yangModelCmHandles) {
         return "{\"cm-handles\":" + jsonObjectMapper.asJsonString(yangModelCmHandles) + "}";
     }
+
+    private static String getParentPath(final String path, final String separator) {
+        final int lastSeparatorIndex = path.lastIndexOf(separator);
+        return lastSeparatorIndex < 0 ? "" : path.substring(0, lastSeparatorIndex);
+    }
 }
index d855442..04acaa5 100644 (file)
@@ -97,7 +97,7 @@ public class DmiServiceUrlBuilder {
     /**
      * This method populates uri variables.
      *
-     * @param dataStoreName data store name 
+     * @param dataStoreName data store name
      * @param dmiServiceName dmi service name
      * @param cmHandleId        cm handle id for dmi registration
      * @return {@code String} dmi service url as string
index 61da706..42bad89 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2023 Nordix Foundation
+ *  Copyright (C) 2023-2024 Nordix Foundation
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -29,7 +29,7 @@ import lombok.AccessLevel;
 import lombok.NoArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.onap.cps.ncmp.api.NcmpResponseStatus;
-import org.onap.cps.ncmp.api.impl.events.NcmpCloudEventBuilder;
+import org.onap.cps.ncmp.api.impl.events.NcmpEvent;
 import org.onap.cps.ncmp.api.impl.operations.DmiDataOperation;
 import org.onap.cps.ncmp.events.async1_0_0.Data;
 import org.onap.cps.ncmp.events.async1_0_0.DataOperationEvent;
@@ -57,8 +57,8 @@ public class DataOperationEventCreator {
         final Data data = createPayloadFromDataOperationResponses(cmHandleIdsPerResponseCodesPerOperation);
         dataOperationEvent.setData(data);
         final Map<String, String> extensions = createDataOperationExtensions(requestId, clientTopic);
-        return NcmpCloudEventBuilder.builder().type(DataOperationEvent.class.getName())
-                .event(dataOperationEvent).extensions(extensions).setCloudEvent().build();
+        return NcmpEvent.builder().type(DataOperationEvent.class.getName())
+                .data(dataOperationEvent).extensions(extensions).build().asCloudEvent();
     }
 
     private static Data createPayloadFromDataOperationResponses(final MultiValueMap<DmiDataOperation,
index a8b4e28..4b016b3 100644 (file)
@@ -31,6 +31,7 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.TimeoutException;
 import java.util.stream.Collectors;
 import lombok.AccessLevel;
 import lombok.NoArgsConstructor;
@@ -109,28 +110,80 @@ public class ResourceDataOperationRequestUtils {
                     DmiDataOperation.buildDmiDataOperationRequestBodyWithoutCmHandles(dataOperationDefinitionIn),
                     CM_HANDLES_NOT_READY, nonReadyCmHandleIds);
         }
-        if (!cmHandleIdsPerResponseCodesPerOperation.isEmpty()) {
-            publishErrorMessageToClientTopic(topicParamInQuery, requestId, cmHandleIdsPerResponseCodesPerOperation);
-        }
+        publishErrorMessageToClientTopic(topicParamInQuery, requestId, cmHandleIdsPerResponseCodesPerOperation);
         return dmiDataOperationsOutPerDmiServiceName;
     }
 
+    /**
+     * Handles the async task completion for an entire data, publishing errors to client topic on task failure.
+     *
+     * @param topicParamInQuery      client given topic
+     * @param requestId              unique identifier per request
+     * @param dataOperationRequest   incoming data operation request details
+     * @param throwable              error cause, or null if task completed with no exception
+     */
+    public static void handleAsyncTaskCompletionForDataOperationsRequest(
+            final String topicParamInQuery,
+            final String requestId,
+            final DataOperationRequest dataOperationRequest,
+            final Throwable throwable) {
+        if (throwable == null) {
+            log.info("Data operations request {} completed.", requestId);
+        } else if (throwable instanceof TimeoutException) {
+            log.error("Data operations request {} timed out.", requestId);
+            ResourceDataOperationRequestUtils.publishErrorMessageToClientTopicForEntireOperation(topicParamInQuery,
+                    requestId, dataOperationRequest, NcmpResponseStatus.DMI_SERVICE_NOT_RESPONDING);
+        } else {
+            log.error("Data operations request {} failed.", requestId, throwable);
+            ResourceDataOperationRequestUtils.publishErrorMessageToClientTopicForEntireOperation(topicParamInQuery,
+                    requestId, dataOperationRequest, NcmpResponseStatus.UNKNOWN_ERROR);
+        }
+    }
+
+    /**
+     * Creates data operation cloud event for when the entire data operation fails and publishes it to client topic.
+     *
+     * @param topicParamInQuery      client given topic
+     * @param requestId              unique identifier per request
+     * @param dataOperationRequestIn incoming data operation request details
+     * @param ncmpResponseStatus     response code to be sent for all cm handle ids in all operations
+     */
+    private static void publishErrorMessageToClientTopicForEntireOperation(
+            final String topicParamInQuery,
+            final String requestId,
+            final DataOperationRequest dataOperationRequestIn,
+            final NcmpResponseStatus ncmpResponseStatus) {
+
+        final MultiValueMap<DmiDataOperation, Map<NcmpResponseStatus, List<String>>>
+                cmHandleIdsPerResponseCodesPerOperation = new LinkedMultiValueMap<>();
+
+        for (final DataOperationDefinition dataOperationDefinitionIn :
+                dataOperationRequestIn.getDataOperationDefinitions()) {
+            cmHandleIdsPerResponseCodesPerOperation.add(
+                    DmiDataOperation.buildDmiDataOperationRequestBodyWithoutCmHandles(dataOperationDefinitionIn),
+                    Map.of(ncmpResponseStatus, dataOperationDefinitionIn.getCmHandleIds()));
+        }
+        publishErrorMessageToClientTopic(topicParamInQuery, requestId, cmHandleIdsPerResponseCodesPerOperation);
+    }
+
     /**
      * Creates data operation cloud event and publish it to client topic.
      *
      * @param clientTopic                              client given topic
      * @param requestId                                unique identifier per request
-     * @param cmHandleIdsPerResponseCodesPerOperation list of cm handle ids per operation with response code
+     * @param cmHandleIdsPerResponseCodesPerOperation  list of cm handle ids per operation with response code
      */
     public static void publishErrorMessageToClientTopic(final String clientTopic,
                                                          final String requestId,
                                                          final MultiValueMap<DmiDataOperation,
                                                                  Map<NcmpResponseStatus, List<String>>>
                                                                     cmHandleIdsPerResponseCodesPerOperation) {
-        final CloudEvent dataOperationCloudEvent = DataOperationEventCreator.createDataOperationEvent(clientTopic,
-                requestId, cmHandleIdsPerResponseCodesPerOperation);
-        final EventsPublisher<CloudEvent> eventsPublisher = CpsApplicationContext.getCpsBean(EventsPublisher.class);
-        eventsPublisher.publishCloudEvent(clientTopic, requestId, dataOperationCloudEvent);
+        if (!cmHandleIdsPerResponseCodesPerOperation.isEmpty()) {
+            final CloudEvent dataOperationCloudEvent = DataOperationEventCreator.createDataOperationEvent(clientTopic,
+                    requestId, cmHandleIdsPerResponseCodesPerOperation);
+            final EventsPublisher<CloudEvent> eventsPublisher = CpsApplicationContext.getCpsBean(EventsPublisher.class);
+            eventsPublisher.publishCloudEvent(clientTopic, requestId, dataOperationCloudEvent);
+        }
     }
 
     private static Map<String, String> getDmiServiceNamesPerCmHandleId(
index a4df9b3..74e3424 100644 (file)
@@ -36,10 +36,10 @@ class NcmpConfigurationSpec extends Specification{
 
     @Autowired
     NcmpConfiguration.DmiProperties dmiProperties
-    
+
     @Autowired
     HttpClientConfiguration httpClientConfiguration
-    
+
     def mockRestTemplateBuilder = new RestTemplateBuilder()
 
     def 'NcmpConfiguration Construction.'() {
index cd9b8dd..cfb28a0 100644 (file)
@@ -23,6 +23,7 @@ package org.onap.cps.ncmp.api.impl.events.cmsubscription
 import com.fasterxml.jackson.databind.ObjectMapper
 import io.cloudevents.CloudEvent
 import org.onap.cps.events.EventsPublisher
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.producer.CmNotificationSubscriptionDmiInEventProducer
 import org.onap.cps.ncmp.api.impl.events.mapper.CloudEventMapper
 import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.ncmp_to_dmi.CmNotificationSubscriptionDmiInEvent
 import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.ncmp_to_dmi.Cmhandle
index 523ec76..c761f4d 100644 (file)
@@ -28,6 +28,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
 import io.cloudevents.CloudEvent
 import io.cloudevents.core.builder.CloudEventBuilder
 import org.apache.kafka.clients.consumer.ConsumerRecord
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.consumer.CmNotificationSubscriptionDmiOutEventConsumer
 import org.onap.cps.ncmp.api.impl.events.cmsubscription.model.CmNotificationSubscriptionStatus
 import org.onap.cps.ncmp.api.kafka.MessagingBaseSpec
 import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.dmi_to_ncmp.CmNotificationSubscriptionDmiOutEvent
diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/CmNotificationSubscriptionEventsHandlerSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/CmNotificationSubscriptionEventsHandlerSpec.groovy
new file mode 100644 (file)
index 0000000..788a7a7
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (c) 2024 Nordix Foundation.
+ *  ================================================================================
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an 'AS IS' BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ *  SPDX-License-Identifier: Apache-2.0
+ *  ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.ncmp.api.impl.events.cmsubscription
+
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.producer.CmNotificationSubscriptionNcmpOutEventProducer
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.producer.CmNotificationSubscriptionDmiInEventProducer
+import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.ncmp_to_dmi.CmNotificationSubscriptionDmiInEvent
+import org.onap.cps.ncmp.events.cmsubscription_merge1_0_0.ncmp_to_client.CmNotificationSubscriptionNcmpOutEvent
+import spock.lang.Specification
+
+class CmNotificationSubscriptionEventsHandlerSpec extends Specification {
+
+    def mockCmNotificationSubscriptionNcmpOutEventProducer = Mock(CmNotificationSubscriptionNcmpOutEventProducer)
+    def mockCmNotificationSubscriptionDmiInEventProducer = Mock(CmNotificationSubscriptionDmiInEventProducer)
+
+    def objectUnderTest = new CmNotificationSubscriptionEventsHandler(mockCmNotificationSubscriptionNcmpOutEventProducer,
+        mockCmNotificationSubscriptionDmiInEventProducer)
+
+    def 'Publish cm notification subscription ncmp out event'() {
+        given: 'an ncmp out event'
+            def cmNotificationSubscriptionNcmpOutEvent = new CmNotificationSubscriptionNcmpOutEvent()
+        when: 'the method to publish cm notification subscription ncmp out event is called'
+            objectUnderTest.publishCmNotificationSubscriptionNcmpOutEvent("some-id",
+                "some-event", cmNotificationSubscriptionNcmpOutEvent, true)
+        then: 'the parameters is delegated to the correct method once'
+            1 * mockCmNotificationSubscriptionNcmpOutEventProducer.publishCmNotificationSubscriptionNcmpOutEvent(
+                "some-id", "some-event", cmNotificationSubscriptionNcmpOutEvent, true)
+    }
+
+    def 'Publish cm notification subscription dmi in event'() {
+        given: 'a dmi in event'
+            def cmNotificationSubscriptionDmiInEvent = new CmNotificationSubscriptionDmiInEvent()
+        when: 'the method to publish cm notification subscription ncmp out event is called'
+            objectUnderTest.publishCmNotificationSubscriptionDmiInEvent("some-id",
+                "some-dmi", "some-event", cmNotificationSubscriptionDmiInEvent)
+        then: 'the parameters is delegated to the correct method once'
+            1 * mockCmNotificationSubscriptionDmiInEventProducer.publishCmNotificationSubscriptionDmiInEvent("some-id",
+                "some-dmi", "some-event", cmNotificationSubscriptionDmiInEvent)
+    }
+}
\ No newline at end of file
diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/CmNotificationSubscriptionMappersHandlerSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/cmsubscription/CmNotificationSubscriptionMappersHandlerSpec.groovy
new file mode 100644 (file)
index 0000000..bdc54bd
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2024 Nordix Foundation
+ *  ================================================================================
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ *  SPDX-License-Identifier: Apache-2.0
+ *  ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.ncmp.api.impl.events.cmsubscription
+
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.mapper.CmNotificationSubscriptionDmiInEventMapper
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.mapper.CmNotificationSubscriptionNcmpOutEventMapper
+import spock.lang.Specification
+
+class CmNotificationSubscriptionMappersHandlerSpec extends Specification{
+
+    def mockCmNotificationDmiInEventMapper = Mock(CmNotificationSubscriptionDmiInEventMapper)
+    def mockCmNotificationNcmpOutEventMapper = Mock(CmNotificationSubscriptionNcmpOutEventMapper)
+
+    def objectUnderTest = new CmNotificationSubscriptionMappersHandler(mockCmNotificationDmiInEventMapper,
+        mockCmNotificationNcmpOutEventMapper)
+
+    def 'Get cm notification subscription DMI in event'() {
+        given: 'a list of predicates'
+            def testListOfPredicates = []
+        when: 'method to create a cm notification subscription dmi in event is called with predicates'
+            objectUnderTest.toCmNotificationSubscriptionDmiInEvent(testListOfPredicates)
+        then: 'the parameters is delegated to the correct dmi in event mapper method'
+            1 * mockCmNotificationDmiInEventMapper.toCmNotificationSubscriptionDmiInEvent(testListOfPredicates)
+    }
+
+    def 'Get cm notification subscription ncmp out event'() {
+        given: 'a subscription details map'
+            def testSubscriptionDetailsMap = [:]
+        when: 'method to create cm notification subscription ncmp out event is called with the following parameters'
+            objectUnderTest.toCmNotificationSubscriptionNcmpOutEvent("test-id", testSubscriptionDetailsMap)
+        then: 'the parameters is delegated to the correct ncmp out event mapper method'
+            1 * mockCmNotificationNcmpOutEventMapper.toCmNotificationSubscriptionNcmpOutEvent("test-id",
+            testSubscriptionDetailsMap)
+    }
+
+    def 'Get cm notification subscription ncmp out event for a rejected request'() {
+        given: 'a list of target filters'
+            def testRejectedTargetFilters = []
+        when: 'method to create cm notification subscription ncmp out event is called with the following parameters'
+            objectUnderTest.toCmNotificationSubscriptionNcmpOutEventForRejectedRequest(
+                "test-id", testRejectedTargetFilters)
+        then: 'the parameters is delegated to the correct ncmp out event mapper method'
+            1 * mockCmNotificationNcmpOutEventMapper.toCmNotificationSubscriptionNcmpOutEventForRejectedRequest(
+                "test-id", testRejectedTargetFilters)
+    }
+}
index 8210cf3..9c84c51 100644 (file)
@@ -28,6 +28,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
 import io.cloudevents.CloudEvent
 import io.cloudevents.core.builder.CloudEventBuilder
 import org.apache.kafka.clients.consumer.ConsumerRecord
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.consumer.CmNotificationSubscriptionNcmpInEventConsumer
 import org.onap.cps.ncmp.api.impl.events.cmsubscription.service.CmNotificationSubscriptionHandlerService
 import org.onap.cps.ncmp.api.kafka.MessagingBaseSpec
 import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.client_to_ncmp.CmNotificationSubscriptionNcmpInEvent
@@ -79,7 +80,7 @@ class CmNotificationSubscriptionNcmpInEventConsumerSpec extends MessagingBaseSpe
             def loggingEvent = getLoggingEvent()
             assert loggingEvent.level == Level.INFO
         and: 'the log indicates the task completed successfully'
-            assert loggingEvent.formattedMessage == 'Subscription for source some-resource with subscription id cm-subscription-001 ...'
+            assert loggingEvent.formattedMessage == 'Subscription for source some-resource with subscription id test-id ...'
         and: 'the subscription handler service is called once'
             1 * mockCmNotificationSubscriptionHandlerService.processSubscriptionCreateRequest(_)
     }
index 970d7e6..77bbe7e 100644 (file)
@@ -3,8 +3,7 @@ package org.onap.cps.ncmp.api.impl.events.cmsubscription
 import com.fasterxml.jackson.databind.ObjectMapper
 import io.cloudevents.CloudEvent
 import org.onap.cps.events.EventsPublisher
-import org.onap.cps.ncmp.api.impl.events.cmsubscription.mapper.CmNotificationSubscriptionNcmpOutEventMapper
-import org.onap.cps.ncmp.api.impl.events.cmsubscription.model.DmiCmNotificationSubscriptionDetails
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.producer.CmNotificationSubscriptionNcmpOutEventProducer
 import org.onap.cps.ncmp.api.impl.events.mapper.CloudEventMapper
 import org.onap.cps.ncmp.events.cmsubscription_merge1_0_0.ncmp_to_client.CmNotificationSubscriptionNcmpOutEvent
 import org.onap.cps.ncmp.events.cmsubscription_merge1_0_0.ncmp_to_client.Data
@@ -15,10 +14,11 @@ class CmNotificationSubscriptionNcmpOutEventProducerSpec extends Specification {
 
     def mockEventsPublisher = Mock(EventsPublisher)
     def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
-    def mockCmNotificationSubscriptionCache = Mock(Map<String, Map<String, DmiCmNotificationSubscriptionDetails>>)
-    def mockCmNotificationSubscriptionNcmpOutEventMapper = Mock(CmNotificationSubscriptionNcmpOutEventMapper)
+    def mockCmNotificationSubscriptionMappersHandler = Mock(CmNotificationSubscriptionMappersHandler)
+    def mockDmiCmNotificationSubscriptionCacheHandler = Mock(DmiCmNotificationSubscriptionCacheHandler)
 
-    def objectUnderTest = new CmNotificationSubscriptionNcmpOutEventProducer(mockEventsPublisher, jsonObjectMapper, mockCmNotificationSubscriptionCache, mockCmNotificationSubscriptionNcmpOutEventMapper)
+    def objectUnderTest = new CmNotificationSubscriptionNcmpOutEventProducer(mockEventsPublisher, jsonObjectMapper,
+        mockCmNotificationSubscriptionMappersHandler, mockDmiCmNotificationSubscriptionCacheHandler)
 
     def 'Create and #scenario Cm Notification Subscription NCMP out event'() {
         given: 'a cm subscription response for the client'
@@ -80,6 +80,8 @@ class CmNotificationSubscriptionNcmpOutEventProducerSpec extends Specification {
                         assert CloudEventMapper.toTargetEvent(cmNotificationSubscriptionNcmpOutEventAsCloudEvent, CmNotificationSubscriptionNcmpOutEvent) == cmNotificationSubscriptionNcmpOutEvent
                     }
             }
+        then: 'the cache handler is called once to remove accepted and rejected entries in cache'
+            1 * mockDmiCmNotificationSubscriptionCacheHandler.removeAcceptedAndRejectedDmiCmNotificationSubscriptionEntries(subscriptionId)
     }
 
 
index 47a1c89..43568be 100644 (file)
@@ -25,6 +25,7 @@ import io.cloudevents.CloudEvent
 import io.cloudevents.core.builder.CloudEventBuilder
 import org.apache.kafka.clients.consumer.ConsumerRecord
 import org.onap.cps.ncmp.api.impl.events.cmsubscription.model.CmNotificationSubscriptionStatus
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.model.DmiCmNotificationSubscriptionDetails
 import org.onap.cps.ncmp.api.impl.events.cmsubscription.service.CmNotificationSubscriptionPersistenceService
 import org.onap.cps.ncmp.api.impl.inventory.InventoryPersistence
 import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle
@@ -75,6 +76,37 @@ class DmiCmNotificationSubscriptionCacheHandlerSpec extends MessagingBaseSpec {
             assert testCache.containsKey(subscriptionId)
     }
 
+    def 'Get cache entry via subscription id'() {
+        given: 'the cache contains value for some-id'
+            testCache.put('some-id',[:])
+        when: 'the get method is called'
+            def result = objectUnderTest.get('some-id')
+        then: 'correct value is returned as expected'
+            assert result == [:]
+    }
+
+    def 'Remove accepted and rejected entries from cache via subscription id'() {
+        given: 'a map as the value for cache entry for some-id'
+            def testMap = [:]
+            testMap.put("dmi-1",
+                new DmiCmNotificationSubscriptionDetails([],CmNotificationSubscriptionStatus.ACCEPTED))
+            testMap.put("dmi-2",
+                new DmiCmNotificationSubscriptionDetails([],CmNotificationSubscriptionStatus.REJECTED))
+            testMap.put("dmi-3",
+                new DmiCmNotificationSubscriptionDetails([],CmNotificationSubscriptionStatus.PENDING))
+            testCache.put("test-id", testMap)
+            assert testCache.get("test-id").size() == 3
+        when: 'the method to remove accepted and rejected entries for test-id is called'
+            objectUnderTest.removeAcceptedAndRejectedDmiCmNotificationSubscriptionEntries("test-id")
+        then: 'all entries with status accepted/rejected are no longer present for test-id'
+            testCache.get("test-id").each { key, testResultMap ->
+                assert testResultMap.cmNotificationSubscriptionStatus != CmNotificationSubscriptionStatus.ACCEPTED
+                    || testResultMap.cmNotificationSubscriptionStatus != CmNotificationSubscriptionStatus.REJECTED
+            }
+        and: 'the size of the map for cache entry test-id is as expected'
+            assert testCache.get("test-id").size() == 1
+    }
+
     def 'Create map for DMI cm notification subscription per DMI service name'() {
         given: 'list of predicates from the create subscription event'
             def predicates = cmNotificationSubscriptionNcmpInEvent.getData().getPredicates()
@@ -139,7 +171,7 @@ class DmiCmNotificationSubscriptionCacheHandlerSpec extends MessagingBaseSpec {
         when: 'subscription is persisted in database'
             objectUnderTest.persistIntoDatabasePerDmi(subscriptionId,'dmi-1')
         then: 'persistence service is called the correct number of times per dmi'
-            4 * mockCmNotificationSubscriptionPersistenceService.addOrUpdateCmNotificationSubscription(_,_,_,subscriptionId)
+            4 * mockCmNotificationSubscriptionPersistenceService.addCmNotificationSubscription(_,_,_,subscriptionId)
     }
 
     def setUpTestEvent(){
index 1020f55..7d1a1d6 100644 (file)
 package org.onap.cps.ncmp.api.impl.events.cmsubscription.service
 
 import com.fasterxml.jackson.databind.ObjectMapper
-import org.onap.cps.ncmp.api.impl.events.cmsubscription.CmNotificationSubscriptionNcmpOutEventProducer
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.CmNotificationSubscriptionDelta
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.CmNotificationSubscriptionEventsHandler
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.CmNotificationSubscriptionMappersHandler
 import org.onap.cps.ncmp.api.impl.events.cmsubscription.DmiCmNotificationSubscriptionCacheHandler
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.mapper.CmNotificationSubscriptionDmiInEventMapper
 import org.onap.cps.ncmp.api.impl.events.cmsubscription.mapper.CmNotificationSubscriptionNcmpOutEventMapper
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.model.CmNotificationSubscriptionStatus
+import org.onap.cps.ncmp.api.impl.events.cmsubscription.model.DmiCmNotificationSubscriptionDetails
 import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.client_to_ncmp.CmNotificationSubscriptionNcmpInEvent
+import org.onap.cps.ncmp.events.cmnotificationsubscription_merge1_0_0.ncmp_to_dmi.CmNotificationSubscriptionDmiInEvent
+import org.onap.cps.ncmp.events.cmsubscription_merge1_0_0.ncmp_to_client.CmNotificationSubscriptionNcmpOutEvent
 import org.onap.cps.ncmp.utils.TestUtils
 import org.onap.cps.utils.JsonObjectMapper
 import spock.lang.Specification
@@ -33,31 +40,55 @@ class CmNotificationSubscriptionHandlerServiceImplSpec extends Specification{
 
     def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
     def mockCmNotificationSubscriptionPersistenceService = Mock(CmNotificationSubscriptionPersistenceService);
-    def mockCmNotificationSubscriptionNcmpOutEventMapper = Mock(CmNotificationSubscriptionNcmpOutEventMapper);
-    def mockCmNotificationSubscriptionNcmpOutEventProducer = Mock(CmNotificationSubscriptionNcmpOutEventProducer);
+    def mockCmNotificationSubscriptionDelta = Mock(CmNotificationSubscriptionDelta);
+    def mockCmNotificationSubscriptionMappersHandler = Mock(CmNotificationSubscriptionMappersHandler);
+    def mockCmNotificationSubscriptionEventsHandler = Mock(CmNotificationSubscriptionEventsHandler);
     def mockDmiCmNotificationSubscriptionCacheHandler = Mock(DmiCmNotificationSubscriptionCacheHandler);
 
-    def objectUnderTest = new CmNotificationSubscriptionHandlerServiceImpl(mockCmNotificationSubscriptionPersistenceService, mockCmNotificationSubscriptionNcmpOutEventMapper, mockCmNotificationSubscriptionNcmpOutEventProducer, mockDmiCmNotificationSubscriptionCacheHandler)
+    def objectUnderTest = new CmNotificationSubscriptionHandlerServiceImpl(mockCmNotificationSubscriptionPersistenceService,
+        mockCmNotificationSubscriptionDelta, mockCmNotificationSubscriptionMappersHandler,
+        mockCmNotificationSubscriptionEventsHandler, mockDmiCmNotificationSubscriptionCacheHandler)
+
+    def testSubscriptionDetailsMap = ["dmi-1":new DmiCmNotificationSubscriptionDetails([], CmNotificationSubscriptionStatus.PENDING)]
+    def testListOfDeltaPredicates = []
 
     def 'Consume valid and unique CmNotificationSubscriptionNcmpInEvent create message'() {
-        given: 'a cmNotificationSubscriptionNcmp in event'
+        given: 'a cmNotificationSubscriptionNcmp in event with unique subscription id'
             def jsonData = TestUtils.getResourceFileContent('cmSubscription/cmNotificationSubscriptionNcmpInEvent.json')
             def testEventConsumed = jsonObjectMapper.convertJsonString(jsonData, CmNotificationSubscriptionNcmpInEvent.class)
-            mockCmNotificationSubscriptionPersistenceService.isUniqueSubscriptionId('cm-subscription-001') >> true
+            mockCmNotificationSubscriptionPersistenceService.isUniqueSubscriptionId("test-id") >> true
+        and: 'the cache handler returns for relevant subscription id'
+            1 * mockDmiCmNotificationSubscriptionCacheHandler.get("test-id") >> testSubscriptionDetailsMap
+        and: 'the delta predicates is returned'
+            1 * mockCmNotificationSubscriptionDelta.getDelta(_) >> testListOfDeltaPredicates
+        and: 'the DMI in event mapper returns cm notification subscription event'
+            def testDmiInEvent = new CmNotificationSubscriptionDmiInEvent()
+            1 *  mockCmNotificationSubscriptionMappersHandler
+                .toCmNotificationSubscriptionDmiInEvent(testListOfDeltaPredicates) >> testDmiInEvent
         when: 'the valid and unique event is consumed'
             objectUnderTest.processSubscriptionCreateRequest(testEventConsumed)
         then: 'the subscription cache handler is called once'
-            1 * mockDmiCmNotificationSubscriptionCacheHandler.add('cm-subscription-001',_)
+            1 * mockDmiCmNotificationSubscriptionCacheHandler.add('test-id',_)
+        and: 'the events handler method to publish DMI event is called correct number of times with the correct parameters'
+            testSubscriptionDetailsMap.size() * mockCmNotificationSubscriptionEventsHandler.publishCmNotificationSubscriptionDmiInEvent(
+                "test-id", "dmi-1", "subscriptionCreateRequest", testDmiInEvent)
     }
 
     def 'Consume valid and but non-unique CmNotificationSubscription create message'() {
         given: 'a cmNotificationSubscriptionNcmp in event'
             def jsonData = TestUtils.getResourceFileContent('cmSubscription/cmNotificationSubscriptionNcmpInEvent.json')
             def testEventConsumed = jsonObjectMapper.convertJsonString(jsonData, CmNotificationSubscriptionNcmpInEvent.class)
-            mockCmNotificationSubscriptionPersistenceService.isUniqueSubscriptionId('cm-subscription-001') >> false
+            mockCmNotificationSubscriptionPersistenceService.isUniqueSubscriptionId('test-id') >> false
+        and: 'the NCMP out in event mapper returns an event for rejected request'
+            def testNcmpOutEvent = new CmNotificationSubscriptionNcmpOutEvent()
+            1 * mockCmNotificationSubscriptionMappersHandler.toCmNotificationSubscriptionNcmpOutEventForRejectedRequest(
+                "test-id",_) >> testNcmpOutEvent
         when: 'the valid but non-unique event is consumed'
             objectUnderTest.processSubscriptionCreateRequest(testEventConsumed)
-        then: 'the subscription out event publisher is called once'
-            1 * mockCmNotificationSubscriptionNcmpOutEventProducer.publishCmNotificationSubscriptionNcmpOutEvent('cm-subscription-001', 'subscriptionCreateResponse', _, false)
+        then: 'the events handler method to publish DMI event is never called'
+            0 * mockCmNotificationSubscriptionEventsHandler.publishCmNotificationSubscriptionDmiInEvent(_,_,_,_)
+        and: 'the events handler method to publish NCMP out event is called once'
+            1 * mockCmNotificationSubscriptionEventsHandler.publishCmNotificationSubscriptionNcmpOutEvent(
+                'test-id', 'subscriptionCreateResponse', testNcmpOutEvent, false)
     }
 }
index 19ebc3d..13a20a1 100644 (file)
@@ -71,12 +71,12 @@ class CmNotificationSubscriptionPersistenceServiceImplSpec extends Specification
 
     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');
+            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')
+            objectUnderTest.addCmNotificationSubscription(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',
@@ -95,7 +95,7 @@ class CmNotificationSubscriptionPersistenceServiceImplSpec extends Specification
                 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')
+            objectUnderTest.addCmNotificationSubscription(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',
@@ -107,4 +107,30 @@ class CmNotificationSubscriptionPersistenceServiceImplSpec extends Specification
             'passthrough_running'     | DatastoreType.PASSTHROUGH_RUNNING      || "ncmp-datastore:passthrough-running"
             'passthrough_operational' | DatastoreType.PASSTHROUGH_OPERATIONAL  || "ncmp-datastore:passthrough-operational"
     }
+
+    def 'Remove subscriber from a list of an ongoing cm notification subscription'() {
+        given: 'a subscription exists when queried'
+            def cpsPathQuery = objectUnderTest.CM_SUBSCRIPTION_CPS_PATH_QUERY.formatted('ncmp-datastore:passthrough-running', 'ch-1', '/x/y')
+            mockCpsQueryService.queryDataNodes('NCMP-Admin', 'cm-data-subscriptions',
+                cpsPathQuery, FetchDescendantsOption.OMIT_DESCENDANTS) >> [new DataNode(xpath: cpsPathQuery, leaves: ['xpath': '/x/y','subscriptionIds': ['sub-1', 'sub-2']])]
+        when: 'the subscriber is removed'
+            objectUnderTest.removeCmNotificationSubscription(DatastoreType.PASSTHROUGH_RUNNING, 'ch-1', '/x/y', 'sub-1')
+        then: 'the list of subscribers is updated'
+            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-2"]}]}', _)
+    }
+
+    def 'Removing ongoing subscription with no subscribers'(){
+        given: 'a subscription exists when queried but has no subscribers'
+            def cpsPathQuery = objectUnderTest.CM_SUBSCRIPTION_CPS_PATH_QUERY.formatted('ncmp-datastore:passthrough-running', 'ch-1', '/x/y')
+            mockCpsQueryService.queryDataNodes('NCMP-Admin', 'cm-data-subscriptions',
+                cpsPathQuery, FetchDescendantsOption.OMIT_DESCENDANTS) >> [new DataNode(xpath: cpsPathQuery, leaves: ['xpath': '/x/y','subscriptionIds': []])]
+        when: 'a an ongoing subscription is refreshed'
+            objectUnderTest.removeCmNotificationSubscription(DatastoreType.PASSTHROUGH_RUNNING, 'ch-1', '/x/y', 'sub-1')
+        then: 'the subscription with empty subscriber list is removed'
+            1 * mockCpsDataService.deleteDataNode('NCMP-Admin', 'cm-data-subscriptions',
+                '/datastores/datastore[@name=\'ncmp-datastore:passthrough-running\']/cm-handles/cm-handle[@id=\'ch-1\']/filters/filter[@xpath=\'/x/y\']',
+                _)
+    }
 }
\ No newline at end of file
index cbc985f..9907e9a 100644 (file)
 
 package org.onap.cps.ncmp.api.impl.inventory
 
+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.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME
+import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NO_TIMESTAMP
+import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
+import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
+
 import com.fasterxml.jackson.databind.ObjectMapper
 import org.onap.cps.api.CpsAnchorService
 import org.onap.cps.api.CpsDataService
@@ -29,6 +37,7 @@ import org.onap.cps.api.CpsModuleService
 import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle
 import org.onap.cps.spi.CascadeDeleteAllowed
 import org.onap.cps.spi.FetchDescendantsOption
+import org.onap.cps.ncmp.api.impl.exception.NoAlternateIdParentFoundException
 import org.onap.cps.spi.exceptions.DataNodeNotFoundException
 import org.onap.cps.spi.model.DataNode
 import org.onap.cps.spi.model.ModuleDefinition
@@ -37,19 +46,10 @@ import org.onap.cps.spi.utils.CpsValidator
 import org.onap.cps.utils.JsonObjectMapper
 import spock.lang.Shared
 import spock.lang.Specification
-
 import java.time.OffsetDateTime
 import java.time.ZoneOffset
 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.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME
-import static org.onap.cps.ncmp.api.impl.ncmppersistence.NcmpPersistence.NO_TIMESTAMP
-import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
-import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
-
 class InventoryPersistenceImplSpec extends Specification {
 
     def spiedJsonObjectMapper = Spy(new JsonObjectMapper(new ObjectMapper()))
@@ -303,6 +303,41 @@ class InventoryPersistenceImplSpec extends Specification {
             assert objectUnderTest.getCmHandleDataNodeByAlternateId('alternate id') == new DataNode()
     }
 
+    def 'Find cm handle parent data node using alternate ids'() {
+        given: 'cm handle in the registry with alternateId /a/b'
+            def matchingCpsPath = "/dmi-registry/cm-handles[@alternate-id='/a/b']"
+            mockCmHandleQueries.queryNcmpRegistryByCpsPath(matchingCpsPath, OMIT_DESCENDANTS) >> [new DataNode()]
+        and: 'no other cm handle'
+            mockCmHandleQueries.queryNcmpRegistryByCpsPath(*_) >> []
+        expect: 'querying for alternate id a matching result found'
+            assert objectUnderTest.getCmHandleDataNodeByLongestMatchAlternateId(alternateId, '/') != null
+        where: 'the following parameters are used'
+            scenario                              | alternateId
+            'exact match'                         | '/a/b'
+            'exact match with trailing separator' | '/a/b/'
+            'child match'                         | '/a/b/c'
+    }
+
+    def 'Find cm handle parent data node using alternate ids mismatches'() {
+        given: 'cm handle in the registry with alternateId'
+            def matchingCpsPath = "/dmi-registry/cm-handles[@alternate-id='${cpsPath}]"
+            mockCmHandleQueries.queryNcmpRegistryByCpsPath(matchingCpsPath, OMIT_DESCENDANTS) >> [new DataNode()]
+        and: 'no other cm handle'
+            mockCmHandleQueries.queryNcmpRegistryByCpsPath(*_) >> []
+        when: 'attempt to find alternateId'
+            objectUnderTest.getCmHandleDataNodeByLongestMatchAlternateId(alternateId, '/')
+        then: 'no alternate id found exception thrown'
+            def thrown = thrown(NoAlternateIdParentFoundException)
+        and: 'the exception has the relevant details from the error response'
+            assert thrown.message == 'No matching (parent) cm handle found using alternate ids'
+            assert thrown.details == 'cannot find a datanode with alternate id ' + alternateId
+        where: 'the following parameters are used'
+            scenario                              | alternateId | cpsPath
+            'no match for parent only'            | '/a'        | '/a/b'
+            'no match at all'                     | '/x/y/z'    | '/a/b'
+            'no match with trailing separator'    | '/c/d/'     | '/c/d'
+    }
+
     def 'Attempt to get non existing cm handle data node by alternate id'() {
         given: 'query service is invoked and returns empty collection of data nodes'
             mockCmHandleQueries.queryNcmpRegistryByCpsPath(*_) >> []
@@ -340,5 +375,4 @@ class InventoryPersistenceImplSpec extends Specification {
         then: 'the cps data service method to delete data nodes is invoked once with the same xPaths'
             1 * mockCpsDataService.deleteDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, ['xpath1', 'xpath2'], NO_TIMESTAMP);
     }
-
 }
index 5690b8f..8df27bb 100644 (file)
@@ -26,6 +26,7 @@ import io.cloudevents.kafka.CloudEventDeserializer
 import io.cloudevents.kafka.impl.KafkaHeaders
 import org.apache.kafka.clients.consumer.KafkaConsumer
 import org.onap.cps.events.EventsPublisher
+import org.onap.cps.ncmp.api.NcmpResponseStatus
 import org.onap.cps.ncmp.api.impl.utils.context.CpsApplicationContext
 import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle
 import org.onap.cps.ncmp.api.impl.inventory.CmHandleState
@@ -38,14 +39,15 @@ import org.onap.cps.utils.JsonObjectMapper
 import org.spockframework.spring.SpringBean
 import org.springframework.beans.factory.annotation.Autowired
 import org.springframework.test.context.ContextConfiguration
+
 import java.time.Duration
+import java.util.concurrent.TimeoutException
 
 import static org.onap.cps.ncmp.api.impl.events.mapper.CloudEventMapper.toTargetEvent
 
 @ContextConfiguration(classes = [EventsPublisher, CpsApplicationContext, ObjectMapper])
 class ResourceDataOperationRequestUtilsSpec extends MessagingBaseSpec {
 
-    def cloudEventKafkaConsumer = new KafkaConsumer<>(eventConsumerConfigProperties('test', CloudEventDeserializer))
     def static clientTopic = 'my-topic-name'
     def static dataOperationType = 'org.onap.cps.ncmp.events.async1_0_0.DataOperationEvent'
 
@@ -90,6 +92,7 @@ class ResourceDataOperationRequestUtilsSpec extends MessagingBaseSpec {
 
     def 'Process per data operation request with non-ready, non-existing cm handle and publish event to client specified topic'() {
         given: 'consumer subscribing to client topic'
+            def cloudEventKafkaConsumer = new KafkaConsumer<>(eventConsumerConfigProperties('test-1', CloudEventDeserializer))
             cloudEventKafkaConsumer.subscribe([clientTopic])
         and: 'data operation request having non-ready and non-existing cm handle ids'
             def dataOperationRequestJsonData = TestUtils.getResourceFileContent('dataOperationRequest.json')
@@ -97,7 +100,7 @@ class ResourceDataOperationRequestUtilsSpec extends MessagingBaseSpec {
         when: 'data operation request is processed'
             ResourceDataOperationRequestUtils.processPerDefinitionInDataOperationsRequest(clientTopic, 'request-id', dataOperationRequest, yangModelCmHandles)
         and: 'subscribed client specified topic is polled and first record is selected'
-            def consumerRecordOut = cloudEventKafkaConsumer.poll(Duration.ofMillis(1500))[0]
+            def consumerRecordOut = cloudEventKafkaConsumer.poll(Duration.ofMillis(1500)).last()
         then: 'verify cloud compliant headers'
             def consumerRecordOutHeaders = consumerRecordOut.headers()
             assert KafkaHeaders.getParsedKafkaHeader(consumerRecordOutHeaders, 'ce_id') != null
@@ -111,10 +114,34 @@ class ResourceDataOperationRequestUtilsSpec extends MessagingBaseSpec {
         and: 'data operation response event response size is 3'
             dataOperationResponseEvent.data.responses.size() == 3
         and: 'verify published data operation response as json string'
-        def dataOperationResponseEventJson = TestUtils.getResourceFileContent('dataOperationResponseEvent.json')
+            def dataOperationResponseEventJson = TestUtils.getResourceFileContent('dataOperationResponseEvent.json')
             jsonObjectMapper.asJsonString(dataOperationResponseEvent.data.responses) == dataOperationResponseEventJson
     }
 
+    def 'Publish error response for entire data operations request when async task fails'() {
+        given: 'consumer subscribing to client topic'
+            def cloudEventKafkaConsumer = new KafkaConsumer<>(eventConsumerConfigProperties(consumerGroupId, CloudEventDeserializer))
+            cloudEventKafkaConsumer.subscribe([clientTopic])
+        and: 'data operation request having non-ready and non-existing cm handle ids'
+            def dataOperationRequestJsonData = TestUtils.getResourceFileContent('dataOperationRequest.json')
+            def dataOperationRequest = jsonObjectMapper.convertJsonString(dataOperationRequestJsonData, DataOperationRequest.class)
+        when: 'an error occurs for the entire data operations request'
+            ResourceDataOperationRequestUtils.handleAsyncTaskCompletionForDataOperationsRequest(clientTopic, 'request-id', dataOperationRequest, exceptionThrown)
+        and: 'subscribed client specified topic is polled and first record is selected'
+            def consumerRecordOut = cloudEventKafkaConsumer.poll(Duration.ofMillis(1500)).last()
+            def dataOperationResponseEvent = toTargetEvent(consumerRecordOut.value(), DataOperationEvent.class)
+        then: 'data operation response event response size is 3'
+            dataOperationResponseEvent.data.responses.size() == 3
+        and: 'all 3 have the expected error code'
+            dataOperationResponseEvent.data.responses.each {
+                assert it.statusCode == errorReportedToClientTopic.code
+            }
+        where:
+            scenario             | exceptionThrown        | consumerGroupId || errorReportedToClientTopic
+            'task timed out'     | new TimeoutException() | 'test-2'        || NcmpResponseStatus.DMI_SERVICE_NOT_RESPONDING
+            'unspecified error'  | new RuntimeException() | 'test-3'        || NcmpResponseStatus.UNKNOWN_ERROR
+    }
+
     static def getYangModelCmHandles() {
         def dmiProperties = [new YangModelCmHandle.Property('prop', 'some DMI property')]
         def readyState = new CompositeStateBuilder().withCmHandleState(CmHandleState.READY).withLastUpdatedTimeNow().build()
index a3283ff..574b499 100644 (file)
@@ -36,6 +36,8 @@ app:
 
 ncmp:
     dmi:
+        httpclient:
+            connectionTimeoutInSeconds: 180
         auth:
             username: some-user
             password: some-password
index ca466d7..531efdf 100644 (file)
@@ -32,7 +32,7 @@
 
     <groupId>org.onap.cps</groupId>
     <artifactId>cps-parent</artifactId>
-    <version>3.4.8-SNAPSHOT</version>
+    <version>3.4.9-SNAPSHOT</version>
     <packaging>pom</packaging>
 
     <properties>
index 93166ec..793b5a7 100644 (file)
@@ -23,7 +23,7 @@
     <parent>
         <groupId>org.onap.cps</groupId>
         <artifactId>cps-parent</artifactId>
-        <version>3.4.8-SNAPSHOT</version>
+        <version>3.4.9-SNAPSHOT</version>
         <relativePath>../cps-parent/pom.xml</relativePath>
     </parent>
 
index 90422d7..7c9df08 100644 (file)
@@ -27,7 +27,7 @@
     <parent>
         <groupId>org.onap.cps</groupId>
         <artifactId>cps-parent</artifactId>
-        <version>3.4.8-SNAPSHOT</version>
+        <version>3.4.9-SNAPSHOT</version>
         <relativePath>../cps-parent/pom.xml</relativePath>
     </parent>
 
index f8d7096..af0efe2 100644 (file)
@@ -30,11 +30,11 @@ public interface DateTimeUtility {
     DateTimeFormatter ISO_TIMESTAMP_FORMATTER = DateTimeFormatter.ofPattern(ISO_TIMESTAMP_PATTERN);
 
     static OffsetDateTime toOffsetDateTime(String datetTimestampAsString) {
-        return ! StringUtils.hasLength(datetTimestampAsString)
-            ? null : OffsetDateTime.parse(datetTimestampAsString, ISO_TIMESTAMP_FORMATTER);
+        return !StringUtils.hasLength(datetTimestampAsString) ? null
+                       : OffsetDateTime.parse(datetTimestampAsString, ISO_TIMESTAMP_FORMATTER);
     }
 
     static String toString(OffsetDateTime offsetDateTime) {
         return offsetDateTime != null ? ISO_TIMESTAMP_FORMATTER.format(offsetDateTime) : null;
     }
-}
+}
\ No newline at end of file
index 583bad0..86b2ea8 100644 (file)
@@ -26,7 +26,7 @@
     <parent>\r
         <groupId>org.onap.cps</groupId>\r
         <artifactId>cps-parent</artifactId>\r
-        <version>3.4.8-SNAPSHOT</version>\r
+        <version>3.4.9-SNAPSHOT</version>\r
         <relativePath>../cps-parent/pom.xml</relativePath>\r
     </parent>\r
 \r
index 2fb08d2..56a0464 100755 (executable)
@@ -1,6 +1,6 @@
 /*
  * ============LICENSE_START=======================================================
- * Copyright (C) 2020-2023 Nordix Foundation.
+ * Copyright (C) 2020-2024 Nordix Foundation.
  * Modifications Copyright (C) 2020-2022 Bell Canada.
  * Modifications Copyright (C) 2021 Pantheon.tech
  * Modifications Copyright (C) 2022 TechMahindra Ltd.
@@ -25,8 +25,6 @@ package org.onap.cps.spi.impl;
 
 import jakarta.transaction.Transactional;
 import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
 import java.util.stream.Collectors;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -34,17 +32,13 @@ import org.onap.cps.spi.CpsAdminPersistenceService;
 import org.onap.cps.spi.entities.AnchorEntity;
 import org.onap.cps.spi.entities.DataspaceEntity;
 import org.onap.cps.spi.entities.SchemaSetEntity;
-import org.onap.cps.spi.entities.YangResourceModuleReference;
 import org.onap.cps.spi.exceptions.AlreadyDefinedException;
 import org.onap.cps.spi.exceptions.DataspaceInUseException;
-import org.onap.cps.spi.exceptions.DataspaceNotFoundException;
-import org.onap.cps.spi.exceptions.ModuleNamesNotFoundException;
 import org.onap.cps.spi.model.Anchor;
 import org.onap.cps.spi.model.Dataspace;
 import org.onap.cps.spi.repository.AnchorRepository;
 import org.onap.cps.spi.repository.DataspaceRepository;
 import org.onap.cps.spi.repository.SchemaSetRepository;
-import org.onap.cps.spi.repository.YangResourceRepository;
 import org.springframework.dao.DataIntegrityViolationException;
 import org.springframework.stereotype.Component;
 
@@ -56,7 +50,6 @@ public class CpsAdminPersistenceServiceImpl implements CpsAdminPersistenceServic
     private final DataspaceRepository dataspaceRepository;
     private final AnchorRepository anchorRepository;
     private final SchemaSetRepository schemaSetRepository;
-    private final YangResourceRepository yangResourceRepository;
 
     @Override
     public void createDataspace(final String dataspaceName) {
@@ -138,18 +131,10 @@ public class CpsAdminPersistenceServiceImpl implements CpsAdminPersistenceServic
     }
 
     @Override
-    public Collection<Anchor> queryAnchors(final String dataspaceName, final Collection<String> inputModuleNames) {
-        try {
-            validateDataspaceAndModuleNames(dataspaceName, inputModuleNames);
-        } catch (DataspaceNotFoundException | ModuleNamesNotFoundException  e) {
-            log.info("Module search encountered unknown dataspace or modulename, treating this as nothing found");
-            return Collections.emptySet();
-        }
-
+    public Collection<String> queryAnchorNames(final String dataspaceName, final Collection<String> inputModuleNames) {
         final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
-        final Collection<AnchorEntity> anchorEntities = anchorRepository
-            .getAnchorsByDataspaceIdAndModuleNames(dataspaceEntity.getId(), inputModuleNames, inputModuleNames.size());
-        return anchorEntities.stream().map(CpsAdminPersistenceServiceImpl::toAnchor).collect(Collectors.toSet());
+        return anchorRepository.getAnchorNamesByDataspaceIdAndModuleNames(dataspaceEntity.getId(), inputModuleNames,
+                inputModuleNames.size());
     }
 
     @Override
@@ -199,25 +184,4 @@ public class CpsAdminPersistenceServiceImpl implements CpsAdminPersistenceServic
     private static Dataspace toDataspace(final DataspaceEntity dataspaceEntity) {
         return Dataspace.builder().name(dataspaceEntity.getName()).build();
     }
-
-    private void validateDataspaceAndModuleNames(final String dataspaceName,
-        final Collection<String> inputModuleNames) {
-        final Collection<String> retrievedModuleReferences =
-            yangResourceRepository.findAllModuleReferencesByDataspaceAndModuleNames(dataspaceName, inputModuleNames)
-                .stream().map(YangResourceModuleReference::getModuleName)
-                .collect(Collectors.toList());
-        if (retrievedModuleReferences.isEmpty()) {
-            verifyDataspaceName(dataspaceName);
-        }
-        if (inputModuleNames.size() > retrievedModuleReferences.size()) {
-            final List<String> unknownModules = inputModuleNames.stream()
-                .filter(moduleName -> !retrievedModuleReferences.contains(moduleName))
-                .collect(Collectors.toList());
-            throw new ModuleNamesNotFoundException(dataspaceName, unknownModules);
-        }
-    }
-
-    private void verifyDataspaceName(final String dataspaceName) {
-        dataspaceRepository.getByName(dataspaceName);
-    }
 }
index 19646c5..d78a016 100755 (executable)
@@ -72,7 +72,7 @@ public interface AnchorRepository extends JpaRepository<AnchorEntity, Long> {
 
     @Query(value = """
             SELECT
-                anchor.*
+                anchor.name
             FROM
                      yang_resource
                 JOIN schema_set_yang_resources ON schema_set_yang_resources.yang_resource_id = yang_resource.id
@@ -89,15 +89,15 @@ public interface AnchorRepository extends JpaRepository<AnchorEntity, Long> {
             HAVING
                 COUNT(DISTINCT module_name) = :sizeOfModuleNames
             """, nativeQuery = true)
-    Collection<AnchorEntity> getAnchorsByDataspaceIdAndModuleNames(@Param("dataspaceId") int dataspaceId,
-                                                                   @Param("moduleNames") String[] moduleNames,
-                                                                   @Param("sizeOfModuleNames") int sizeOfModuleNames);
+    Collection<String> getAnchorNamesByDataspaceIdAndModuleNames(@Param("dataspaceId") int dataspaceId,
+                                                                 @Param("moduleNames") String[] moduleNames,
+                                                                 @Param("sizeOfModuleNames") int sizeOfModuleNames);
 
-    default Collection<AnchorEntity> getAnchorsByDataspaceIdAndModuleNames(final int dataspaceId,
-                                                                           final Collection<String> moduleNames,
-                                                                           final int sizeOfModuleNames) {
+    default Collection<String> getAnchorNamesByDataspaceIdAndModuleNames(final int dataspaceId,
+                                                                         final Collection<String> moduleNames,
+                                                                         final int sizeOfModuleNames) {
         final String[] moduleNamesArray = moduleNames.toArray(new String[0]);
-        return getAnchorsByDataspaceIdAndModuleNames(dataspaceId, moduleNamesArray, sizeOfModuleNames);
+        return getAnchorNamesByDataspaceIdAndModuleNames(dataspaceId, moduleNamesArray, sizeOfModuleNames);
     }
 
     @Modifying
index b37f635..8be0d9a 100644 (file)
@@ -91,26 +91,6 @@ public interface YangResourceRepository extends JpaRepository<YangResourceEntity
             @Param("dataspaceName") String dataspaceName, @Param("anchorName") String anchorName,
             @Param("moduleName") String moduleName, @Param("revision") String revision);
 
-    @Query(value = """
-            SELECT DISTINCT
-                yang_resource.*
-            FROM
-                     dataspace
-                JOIN schema_set ON schema_set.dataspace_id = dataspace.id
-                JOIN schema_set_yang_resources ON schema_set_yang_resources.schema_set_id = schema_set.id
-                JOIN yang_resource ON yang_resource.id = schema_set_yang_resources.yang_resource_id
-            WHERE
-                    dataspace.name = :dataspaceName
-                AND yang_resource.module_name = ANY ( :moduleNames )
-            """, nativeQuery = true)
-    Set<YangResourceModuleReference> findAllModuleReferencesByDataspaceAndModuleNames(
-            @Param("dataspaceName") String dataspaceName, @Param("moduleNames") String[] moduleNames);
-
-    default Set<YangResourceModuleReference> findAllModuleReferencesByDataspaceAndModuleNames(
-        final String dataspaceName, final Collection<String> moduleNames) {
-        return findAllModuleReferencesByDataspaceAndModuleNames(dataspaceName, moduleNames.toArray(new String[0]));
-    }
-
     @Modifying
     @Query(value = "DELETE FROM schema_set_yang_resources WHERE schema_set_id = :schemaSetId", nativeQuery = true)
     void deleteSchemaSetYangResourceForSchemaSetId(@Param("schemaSetId") int schemaSetId);
index adca617..647b2e4 100644 (file)
   <parent>
     <groupId>org.onap.cps</groupId>
     <artifactId>cps-parent</artifactId>
-    <version>3.4.8-SNAPSHOT</version>
+    <version>3.4.9-SNAPSHOT</version>
     <relativePath>../cps-parent/pom.xml</relativePath>
   </parent>
 
   <artifactId>cps-service</artifactId>
 
   <dependencies>
+    <dependency>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-lang3</artifactId>
+    </dependency>
     <dependency>
       <groupId>org.springframework</groupId>
       <artifactId>spring-messaging</artifactId>
       <groupId>io.micrometer</groupId>
       <artifactId>micrometer-core</artifactId>
     </dependency>
+    <dependency>
+      <groupId>io.cloudevents</groupId>
+      <artifactId>cloudevents-json-jackson</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>io.cloudevents</groupId>
+      <artifactId>cloudevents-kafka</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>io.cloudevents</groupId>
+      <artifactId>cloudevents-spring</artifactId>
+    </dependency>
     <dependency>
       <groupId>jakarta.validation</groupId>
       <artifactId>jakarta.validation-api</artifactId>
index f09a795..aa9c45d 100644 (file)
@@ -21,7 +21,6 @@
 package org.onap.cps.api.impl;
 
 import java.util.Collection;
-import java.util.stream.Collectors;
 import lombok.RequiredArgsConstructor;
 import org.onap.cps.api.CpsAnchorService;
 import org.onap.cps.spi.CpsAdminPersistenceService;
@@ -87,8 +86,7 @@ public class CpsAnchorServiceImpl implements CpsAnchorService {
     @Override
     public Collection<String> queryAnchorNames(final String dataspaceName, final Collection<String> moduleNames) {
         cpsValidator.validateNameCharacters(dataspaceName);
-        final Collection<Anchor> anchors = cpsAdminPersistenceService.queryAnchors(dataspaceName, moduleNames);
-        return anchors.stream().map(Anchor::getName).collect(Collectors.toList());
+        return cpsAdminPersistenceService.queryAnchorNames(dataspaceName, moduleNames);
     }
 
     @Override
index b3f4227..f556f40 100644 (file)
@@ -3,7 +3,7 @@
  *  Copyright (C) 2021-2024 Nordix Foundation
  *  Modifications Copyright (C) 2020-2022 Bell Canada.
  *  Modifications Copyright (C) 2021 Pantheon.tech
- *  Modifications Copyright (C) 2022-2023 TechMahindra Ltd.
+ *  Modifications Copyright (C) 2022-2024 TechMahindra Ltd.
  *  Modifications Copyright (C) 2022 Deutsche Telekom AG
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
@@ -39,6 +39,8 @@ import org.onap.cps.api.CpsAnchorService;
 import org.onap.cps.api.CpsDataService;
 import org.onap.cps.api.CpsDeltaService;
 import org.onap.cps.cpspath.parser.CpsPathUtil;
+import org.onap.cps.events.CpsDataUpdateEventsService;
+import org.onap.cps.events.model.Data.Operation;
 import org.onap.cps.spi.CpsDataPersistenceService;
 import org.onap.cps.spi.FetchDescendantsOption;
 import org.onap.cps.spi.exceptions.DataValidationException;
@@ -61,7 +63,9 @@ public class CpsDataServiceImpl implements CpsDataService {
     private static final long DEFAULT_LOCK_TIMEOUT_IN_MILLISECONDS = 300L;
 
     private final CpsDataPersistenceService cpsDataPersistenceService;
+    private final CpsDataUpdateEventsService cpsDataUpdateEventsService;
     private final CpsAnchorService cpsAnchorService;
+
     private final CpsValidator cpsValidator;
     private final YangParser yangParser;
     private final CpsDeltaService cpsDeltaService;
@@ -81,6 +85,7 @@ public class CpsDataServiceImpl implements CpsDataService {
         final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName);
         final Collection<DataNode> dataNodes = buildDataNodes(anchor, ROOT_NODE_XPATH, nodeData, contentType);
         cpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName, dataNodes);
+        sendDataUpdatedEvent(anchor, ROOT_NODE_XPATH, Operation.CREATE, observedTimestamp);
     }
 
     @Override
@@ -99,6 +104,7 @@ public class CpsDataServiceImpl implements CpsDataService {
         final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName);
         final Collection<DataNode> dataNodes = buildDataNodes(anchor, parentNodeXpath, nodeData, contentType);
         cpsDataPersistenceService.addChildDataNodes(dataspaceName, anchorName, parentNodeXpath, dataNodes);
+        sendDataUpdatedEvent(anchor, parentNodeXpath, Operation.CREATE, observedTimestamp);
     }
 
     @Override
@@ -116,6 +122,7 @@ public class CpsDataServiceImpl implements CpsDataService {
             cpsDataPersistenceService.addListElements(dataspaceName, anchorName, parentNodeXpath,
                                                       listElementDataNodeCollection);
         }
+        sendDataUpdatedEvent(anchor, parentNodeXpath, Operation.UPDATE, observedTimestamp);
     }
 
     @Override
@@ -151,6 +158,7 @@ public class CpsDataServiceImpl implements CpsDataService {
         final Map<String, Map<String, Serializable>> xpathToUpdatedLeaves = dataNodesInPatch.stream()
                 .collect(Collectors.toMap(DataNode::getXpath, DataNode::getLeaves));
         cpsDataPersistenceService.batchUpdateDataLeaves(dataspaceName, anchorName, xpathToUpdatedLeaves);
+        sendDataUpdatedEvent(anchor, parentNodeXpath, Operation.UPDATE, observedTimestamp);
     }
 
     @Override
@@ -167,6 +175,7 @@ public class CpsDataServiceImpl implements CpsDataService {
         for (final DataNode dataNodeUpdate : dataNodeUpdates) {
             processDataNodeUpdate(anchor, dataNodeUpdate);
         }
+        sendDataUpdatedEvent(anchor, parentNodeXpath, Operation.UPDATE, observedTimestamp);
     }
 
     @Override
@@ -216,6 +225,7 @@ public class CpsDataServiceImpl implements CpsDataService {
         final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName);
         final Collection<DataNode> dataNodes = buildDataNodes(anchor, parentNodeXpath, jsonData, ContentType.JSON);
         cpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName, dataNodes);
+        sendDataUpdatedEvent(anchor, parentNodeXpath, Operation.UPDATE, observedTimestamp);
     }
 
     @Override
@@ -228,6 +238,8 @@ public class CpsDataServiceImpl implements CpsDataService {
         final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName);
         final Collection<DataNode> dataNodes = buildDataNodes(anchor, nodesJsonData);
         cpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName, dataNodes);
+        nodesJsonData.keySet().forEach(nodeXpath ->
+                sendDataUpdatedEvent(anchor, nodeXpath, Operation.UPDATE, observedTimestamp));
     }
 
     @Override
@@ -248,7 +260,9 @@ public class CpsDataServiceImpl implements CpsDataService {
     public void replaceListContent(final String dataspaceName, final String anchorName, final String parentNodeXpath,
             final Collection<DataNode> dataNodes, final OffsetDateTime observedTimestamp) {
         cpsValidator.validateNameCharacters(dataspaceName, anchorName);
+        final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName);
         cpsDataPersistenceService.replaceListContent(dataspaceName, anchorName, parentNodeXpath, dataNodes);
+        sendDataUpdatedEvent(anchor, parentNodeXpath, Operation.UPDATE, observedTimestamp);
     }
 
     @Override
@@ -258,6 +272,8 @@ public class CpsDataServiceImpl implements CpsDataService {
                                final OffsetDateTime observedTimestamp) {
         cpsValidator.validateNameCharacters(dataspaceName, anchorName);
         cpsDataPersistenceService.deleteDataNode(dataspaceName, anchorName, dataNodeXpath);
+        final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName);
+        sendDataUpdatedEvent(anchor, dataNodeXpath, Operation.DELETE, observedTimestamp);
     }
 
     @Override
@@ -267,8 +283,12 @@ public class CpsDataServiceImpl implements CpsDataService {
                                 final Collection<String> dataNodeXpaths, final OffsetDateTime observedTimestamp) {
         cpsValidator.validateNameCharacters(dataspaceName, anchorName);
         cpsDataPersistenceService.deleteDataNodes(dataspaceName, anchorName, dataNodeXpaths);
+        final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName);
+        dataNodeXpaths.forEach(dataNodeXpath ->
+                sendDataUpdatedEvent(anchor, dataNodeXpath, Operation.DELETE, observedTimestamp));
     }
 
+
     @Override
     @Timed(value = "cps.data.service.datanode.delete.anchor",
         description = "Time taken to delete all datanodes for an anchor")
@@ -276,6 +296,8 @@ public class CpsDataServiceImpl implements CpsDataService {
                                 final OffsetDateTime observedTimestamp) {
         cpsValidator.validateNameCharacters(dataspaceName, anchorName);
         cpsDataPersistenceService.deleteDataNodes(dataspaceName, anchorName);
+        final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName);
+        sendDataUpdatedEvent(anchor, ROOT_NODE_XPATH, Operation.DELETE, observedTimestamp);
     }
 
     @Override
@@ -286,6 +308,9 @@ public class CpsDataServiceImpl implements CpsDataService {
         cpsValidator.validateNameCharacters(dataspaceName);
         cpsValidator.validateNameCharacters(anchorNames);
         cpsDataPersistenceService.deleteDataNodes(dataspaceName, anchorNames);
+        for (final Anchor anchor : cpsAnchorService.getAnchors(dataspaceName, anchorNames)) {
+            sendDataUpdatedEvent(anchor, ROOT_NODE_XPATH, Operation.DELETE, observedTimestamp);
+        }
     }
 
     @Override
@@ -295,6 +320,8 @@ public class CpsDataServiceImpl implements CpsDataService {
         final OffsetDateTime observedTimestamp) {
         cpsValidator.validateNameCharacters(dataspaceName, anchorName);
         cpsDataPersistenceService.deleteListDataNode(dataspaceName, anchorName, listNodeXpath);
+        final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName);
+        sendDataUpdatedEvent(anchor, listNodeXpath, Operation.DELETE, observedTimestamp);
     }
 
     private Collection<DataNode> buildDataNodes(final Anchor anchor, final Map<String, String> nodesJsonData) {
@@ -345,4 +372,12 @@ public class CpsDataServiceImpl implements CpsDataService {
         }
     }
 
+    private void sendDataUpdatedEvent(final Anchor anchor, final String xpath,
+                                      final Operation operation, final OffsetDateTime observedTimestamp) {
+        try {
+            cpsDataUpdateEventsService.publishCpsDataUpdateEvent(anchor, xpath, operation, observedTimestamp);
+        } catch (final Exception exception) {
+            log.error("Failed to send message to notification service", exception);
+        }
+    }
 }
diff --git a/cps-service/src/main/java/org/onap/cps/events/CpsDataUpdateEventsService.java b/cps-service/src/main/java/org/onap/cps/events/CpsDataUpdateEventsService.java
new file mode 100644 (file)
index 0000000..d38432d
--- /dev/null
@@ -0,0 +1,109 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2024 TechMahindra Ltd.
+ * 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.events;
+
+import io.cloudevents.CloudEvent;
+import io.micrometer.core.annotation.Timed;
+import java.time.OffsetDateTime;
+import java.util.HashMap;
+import java.util.Map;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.onap.cps.events.model.CpsDataUpdatedEvent;
+import org.onap.cps.events.model.Data;
+import org.onap.cps.events.model.Data.Operation;
+import org.onap.cps.spi.model.Anchor;
+import org.onap.cps.utils.DateTimeUtility;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class CpsDataUpdateEventsService {
+
+    private final EventsPublisher<CpsDataUpdatedEvent> eventsPublisher;
+
+    @Value("${app.cps.data-updated.topic:cps-data-updated-events}")
+    private String topicName;
+
+    @Value("${notification.enabled:false}")
+    private boolean notificationsEnabled;
+
+    /**
+     * Publish the cps data update event with header to the public topic.
+     *
+     * @param anchor Anchor of the updated data
+     * @param xpath  xpath of the updated data
+     * @param operation operation performed on the data
+     * @param observedTimestamp timestamp when data was updated.
+     */
+    @Timed(value = "cps.dataupdate.events.publish", description = "Time taken to publish Data Update event")
+    public void publishCpsDataUpdateEvent(final Anchor anchor, final String xpath,
+                                          final Operation operation, final OffsetDateTime observedTimestamp) {
+        if (notificationsEnabled) {
+            final CpsDataUpdatedEvent cpsDataUpdatedEvent = createCpsDataUpdatedEvent(anchor,
+                    observedTimestamp, xpath, operation);
+            final String updateEventId = anchor.getDataspaceName() + ":" + anchor.getName();
+            final Map<String, String> extensions = createUpdateEventExtensions(updateEventId);
+            final CloudEvent cpsDataUpdatedEventAsCloudEvent =
+                    CpsEvent.builder().type(CpsDataUpdatedEvent.class.getTypeName()).data(cpsDataUpdatedEvent)
+                            .extensions(extensions).build().asCloudEvent();
+            eventsPublisher.publishCloudEvent(topicName, updateEventId, cpsDataUpdatedEventAsCloudEvent);
+        } else {
+            log.debug("Notifications disabled.");
+        }
+    }
+
+    private CpsDataUpdatedEvent createCpsDataUpdatedEvent(final Anchor anchor, final OffsetDateTime observedTimestamp,
+                                                          final String xpath,
+                                                          final Operation rootNodeOperation) {
+        final CpsDataUpdatedEvent cpsDataUpdatedEvent = new CpsDataUpdatedEvent();
+        final Data updateEventData = new Data();
+        updateEventData.setObservedTimestamp(DateTimeUtility.toString(observedTimestamp));
+        updateEventData.setDataspaceName(anchor.getDataspaceName());
+        updateEventData.setAnchorName(anchor.getName());
+        updateEventData.setSchemaSetName(anchor.getSchemaSetName());
+        updateEventData.setOperation(getRootNodeOperation(xpath, rootNodeOperation));
+        updateEventData.setXpath(xpath);
+        cpsDataUpdatedEvent.setData(updateEventData);
+        return cpsDataUpdatedEvent;
+    }
+
+    private Map<String, String> createUpdateEventExtensions(final String eventKey) {
+        final Map<String, String> extensions = new HashMap<>();
+        extensions.put("correlationid", eventKey);
+        return extensions;
+    }
+
+    private Operation getRootNodeOperation(final String xpath, final Operation operation) {
+        return isRootXpath(xpath) || isRootContainerNodeXpath(xpath) ? operation : Operation.UPDATE;
+    }
+
+    private static boolean isRootXpath(final String xpath) {
+        return "/".equals(xpath) || "".equals(xpath);
+    }
+
+    private static boolean isRootContainerNodeXpath(final String xpath) {
+        return 0 == xpath.lastIndexOf('/');
+    }
+}
diff --git a/cps-service/src/main/java/org/onap/cps/events/CpsEvent.java b/cps-service/src/main/java/org/onap/cps/events/CpsEvent.java
new file mode 100644 (file)
index 0000000..c19abc1
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2024 TechMahindra Ltd.
+ *  ================================================================================
+ *  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.events;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.cloudevents.CloudEvent;
+import io.cloudevents.core.builder.CloudEventBuilder;
+import java.net.URI;
+import java.util.Map;
+import java.util.UUID;
+import lombok.Builder;
+import org.apache.commons.lang3.StringUtils;
+import org.onap.cps.utils.JsonObjectMapper;
+
+@Builder
+public class CpsEvent {
+
+    private Object data;
+    private Map<String, String> extensions;
+    private String type;
+    @Builder.Default
+    private static final String CLOUD_EVENT_SPEC_VERSION_V1 = "1.0.0";
+    @Builder.Default
+    private static final String CLOUD_EVENT_SOURCE = "CPS";
+
+    private final JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(new ObjectMapper());
+
+    /**
+     * Creates ncmp cloud event with provided attributes.
+     *
+     * @return Cloud Event
+     */
+
+    public CloudEvent asCloudEvent() {
+        final CloudEventBuilder cloudEventBuilder = io.cloudevents.core.builder
+            .CloudEventBuilder.v1()
+            .withId(UUID.randomUUID().toString())
+            .withSource(URI.create(CLOUD_EVENT_SOURCE))
+            .withType(type)
+            .withDataSchema(URI.create("urn:cps:" + type + ":" + CLOUD_EVENT_SPEC_VERSION_V1))
+            .withData(jsonObjectMapper.asJsonBytes(data));
+        extensions.entrySet().stream()
+            .filter(extensionEntry -> StringUtils.isNotBlank(extensionEntry.getValue()))
+            .forEach(extensionEntry ->
+                    cloudEventBuilder.withExtension(extensionEntry.getKey(), extensionEntry.getValue()));
+        return cloudEventBuilder.build();
+    }
+}
index 5a1810f..2b21619 100755 (executable)
@@ -107,7 +107,7 @@ public interface CpsAdminPersistenceService {
      * @return a collection of anchor names in the given dataspace. The schema set for each anchor must include all the
      *         given module names
      */
-    Collection<Anchor> queryAnchors(String dataspaceName, Collection<String> moduleNames);
+    Collection<String> queryAnchorNames(String dataspaceName, Collection<String> moduleNames);
 
     /**
      * Get an anchor in the given dataspace using the anchor name.
@@ -1,12 +1,13 @@
 /*
  * ============LICENSE_START=======================================================
  * Copyright (c) 2021 Bell Canada.
+ * 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
+ *       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,
@@ -22,18 +23,12 @@ package org.onap.cps.utils;
 
 import java.time.OffsetDateTime;
 import java.time.format.DateTimeFormatter;
-import org.springframework.util.StringUtils;
 
 public interface DateTimeUtility {
 
     String ISO_TIMESTAMP_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";
     DateTimeFormatter ISO_TIMESTAMP_FORMATTER = DateTimeFormatter.ofPattern(ISO_TIMESTAMP_PATTERN);
 
-    static OffsetDateTime toOffsetDateTime(String datetTimestampAsString) {
-        return ! StringUtils.hasLength(datetTimestampAsString)
-            ? null : OffsetDateTime.parse(datetTimestampAsString, ISO_TIMESTAMP_FORMATTER);
-    }
-
     static String toString(OffsetDateTime offsetDateTime) {
         return offsetDateTime != null ? ISO_TIMESTAMP_FORMATTER.format(offsetDateTime) : null;
     }
index 3546b81..c786538 100644 (file)
@@ -118,7 +118,7 @@ class CpsAnchorServiceImplSpec extends Specification {
 
     def 'Query all anchor identifiers for a dataspace and module names.'() {
         given: 'the persistence service is invoked with the expected parameters and returns a list of anchors'
-            mockCpsAdminPersistenceService.queryAnchors('some-dataspace-name', ['some-module-name']) >> [new Anchor(name:'some-anchor-identifier')]
+            mockCpsAdminPersistenceService.queryAnchorNames('some-dataspace-name', ['some-module-name']) >> ['some-anchor-identifier']
         when: 'query anchor names is called using a dataspace name and module name'
             def result = objectUnderTest.queryAnchorNames('some-dataspace-name', ['some-module-name'])
         then: 'get anchor identifiers returns the same anchor identifier returned by the persistence layer'
@@ -130,7 +130,7 @@ class CpsAnchorServiceImplSpec extends Specification {
     def 'Query all anchors with Module Names Not Found Exception in persistence layer.'() {
         given: 'the persistence layer throws a Module Names Not Found Exception'
             def originalException = new ModuleNamesNotFoundException('exception-ds', ['m1', 'm2'])
-            mockCpsAdminPersistenceService.queryAnchors(*_) >> { throw originalException}
+            mockCpsAdminPersistenceService.queryAnchorNames(*_) >> { throw originalException}
         when: 'attempt query anchors'
             objectUnderTest.queryAnchorNames('some-dataspace-name', [])
         then: 'the same exception is thrown (up)'
index b2b2d7d..fcbfd05 100644 (file)
@@ -3,7 +3,7 @@
  *  Copyright (C) 2021-2024 Nordix Foundation
  *  Modifications Copyright (C) 2021 Pantheon.tech
  *  Modifications Copyright (C) 2021-2022 Bell Canada.
- *  Modifications Copyright (C) 2022-2023 TechMahindra Ltd.
+ *  Modifications Copyright (C) 2022-2024 TechMahindra Ltd.
  *  Modifications Copyright (C) 2022 Deutsche Telekom AG
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
 
 package org.onap.cps.api.impl
 
+import ch.qos.logback.classic.Level
+import ch.qos.logback.classic.Logger
+import ch.qos.logback.core.read.ListAppender
 import org.onap.cps.TestUtils
 import org.onap.cps.api.CpsAnchorService
 import org.onap.cps.api.CpsDeltaService
+import org.onap.cps.events.CpsDataUpdateEventsService
 import org.onap.cps.spi.CpsDataPersistenceService
 import org.onap.cps.spi.FetchDescendantsOption
 import org.onap.cps.spi.exceptions.ConcurrencyException
@@ -41,6 +45,8 @@ import org.onap.cps.utils.YangParser
 import org.onap.cps.utils.YangParserHelper
 import org.onap.cps.yang.YangTextSchemaSourceSet
 import org.onap.cps.yang.YangTextSchemaSourceSetBuilder
+import org.slf4j.LoggerFactory
+import org.springframework.context.annotation.AnnotationConfigApplicationContext
 import spock.lang.Shared
 import spock.lang.Specification
 import java.time.OffsetDateTime
@@ -52,13 +58,28 @@ class CpsDataServiceImplSpec extends Specification {
     def mockCpsValidator = Mock(CpsValidator)
     def yangParser = new YangParser(new YangParserHelper(), mockYangTextSchemaSourceSetCache)
     def mockCpsDeltaService = Mock(CpsDeltaService);
+    def mockDataUpdateEventsService = Mock(CpsDataUpdateEventsService)
 
-    def objectUnderTest = new CpsDataServiceImpl(mockCpsDataPersistenceService, mockCpsAnchorService, mockCpsValidator, yangParser, mockCpsDeltaService)
+    def objectUnderTest = new CpsDataServiceImpl(mockCpsDataPersistenceService, mockDataUpdateEventsService, mockCpsAnchorService, mockCpsValidator, yangParser, mockCpsDeltaService)
+
+    def logger = (Logger) LoggerFactory.getLogger(objectUnderTest.class)
+    def loggingListAppender
+    def applicationContext = new AnnotationConfigApplicationContext()
 
     def setup() {
         mockCpsAnchorService.getAnchor(dataspaceName, anchorName) >> anchor
         mockCpsAnchorService.getAnchor(dataspaceName, ANCHOR_NAME_1) >> anchor1
         mockCpsAnchorService.getAnchor(dataspaceName, ANCHOR_NAME_2) >> anchor2
+        logger.setLevel(Level.DEBUG)
+        loggingListAppender = new ListAppender()
+        logger.addAppender(loggingListAppender)
+        loggingListAppender.start()
+        applicationContext.refresh()
+    }
+
+    void cleanup() {
+        ((Logger) LoggerFactory.getLogger(CpsDataServiceImpl.class)).detachAndStopAllAppenders()
+        applicationContext.close()
     }
 
     @Shared
@@ -459,6 +480,19 @@ class CpsDataServiceImplSpec extends Specification {
             1 * mockCpsDataPersistenceService.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName', 250L)
     }
 
+    def 'Exception is thrown while publishing the notification.'(){
+        given: 'schema set for given anchor and dataspace references test-tree model'
+            setupSchemaSetMocks('test-tree.yang')
+        when: 'publisher set to throw an exception'
+            mockDataUpdateEventsService.publishCpsDataUpdateEvent(_, _, _, _) >> { throw new Exception("publishing failed")}
+        and: 'an update event is performed'
+            objectUnderTest.updateNodeLeaves(dataspaceName, anchorName, '/', '{"test-tree": {"branch": []}}', observedTimestamp)
+        then: 'the exception is not bubbled up'
+            noExceptionThrown()
+        and: "the exception message is logged"
+            def logs = loggingListAppender.list.toString()
+            assert logs.contains('Failed to send message to notification service')
+    }
     def setupSchemaSetMocks(String... yangResources) {
         def mockYangTextSchemaSourceSet = Mock(YangTextSchemaSourceSet)
         mockYangTextSchemaSourceSetCache.get(dataspaceName, schemaSetName) >> mockYangTextSchemaSourceSet
index 140dfaa..57f2f8e 100755 (executable)
@@ -3,7 +3,7 @@
  * Copyright (C) 2021-2024 Nordix Foundation.\r
  * Modifications Copyright (C) 2021-2022 Bell Canada.\r
  * Modifications Copyright (C) 2021 Pantheon.tech\r
- * Modifications Copyright (C) 2022-2023 TechMahindra Ltd.\r
+ * Modifications Copyright (C) 2022-2024 TechMahindra Ltd.\r
  * ================================================================================\r
  * Licensed under the Apache License, Version 2.0 (the "License");\r
  * you may not use this file except in compliance with the License.\r
@@ -26,6 +26,7 @@ package org.onap.cps.api.impl
 import org.onap.cps.TestUtils\r
 import org.onap.cps.api.CpsAnchorService\r
 import org.onap.cps.api.CpsDeltaService\r
+import org.onap.cps.events.CpsDataUpdateEventsService\r
 import org.onap.cps.spi.CpsDataPersistenceService\r
 import org.onap.cps.spi.CpsModulePersistenceService\r
 import org.onap.cps.spi.model.Anchor\r
@@ -50,7 +51,8 @@ class E2ENetworkSliceSpec extends Specification {
     def cpsModuleServiceImpl = new CpsModuleServiceImpl(mockModuleStoreService,\r
             mockYangTextSchemaSourceSetCache, mockCpsAnchorService, mockCpsValidator,timedYangTextSchemaSourceSetBuilder)\r
 \r
-    def cpsDataServiceImpl = new CpsDataServiceImpl(mockDataStoreService, mockCpsAnchorService, mockCpsValidator, yangParser, mockCpsDeltaService)\r
+    def mockDataUpdateEventsService = Mock(CpsDataUpdateEventsService)\r
+    def cpsDataServiceImpl = new CpsDataServiceImpl(mockDataStoreService, mockDataUpdateEventsService, mockCpsAnchorService, mockCpsValidator, yangParser, mockCpsDeltaService)\r
 \r
     def dataspaceName = 'someDataspace'\r
     def anchorName = 'someAnchor'\r
diff --git a/cps-service/src/test/groovy/org/onap/cps/events/CpsDataUpdateEventsServiceSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/events/CpsDataUpdateEventsServiceSpec.groovy
new file mode 100644 (file)
index 0000000..24b9ab5
--- /dev/null
@@ -0,0 +1,109 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2024 TechMahindra Ltd.
+ * ================================================================================
+ * 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.events
+
+import static org.onap.cps.events.model.Data.Operation.CREATE
+import static org.onap.cps.events.model.Data.Operation.DELETE
+import static org.onap.cps.events.model.Data.Operation.UPDATE
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import io.cloudevents.CloudEvent
+import io.cloudevents.core.CloudEventUtils
+import io.cloudevents.jackson.PojoCloudEventDataMapper
+import org.onap.cps.events.model.CpsDataUpdatedEvent
+import org.onap.cps.events.model.Data
+import org.onap.cps.spi.model.Anchor
+import org.onap.cps.utils.JsonObjectMapper
+import org.springframework.test.context.ContextConfiguration
+import spock.lang.Specification
+
+import java.time.OffsetDateTime
+
+@ContextConfiguration(classes = [ObjectMapper, JsonObjectMapper])
+class CpsDataUpdateEventsServiceSpec extends Specification {
+    def mockEventsPublisher = Mock(EventsPublisher)
+    def notificationsEnabled = true
+    def objectMapper = new ObjectMapper();
+
+    def objectUnderTest = new CpsDataUpdateEventsService(mockEventsPublisher)
+
+    def 'Create and Publish cps update event where events are #scenario'() {
+        given: 'an anchor, operation and observed timestamp'
+            def anchor = new Anchor('anchor01', 'dataspace01', 'schema01');
+            def operation = operationInRequest
+            def observedTimestamp = OffsetDateTime.now()
+        and: 'notificationsEnabled is #notificationsEnabled and it will be true as default'
+            objectUnderTest.notificationsEnabled = true
+        when: 'service is called to publish data update event'
+            objectUnderTest.topicName = "cps-core-event"
+            objectUnderTest.publishCpsDataUpdateEvent(anchor, xpath, operation, observedTimestamp)
+        then: 'the event contains the required attributes'
+            1 * mockEventsPublisher.publishCloudEvent('cps-core-event', 'dataspace01:anchor01', _) >> {
+            args ->
+                {
+                    def cpsDataUpdatedEvent = (args[2] as CloudEvent)
+                    assert cpsDataUpdatedEvent.getExtension('correlationid') == 'dataspace01:anchor01'
+                    assert cpsDataUpdatedEvent.type == 'org.onap.cps.events.model.CpsDataUpdatedEvent'
+                    assert cpsDataUpdatedEvent.source.toString() == 'CPS'
+                    def actualEventOperation = CloudEventUtils.mapData(cpsDataUpdatedEvent, PojoCloudEventDataMapper.from(objectMapper, CpsDataUpdatedEvent.class)).getValue().data.operation
+                    assert actualEventOperation == expectedOperation
+                }
+            }
+        where: 'the following values are used'
+        scenario                                   | xpath        | operationInRequest  || expectedOperation
+        'empty xpath'                              | ''           | CREATE              || CREATE
+        'root xpath and create operation'          | '/'          | CREATE              || CREATE
+        'root xpath and update operation'          | '/'          | UPDATE              || UPDATE
+        'root xpath and delete operation'          | '/'          | DELETE              || DELETE
+        'not root xpath and update operation'      | 'test'       | UPDATE              || UPDATE
+        'root node xpath and create operation'     | '/test'      | CREATE              || CREATE
+        'non root node xpath and update operation' | '/test/path' | CREATE              || UPDATE
+        'non root node xpath and delete operation' | '/test/path' | DELETE              || UPDATE
+    }
+
+    def 'publish cps update event when notification service is disabled'() {
+        given: 'an anchor, operation and observed timestamp'
+            def anchor = new Anchor('anchor01', 'dataspace01', 'schema01');
+            def operation = CREATE
+            def observedTimestamp = OffsetDateTime.now()
+        and: 'notificationsEnabled is false'
+            objectUnderTest.notificationsEnabled = false
+        when: 'service is called to publish data update event'
+            objectUnderTest.topicName = "cps-core-event"
+            objectUnderTest.publishCpsDataUpdateEvent(anchor, '/', operation, observedTimestamp)
+        then: 'the event contains the required attributes'
+            0 * mockEventsPublisher.publishCloudEvent('cps-core-event', 'dataspace01:anchor01', _)
+    }
+
+    def 'publish cps update event when no timestamp provided'() {
+        given: 'an anchor, operation and null timestamp'
+            def anchor = new Anchor('anchor01', 'dataspace01', 'schema01');
+            def operation = CREATE
+            def observedTimestamp = null
+        and: 'notificationsEnabled is true'
+            objectUnderTest.notificationsEnabled = true
+        when: 'service is called to publish data update event'
+            objectUnderTest.topicName = "cps-core-event"
+            objectUnderTest.publishCpsDataUpdateEvent(anchor, '/', operation, observedTimestamp)
+        then: 'the event is published'
+            1 * mockEventsPublisher.publishCloudEvent('cps-core-event', 'dataspace01:anchor01', _)
+    }
+}
index bb7d3e5..1b110ed 100644 (file)
@@ -22,7 +22,7 @@
     <parent>
         <groupId>org.onap.cps</groupId>
         <artifactId>dmi-plugin-demo-and-csit-stub</artifactId>
-        <version>3.4.8-SNAPSHOT</version>
+        <version>3.4.9-SNAPSHOT</version>
     </parent>
 
     <artifactId>dmi-plugin-demo-and-csit-stub-app</artifactId>
index 288159c..43a47c6 100644 (file)
@@ -21,7 +21,7 @@
     <parent>
         <groupId>org.onap.cps</groupId>
         <artifactId>dmi-plugin-demo-and-csit-stub</artifactId>
-        <version>3.4.8-SNAPSHOT</version>
+        <version>3.4.9-SNAPSHOT</version>
     </parent>
     <artifactId>dmi-plugin-demo-and-csit-stub-service</artifactId>
 
index d857645..61ae891 100644 (file)
@@ -22,7 +22,7 @@
     <parent>
         <groupId>org.onap.cps</groupId>
         <artifactId>cps-parent</artifactId>
-        <version>3.4.8-SNAPSHOT</version>
+        <version>3.4.9-SNAPSHOT</version>
         <relativePath>../cps-parent/pom.xml</relativePath>
     </parent>
 
index 8a6c13e..4f5180d 100644 (file)
@@ -26,7 +26,7 @@ paths:
           content:
             application/json:
               example:
-                status: 400 BAD_REQUEST
+                status: 400
                 message: Bad request error message
                 details: Bad request error details
               schema:
@@ -190,7 +190,7 @@ components:
       content:
         application/json:
           example:
-            status: 400 BAD_REQUEST
+            status: 400
             message: Bad request error message
             details: Bad request error details
           schema:
index da0b0b3..9f6a1b2 100644 (file)
@@ -70,7 +70,7 @@ paths:
           content:
             application/json:
               example:
-                status: 400 BAD_REQUEST
+                status: 400
                 message: Bad request error message
                 details: Bad request error details
               schema:
@@ -216,7 +216,7 @@ paths:
           content:
             application/json:
               example:
-                status: 400 BAD_REQUEST
+                status: 400
                 message: Bad request error message
                 details: Bad request error details
               schema:
@@ -331,7 +331,7 @@ paths:
           content:
             application/json:
               example:
-                status: 400 BAD_REQUEST
+                status: 400
                 message: Bad request error message
                 details: Bad request error details
               schema:
@@ -449,7 +449,7 @@ paths:
           content:
             application/json:
               example:
-                status: 400 BAD_REQUEST
+                status: 400
                 message: Bad request error message
                 details: Bad request error details
               schema:
@@ -571,7 +571,7 @@ paths:
           content:
             application/json:
               example:
-                status: 400 BAD_REQUEST
+                status: 400
                 message: Bad request error message
                 details: Bad request error details
               schema:
@@ -615,7 +615,8 @@ paths:
     post:
       description: This request will be handled asynchronously using messaging to
         the supplied topic. The rest response will be an acknowledge with a requestId
-        to identify the relevant messages.
+        to identify the relevant messages. A maximum of 50 cm handles per operation
+        is supported.
       operationId: executeDataOperationForCmHandles
       parameters:
       - allowReserved: true
@@ -652,7 +653,7 @@ paths:
           content:
             application/json:
               example:
-                status: 400 BAD_REQUEST
+                status: 400
                 message: Bad request error message
                 details: Bad request error details
               schema:
@@ -668,6 +669,17 @@ paths:
               schema:
                 $ref: '#/components/schemas/ErrorMessage'
           description: Forbidden
+        "413":
+          content:
+            application/json:
+              example:
+                status: 413
+                message: Payload Too Large error message
+                details: Payload Too Large error details
+              schema:
+                $ref: '#/components/schemas/ErrorMessage'
+          description: The request is larger than the server is willing or able to
+            process
         "500":
           content:
             application/json:
@@ -776,7 +788,7 @@ paths:
           content:
             application/json:
               example:
-                status: 400 BAD_REQUEST
+                status: 400
                 message: Bad request error message
                 details: Bad request error details
               schema:
@@ -843,7 +855,7 @@ paths:
           content:
             application/json:
               example:
-                status: 400 BAD_REQUEST
+                status: 400
                 message: Bad request error message
                 details: Bad request error details
               schema:
@@ -981,7 +993,7 @@ paths:
           content:
             application/json:
               example:
-                status: 400 BAD_REQUEST
+                status: 400
                 message: Bad request error message
                 details: Bad request error details
               schema:
@@ -1034,7 +1046,7 @@ paths:
           content:
             application/json:
               example:
-                status: 400 BAD_REQUEST
+                status: 400
                 message: Bad request error message
                 details: Bad request error details
               schema:
@@ -1087,7 +1099,7 @@ paths:
           content:
             application/json:
               example:
-                status: 400 BAD_REQUEST
+                status: 400
                 message: Bad request error message
                 details: Bad request error details
               schema:
@@ -1164,7 +1176,7 @@ paths:
           content:
             application/json:
               example:
-                status: 400 BAD_REQUEST
+                status: 400
                 message: Bad request error message
                 details: Bad request error details
               schema:
@@ -1227,7 +1239,7 @@ paths:
           content:
             application/json:
               example:
-                status: 400 BAD_REQUEST
+                status: 400
                 message: Bad request error message
                 details: Bad request error details
               schema:
@@ -1289,7 +1301,7 @@ paths:
           content:
             application/json:
               example:
-                status: 400 BAD_REQUEST
+                status: 400
                 message: Bad request error message
                 details: Bad request error details
               schema:
@@ -1603,7 +1615,7 @@ components:
       content:
         application/json:
           example:
-            status: 400 BAD_REQUEST
+            status: 400
             message: Bad request error message
             details: Bad request error details
           schema:
@@ -1662,6 +1674,16 @@ components:
           schema:
             $ref: '#/components/schemas/ErrorMessage'
       description: The specified resource was not found
+    PayloadTooLarge:
+      content:
+        application/json:
+          example:
+            status: 413
+            message: Payload Too Large error message
+            details: Payload Too Large error details
+          schema:
+            $ref: '#/components/schemas/ErrorMessage'
+      description: The request is larger than the server is willing or able to process
   schemas:
     ErrorMessage:
       properties:
@@ -1743,6 +1765,8 @@ components:
           type: string
         targetIds:
           items:
+            description: "targeted cm handles, maximum of 50 supported. If this limit\
+              \ is exceeded the request wil be refused."
             example: "[\"da310eecdb8d44c2acc0ddaae01174b1\",\"c748c58f8e0b438f9fd1f28370b17d47\"\
               ]"
             type: string
index 018cf4a..0c6ce0e 100644 (file)
@@ -16,7 +16,7 @@ CPS-NCMP Message Status Codes
     +-----------------+------------------------------------------------------+-----------------------------------+
     | 1               | successfully applied subscription                    | CM Data Notification Subscription |
     +-----------------+------------------------------------------------------+-----------------------------------+
-    | 100             | cm handle id(s) is(are) not found                    | Data Operation, Inventory         |
+    | 100             | cm handle id(s) is(are) not found                    | All features                      |
     +-----------------+------------------------------------------------------+-----------------------------------+
     | 101             | cm handle(s) not ready                               | Data Operation                    |
     +-----------------+------------------------------------------------------+-----------------------------------+
@@ -32,7 +32,7 @@ CPS-NCMP Message Status Codes
     +-----------------+------------------------------------------------------+-----------------------------------+
     | 107             | southbound system is busy                            | Data Operation                    |
     +-----------------+------------------------------------------------------+-----------------------------------+
-    | 108             | Unknown error                                        | Inventory                         |
+    | 108             | Unknown error                                        | All features                      |
     +-----------------+------------------------------------------------------+-----------------------------------+
     | 109             | cm-handle already exists                             | Inventory                         |
     +-----------------+------------------------------------------------------+-----------------------------------+
index d35ed99..bd85137 100644 (file)
@@ -16,6 +16,33 @@ CPS Release Notes
 ..      * * *   NEW DELHI   * * *
 ..      =========================
 
+Version: 3.4.9
+==============
+
+Release Data
+------------
+
++--------------------------------------+--------------------------------------------------------+
+| **CPS Project**                      |                                                        |
+|                                      |                                                        |
++--------------------------------------+--------------------------------------------------------+
+| **Docker images**                    | onap/cps-and-ncmp:3.4.9                                |
+|                                      |                                                        |
++--------------------------------------+--------------------------------------------------------+
+| **Release designation**              | 3.4.9 New Delhi                                        |
+|                                      |                                                        |
++--------------------------------------+--------------------------------------------------------+
+| **Release date**                     | Not yet released                                       |
+|                                      |                                                        |
++--------------------------------------+--------------------------------------------------------+
+
+Bug Fixes
+---------
+3.4.9
+
+Features
+--------
+
 Version: 3.4.8
 ==============
 
@@ -32,10 +59,18 @@ Release Data
 | **Release designation**              | 3.4.8 New Delhi                                        |
 |                                      |                                                        |
 +--------------------------------------+--------------------------------------------------------+
-| **Release date**                     | Not yet released                                       |
+| **Release date**                     | 2024 May 1                                             |
 |                                      |                                                        |
 +--------------------------------------+--------------------------------------------------------+
 
+Bug Fixes
+---------
+3.4.8
+    - `CPS-2186 <https://jira.onap.org/browse/CPS-2186>`_ Report async task failures to client topic during data operations request
+    - `CPS-2190 <https://jira.onap.org/browse/CPS-2190>`_ Improve performance of NCMP module searches
+    - `CPS-2194 <https://jira.onap.org/browse/CPS-2194>`_ Added defaults for CPS and DMI username and password
+    - `CPS-2204 <https://jira.onap.org/browse/CPS-2204>`_ Added error handling for yang module upgrade operation
+
 Features
 --------
 
index d4ee0cc..98513f2 100644 (file)
@@ -23,7 +23,7 @@
     <parent>
         <groupId>org.onap.cps</groupId>
         <artifactId>cps-parent</artifactId>
-        <version>3.4.8-SNAPSHOT</version>
+        <version>3.4.9-SNAPSHOT</version>
         <relativePath>../cps-parent/pom.xml</relativePath>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 
     <dependencies>
         <!-- T E S T   D E P E N D E N C I E S -->
-        <dependency>
-            <groupId>org.codehaus.groovy</groupId>
-            <artifactId>groovy</artifactId>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.junit.jupiter</groupId>
-            <artifactId>junit-jupiter</artifactId>
-        </dependency>
         <dependency>
             <groupId>${project.groupId}</groupId>
             <artifactId>cps-rest</artifactId>
             <artifactId>cps-ncmp-service</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.codehaus.groovy</groupId>
+            <artifactId>groovy</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.testcontainers</groupId>
+            <artifactId>kafka</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.squareup.okhttp3</groupId>
+            <artifactId>mockwebserver</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.testcontainers</groupId>
+            <artifactId>postgresql</artifactId>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>org.spockframework</groupId>
             <artifactId>spock-core</artifactId>
             <artifactId>spring-kafka-test</artifactId>
             <scope>test</scope>
         </dependency>
-        <dependency>
-            <groupId>org.testcontainers</groupId>
-            <artifactId>postgresql</artifactId>
-            <scope>test</scope>
-        </dependency>
         <dependency>
             <groupId>org.testcontainers</groupId>
             <artifactId>spock</artifactId>
             <scope>test</scope>
         </dependency>
-        <dependency>
-            <groupId>org.testcontainers</groupId>
-            <artifactId>kafka</artifactId>
-            <scope>test</scope>
-        </dependency>
     </dependencies>
 
     <profiles>
index 51b0238..44fc258 100644 (file)
 
 package org.onap.cps.integration.base
 
+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 java.time.OffsetDateTime
 import java.time.format.DateTimeFormatter
+import okhttp3.mockwebserver.MockWebServer
 import org.onap.cps.api.CpsAnchorService
 import org.onap.cps.api.CpsDataService
 import org.onap.cps.api.CpsDataspaceService
@@ -48,23 +53,12 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMock
 import org.springframework.boot.test.context.SpringBootTest
 import org.springframework.context.annotation.ComponentScan
 import org.springframework.data.jpa.repository.config.EnableJpaRepositories
-import org.springframework.http.HttpStatus
-import org.springframework.http.MediaType
-import org.springframework.test.web.client.ExpectedCount
-import org.springframework.test.web.client.MockRestServiceServer
 import org.springframework.test.web.servlet.MockMvc
-import org.springframework.web.client.RestTemplate
 import org.testcontainers.spock.Testcontainers
 import spock.lang.Shared
 import spock.lang.Specification
 import spock.util.concurrent.PollingConditions
 
-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
-
 @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK, classes = [CpsDataspaceService])
 @Testcontainers
 @EnableAutoConfiguration
@@ -110,18 +104,17 @@ abstract class CpsIntegrationSpecBase extends Specification {
     @Autowired
     NetworkCmProxyQueryService networkCmProxyQueryService
 
-    @Autowired
-    RestTemplate restTemplate
-
     @Autowired
     ModuleSyncWatchdog moduleSyncWatchdog
 
     @Autowired
     JsonObjectMapper jsonObjectMapper
 
-    MockRestServiceServer mockDmiServer = null
+    MockWebServer mockDmiServer = null
+    DmiDispatcher dmiDispatcher = new DmiDispatcher()
+
+    def DMI_URL = null
 
-    static DMI_URL = 'http://mock-dmi-server'
     static NO_MODULE_SET_TAG = ''
     static GENERAL_TEST_DATASPACE = 'generalTestDataspace'
     static BOOKSTORE_SCHEMA_SET = 'bookstoreSchemaSet'
@@ -135,7 +128,14 @@ abstract class CpsIntegrationSpecBase extends Specification {
             createStandardBookStoreSchemaSet(GENERAL_TEST_DATASPACE)
             initialized = true
         }
-        mockDmiServer = MockRestServiceServer.bindTo(restTemplate).ignoreExpectOrder(true).build()
+        mockDmiServer = new MockWebServer()
+        mockDmiServer.setDispatcher(dmiDispatcher)
+        mockDmiServer.start()
+        DMI_URL = String.format("http://%s:%s", mockDmiServer.getHostName(), mockDmiServer.getPort())
+    }
+
+    def cleanup() {
+        mockDmiServer.shutdown()
     }
 
     def static readResourceDataFile(filename) {
@@ -217,23 +217,6 @@ abstract class CpsIntegrationSpecBase extends Specification {
         networkCmProxyDataService.updateDmiRegistrationAndSyncModule(new DmiPluginRegistration(dmiPlugin: dmiPlugin, removedCmHandles: cmHandleIds))
     }
 
-    def mockDmiResponsesForModuleSync(dmiPlugin, cmHandleId, dmiModuleReferencesResponse, dmiModuleResourcesResponse) {
-        mockDmiServer.expect(requestTo("${dmiPlugin}/dmi/v1/ch/${cmHandleId}/modules"))
-                .andRespond(withStatus(HttpStatus.OK).contentType(MediaType.APPLICATION_JSON).body(dmiModuleReferencesResponse))
-        mockDmiServer.expect(requestTo("${dmiPlugin}/dmi/v1/ch/${cmHandleId}/moduleResources"))
-                .andRespond(withStatus(HttpStatus.OK).contentType(MediaType.APPLICATION_JSON).body(dmiModuleResourcesResponse))
-    }
-
-    def mockDmiIsNotAvailableForModuleSync(dmiPlugin, cmHandleId) {
-        mockDmiServer.expect(requestTo("${dmiPlugin}/dmi/v1/ch/${cmHandleId}/modules"))
-                .andRespond(withStatus(HttpStatus.SERVICE_UNAVAILABLE))
-    }
-
-    def mockDmiWillRespondToHealthChecks(dmiPlugin) {
-        mockDmiServer.expect(ExpectedCount.between(0, Integer.MAX_VALUE), requestTo("${dmiPlugin}/actuator/health"))
-                .andRespond(withStatus(HttpStatus.OK).contentType(MediaType.APPLICATION_JSON).body('{"status":"UP"}'))
-    }
-
     def overrideCmHandleLastUpdateTime(cmHandleId, newUpdateTime) {
         String ISO_TIMESTAMP_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";
         DateTimeFormatter ISO_TIMESTAMP_FORMATTER = DateTimeFormatter.ofPattern(ISO_TIMESTAMP_PATTERN);
diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/base/DmiDispatcher.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/base/DmiDispatcher.groovy
new file mode 100644 (file)
index 0000000..6676cb7
--- /dev/null
@@ -0,0 +1,105 @@
+/*
+ *  ============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.base
+
+import static org.onap.cps.integration.base.CpsIntegrationSpecBase.readResourceDataFile
+
+import org.springframework.http.HttpHeaders
+import java.util.regex.Matcher
+import okhttp3.mockwebserver.Dispatcher
+import okhttp3.mockwebserver.MockResponse
+import okhttp3.mockwebserver.RecordedRequest
+import org.springframework.http.HttpStatus
+import org.springframework.http.MediaType
+
+/**
+ * This class simulates responses from the DMI server in NCMP integration tests.
+ *
+ * It is to be used with a MockWebServer, using mockWebServer.setDispatcher(new DmiDispatcher()).
+ *
+ * It currently implements the following endpoints:
+ * - /actuator/health: healthcheck endpoint that responds with 200 OK / {"status":"UP"}
+ * - /dmi/v1/ch/{cmHandleId}/modules: returns module references for a CM handle
+ * - /dmi/v1/ch/{cmHandleId}/moduleResources: returns modules resources for a CM handle
+ *
+ * The module resource/reference responses are generated based on the module names in the map moduleNamesPerCmHandleId.
+ * To configure the DMI so that CM handle 'ch-1' will have modules 'M1' and 'M2', you may use:
+ *   dmiDispatcher.moduleNamesPerCmHandleId.put('ch-1', ['M1', 'M2']);
+ *
+ * To simulate the DMI not being available, the boolean isAvailable may be set to false, in which case the mock server
+ * will always respond with 503 Service Unavailable.
+ */
+class DmiDispatcher extends Dispatcher {
+
+    static final MODULE_REFERENCES_RESPONSE_TEMPLATE = readResourceDataFile('mock-dmi-responses/moduleReferencesTemplate.json')
+    static final MODULE_RESOURCES_RESPONSE_TEMPLATE = readResourceDataFile('mock-dmi-responses/moduleResourcesTemplate.json')
+
+    def isAvailable = true
+
+    Map<String, List<String>> moduleNamesPerCmHandleId = [:]
+
+    @Override
+    MockResponse dispatch(RecordedRequest request) {
+        if (!isAvailable) {
+            return new MockResponse().setResponseCode(HttpStatus.SERVICE_UNAVAILABLE.value())
+        }
+        switch (request.path) {
+            case ~/^\/dmi\/v1\/ch\/(.*)\/modules$/:
+                def cmHandleId = Matcher.lastMatcher[0][1]
+                return getModuleReferencesResponse(cmHandleId)
+
+            case ~/^\/dmi\/v1\/ch\/(.*)\/moduleResources$/:
+                def cmHandleId = Matcher.lastMatcher[0][1]
+                return getModuleResourcesResponse(cmHandleId)
+
+            default:
+                throw new IllegalArgumentException('Mock DMI does not handle path ' + request.path)
+        }
+    }
+
+    private getModuleReferencesResponse(cmHandleId) {
+        def moduleReferences = '{"schemas":[' + getModuleNamesForCmHandle(cmHandleId).collect {
+            MODULE_REFERENCES_RESPONSE_TEMPLATE.replaceAll("<MODULE_NAME>", it)
+        }.join(',') + ']}'
+        return mockOkResponseWithBody(moduleReferences)
+    }
+
+    private getModuleResourcesResponse(cmHandleId) {
+        def moduleResources = '[' + getModuleNamesForCmHandle(cmHandleId).collect {
+            MODULE_RESOURCES_RESPONSE_TEMPLATE.replaceAll("<MODULE_NAME>", it)
+        }.join(',') + ']'
+        return mockOkResponseWithBody(moduleResources)
+    }
+
+    private getModuleNamesForCmHandle(cmHandleId) {
+        if (!moduleNamesPerCmHandleId.containsKey(cmHandleId)) {
+            throw new IllegalArgumentException('Mock DMI has no modules configured for ' + cmHandleId)
+        }
+        return moduleNamesPerCmHandleId.get(cmHandleId)
+    }
+
+    private static mockOkResponseWithBody(responseBody) {
+        return new MockResponse()
+                .setResponseCode(HttpStatus.OK.value())
+                .addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
+                .setBody(responseBody)
+    }
+}
index 04c5dfc..4bba8a5 100644 (file)
@@ -84,15 +84,11 @@ class CpsAnchorServiceIntegrationSpec extends CpsIntegrationSpecBase {
             objectUnderTest.deleteAnchor(GENERAL_TEST_DATASPACE, 'newAnchor')
     }
 
-    def 'Query anchors without any known modules and #scenario'() {
+    def 'Query anchors without any known modules'() {
         when: 'querying for anchors with #scenario'
-            def result = objectUnderTest.queryAnchorNames(dataspaceName, ['unknownModule'])
+            def result = objectUnderTest.queryAnchorNames(GENERAL_TEST_DATASPACE, ['unknownModule'])
         then: 'an empty result is returned (no error)'
             assert result == []
-        where:
-            scenario                 | dataspaceName
-            'non existing database'  | 'nonExistingDataspace'
-            'just unknown module(s)' | GENERAL_TEST_DATASPACE
     }
 
     def 'Update anchor schema set.'() {
index 28c4280..4ffe586 100644 (file)
 
 package org.onap.cps.integration.functional
 
-import java.time.Duration
-import org.onap.cps.integration.base.CpsIntegrationSpecBase
-import org.springframework.http.HttpHeaders
-import org.springframework.http.HttpStatus
-import org.springframework.http.MediaType
-import org.springframework.test.web.client.match.MockRestRequestMatchers
-
 import static org.springframework.http.HttpMethod.GET
 import static org.springframework.http.HttpMethod.DELETE
 import static org.springframework.http.HttpMethod.PATCH
 import static org.springframework.http.HttpMethod.POST
 import static org.springframework.http.HttpMethod.PUT
-import static org.springframework.test.web.client.match.MockRestRequestMatchers.method
-import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo
-import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.request
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
 
+import okhttp3.mockwebserver.Dispatcher
+import okhttp3.mockwebserver.MockResponse
+import okhttp3.mockwebserver.RecordedRequest
+import org.jetbrains.annotations.NotNull
+import org.onap.cps.integration.base.CpsIntegrationSpecBase
+import org.springframework.http.HttpHeaders
+import org.springframework.http.HttpStatus
+import org.springframework.http.MediaType
+import spock.util.concurrent.PollingConditions
+
 class NcmpBearerTokenPassthroughSpec extends CpsIntegrationSpecBase {
 
-    static final MODULE_REFERENCES_RESPONSE = readResourceDataFile('mock-dmi-responses/bookStoreAWithModules_M1_M2_Response.json')
-    static final MODULE_RESOURCES_RESPONSE = readResourceDataFile('mock-dmi-responses/bookStoreAWithModules_M1_M2_ResourcesResponse.json')
+    def lastAuthHeaderReceived = null
 
     def setup() {
-        mockDmiWillRespondToHealthChecks(DMI_URL)
-        mockDmiResponsesForModuleSync(DMI_URL, 'ch-1', MODULE_REFERENCES_RESPONSE, MODULE_RESOURCES_RESPONSE)
+        dmiDispatcher.moduleNamesPerCmHandleId['ch-1'] = ['M1', 'M2']
         registerCmHandle(DMI_URL, 'ch-1', NO_MODULE_SET_TAG)
-        mockDmiServer.reset()
-        mockDmiWillRespondToHealthChecks(DMI_URL)
+
+        mockDmiServer.setDispatcher(new Dispatcher() {
+            @Override
+            MockResponse dispatch(@NotNull RecordedRequest request) throws InterruptedException {
+                if (request.path == '/actuator/health') {
+                        return new MockResponse()
+                                .addHeader("Content-Type", MediaType.APPLICATION_JSON).setBody('{"status":"UP"}')
+                                .setResponseCode(HttpStatus.OK.value())
+                } else {
+                    lastAuthHeaderReceived = request.getHeader('Authorization')
+                    return new MockResponse().setResponseCode(HttpStatus.OK.value())
+                }
+            }
+        })
     }
 
     def cleanup() {
@@ -56,12 +66,6 @@ class NcmpBearerTokenPassthroughSpec extends CpsIntegrationSpecBase {
     }
 
     def 'Bearer token is passed from NCMP to DMI in pass-through data operations.'() {
-        given: 'DMI will expect to receive a request with a bearer token'
-            def targetDmiUrl = "$DMI_URL/dmi/v1/ch/ch-1/data/ds/ncmp-datastore:passthrough-running?resourceIdentifier=my-resource-id"
-            mockDmiServer.expect(requestTo(targetDmiUrl))
-                    .andExpect(MockRestRequestMatchers.header(HttpHeaders.AUTHORIZATION, 'Bearer some-bearer-token'))
-                    .andRespond(withStatus(HttpStatus.OK).contentType(MediaType.APPLICATION_JSON))
-
         when: 'a pass-through data request is sent to NCMP with a bearer token'
             mvc.perform(request(httpMethod, '/ncmp/v1/ch/ch-1/data/ds/ncmp-datastore:passthrough-running')
                     .queryParam('resourceIdentifier', 'my-resource-id')
@@ -71,19 +75,13 @@ class NcmpBearerTokenPassthroughSpec extends CpsIntegrationSpecBase {
                     .andExpect(status().is2xxSuccessful())
 
         then: 'DMI has received request with bearer token'
-            mockDmiServer.verify()
+            lastAuthHeaderReceived == 'Bearer some-bearer-token'
 
         where: 'all HTTP operations are applied'
             httpMethod << [GET, POST, PUT, PATCH, DELETE]
     }
 
     def 'Basic auth header is NOT passed from NCMP to DMI in pass-through data operations.'() {
-        given: 'DMI will expect to receive a request with no authorization header'
-            def targetDmiUrl = "$DMI_URL/dmi/v1/ch/ch-1/data/ds/ncmp-datastore:passthrough-running?resourceIdentifier=my-resource-id"
-            mockDmiServer.expect(requestTo(targetDmiUrl))
-                    .andExpect(MockRestRequestMatchers.headerDoesNotExist(HttpHeaders.AUTHORIZATION))
-                    .andRespond(withStatus(HttpStatus.OK).contentType(MediaType.APPLICATION_JSON))
-
         when: 'a pass-through data request is sent to NCMP with basic authentication'
             mvc.perform(request(httpMethod, '/ncmp/v1/ch/ch-1/data/ds/ncmp-datastore:passthrough-running')
                     .queryParam('resourceIdentifier', 'my-resource-id')
@@ -93,18 +91,13 @@ class NcmpBearerTokenPassthroughSpec extends CpsIntegrationSpecBase {
                     .andExpect(status().is2xxSuccessful())
 
         then: 'DMI has received request with no authorization header'
-            mockDmiServer.verify()
+            lastAuthHeaderReceived == null
 
         where: 'all HTTP operations are applied'
             httpMethod << [GET, POST, PUT, PATCH, DELETE]
     }
 
     def 'Bearer token is passed from NCMP to DMI in async batch pass-through data operation.'() {
-        given: 'DMI will expect to receive a request with a bearer token'
-            mockDmiServer.expect(method(POST))
-                    .andExpect(MockRestRequestMatchers.header(HttpHeaders.AUTHORIZATION, 'Bearer some-bearer-token'))
-                    .andRespond(withStatus(HttpStatus.OK).contentType(MediaType.APPLICATION_JSON))
-
         when: 'a pass-through async data request is sent to NCMP with a bearer token'
             def requestBody = """{"operations": [{
                 "operation": "read",
@@ -121,7 +114,9 @@ class NcmpBearerTokenPassthroughSpec extends CpsIntegrationSpecBase {
                     .andExpect(status().is2xxSuccessful())
 
         then: 'DMI will receive the async request with bearer token'
-            mockDmiServer.verify(Duration.ofSeconds(1))
+            new PollingConditions().within(3, () -> {
+                assert lastAuthHeaderReceived == 'Bearer some-bearer-token'
+            })
     }
 
 }
index a6b516c..5c337f1 100644 (file)
@@ -41,19 +41,13 @@ class NcmpCmHandleCreateSpec extends CpsIntegrationSpecBase {
 
     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')
-    static final MODULE_RESOURCES_RESPONSE_B = readResourceDataFile('mock-dmi-responses/bookStoreBWithModules_M1_M3_ResourcesResponse.json')
-
     def setup() {
         objectUnderTest = networkCmProxyDataService
-        mockDmiWillRespondToHealthChecks(DMI_URL)
     }
 
     def 'CM Handle registration is successful.'() {
         given: 'DMI will return modules when requested'
-            mockDmiResponsesForModuleSync(DMI_URL, 'ch-1', MODULE_REFERENCES_RESPONSE_A, MODULE_RESOURCES_RESPONSE_A)
+            dmiDispatcher.moduleNamesPerCmHandleId['ch-1'] = ['M1', 'M2']
 
         and: 'consumer subscribed to topic'
             kafkaConsumer.subscribe(['ncmp-events'])
@@ -88,16 +82,13 @@ class NcmpCmHandleCreateSpec extends CpsIntegrationSpecBase {
         and: 'the CM-handle has expected modules'
             assert ['M1', 'M2'] == objectUnderTest.getYangResourcesModuleReferences('ch-1').moduleName.sort()
 
-        and: 'DMI received expected requests'
-            mockDmiServer.verify()
-
         cleanup: 'deregister CM handle'
             deregisterCmHandle(DMI_URL, 'ch-1')
     }
 
     def 'CM Handle goes to LOCKED state when DMI gives error during module sync.'() {
         given: 'DMI is not available to handle requests'
-            mockDmiIsNotAvailableForModuleSync(DMI_URL, 'ch-1')
+            dmiDispatcher.isAvailable = false
 
         when: 'a CM-handle is registered for creation'
             def cmHandleToCreate = new NcmpServiceCmHandle(cmHandleId: 'ch-1')
@@ -122,13 +113,11 @@ class NcmpCmHandleCreateSpec extends CpsIntegrationSpecBase {
     }
 
     def 'Create a CM-handle with existing moduleSetTag.'() {
-        given: 'existing CM-handles cm-1 with moduleSetTag "A", and cm-2 with moduleSetTag "B"'
-            mockDmiResponsesForModuleSync(DMI_URL, 'ch-1', MODULE_REFERENCES_RESPONSE_A, MODULE_RESOURCES_RESPONSE_A)
-            mockDmiResponsesForModuleSync(DMI_URL, 'ch-2', MODULE_REFERENCES_RESPONSE_B, MODULE_RESOURCES_RESPONSE_B)
+        given: 'DMI will return modules when requested'
+            dmiDispatcher.moduleNamesPerCmHandleId = ['ch-1': ['M1', 'M2'], 'ch-2': ['M1', 'M3']]
+        and: 'existing CM-handles cm-1 with moduleSetTag "A", and cm-2 with moduleSetTag "B"'
             registerCmHandle(DMI_URL, 'ch-1', 'A')
             registerCmHandle(DMI_URL, 'ch-2', 'B')
-            assert ['M1', 'M2'] == objectUnderTest.getYangResourcesModuleReferences('ch-1').moduleName.sort()
-            assert ['M1', 'M3'] == objectUnderTest.getYangResourcesModuleReferences('ch-2').moduleName.sort()
 
         when: 'a CM-handle is registered for creation with moduleSetTag "B"'
             def cmHandleToCreate = new NcmpServiceCmHandle(cmHandleId: 'ch-3', moduleSetTag: 'B')
@@ -152,11 +141,7 @@ class NcmpCmHandleCreateSpec extends CpsIntegrationSpecBase {
 
     def 'CM Handle retry after failed module sync.'() {
         given: 'DMI is not initially available to handle requests'
-            mockDmiIsNotAvailableForModuleSync(DMI_URL, 'ch-1')
-            mockDmiIsNotAvailableForModuleSync(DMI_URL, 'ch-2')
-        and: 'DMI will be available for retry'
-            mockDmiResponsesForModuleSync(DMI_URL, 'ch-1', MODULE_REFERENCES_RESPONSE_A, MODULE_RESOURCES_RESPONSE_A)
-            mockDmiResponsesForModuleSync(DMI_URL, 'ch-2', MODULE_REFERENCES_RESPONSE_B, MODULE_RESOURCES_RESPONSE_B)
+            dmiDispatcher.isAvailable = false
 
         when: 'CM-handles are registered for creation'
             def cmHandlesToCreate = [new NcmpServiceCmHandle(cmHandleId: 'ch-1'), new NcmpServiceCmHandle(cmHandleId: 'ch-2')]
@@ -179,7 +164,11 @@ class NcmpCmHandleCreateSpec extends CpsIntegrationSpecBase {
             assert objectUnderTest.getCmHandleCompositeState('ch-1').cmHandleState == CmHandleState.ADVISED
             assert objectUnderTest.getCmHandleCompositeState('ch-2').cmHandleState == CmHandleState.ADVISED
 
-        when: 'module sync runs'
+        when: 'DMI is available for retry'
+            dmiDispatcher.isAvailable = true
+        and: 'DMI will return expected modules'
+            dmiDispatcher.moduleNamesPerCmHandleId = ['ch-1': ['M1', 'M2'], 'ch-2': ['M1', 'M3']]
+        and: 'module sync runs'
             moduleSyncWatchdog.moduleSyncAdvisedCmHandles()
         then: 'CM-handles go to READY state'
             new PollingConditions().within(3, () -> {
@@ -192,8 +181,6 @@ class NcmpCmHandleCreateSpec extends CpsIntegrationSpecBase {
         and: 'CM-handles have expected module set tags (blank)'
             assert objectUnderTest.getNcmpServiceCmHandle('ch-1').moduleSetTag == ''
             assert objectUnderTest.getNcmpServiceCmHandle('ch-2').moduleSetTag == ''
-        and: 'DMI received expected requests'
-            mockDmiServer.verify()
 
         cleanup: 'deregister CM handle'
             deregisterCmHandles(DMI_URL, ['ch-1', 'ch-2'])
index 5421ad3..4d1d77e 100644 (file)
@@ -27,38 +27,26 @@ 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.UpgradedCmHandles
-import org.springframework.http.HttpStatus
 import spock.util.concurrent.PollingConditions
 
-import static org.springframework.test.web.client.match.MockRestRequestMatchers.anything
-import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus
-
 class NcmpCmHandleUpgradeSpec extends CpsIntegrationSpecBase {
 
     NetworkCmProxyDataService objectUnderTest
 
-    static final INITIAL_MODULE_REFERENCES_RESPONSE = readResourceDataFile('mock-dmi-responses/bookStoreAWithModules_M1_M2_Response.json')
-    static final INITIAL_MODULE_RESOURCES_RESPONSE = readResourceDataFile('mock-dmi-responses/bookStoreAWithModules_M1_M2_ResourcesResponse.json')
-    static final UPDATED_MODULE_REFERENCES_RESPONSE = readResourceDataFile('mock-dmi-responses/bookStoreBWithModules_M1_M3_Response.json')
-    static final UPDATED_MODULE_RESOURCES_RESPONSE = readResourceDataFile('mock-dmi-responses/bookStoreBWithModules_M1_M3_ResourcesResponse.json')
     static final CM_HANDLE_ID = 'ch-1'
     static final CM_HANDLE_ID_WITH_EXISTING_MODULE_SET_TAG = 'ch-2'
 
     def setup() {
         objectUnderTest = networkCmProxyDataService
-        mockDmiWillRespondToHealthChecks(DMI_URL)
     }
 
     def 'Upgrade CM-handle with new moduleSetTag or no moduleSetTag.'() {
-        given: 'DMI will return modules for initial registration'
-            mockDmiResponsesForModuleSync(DMI_URL, CM_HANDLE_ID, INITIAL_MODULE_REFERENCES_RESPONSE, INITIAL_MODULE_RESOURCES_RESPONSE)
-        and: 'DMI returns different modules for upgrade'
-            mockDmiResponsesForModuleSync(DMI_URL, CM_HANDLE_ID, UPDATED_MODULE_REFERENCES_RESPONSE, UPDATED_MODULE_RESOURCES_RESPONSE)
-
-        when: 'a CM-handle is created with expected initial modules: M1 and M2'
+        given: 'a CM-handle is created with expected initial modules: M1 and M2'
+            dmiDispatcher.moduleNamesPerCmHandleId[CM_HANDLE_ID] = ['M1', 'M2']
             registerCmHandle(DMI_URL, CM_HANDLE_ID, initialModuleSetTag)
             assert ['M1', 'M2'] == objectUnderTest.getYangResourcesModuleReferences(CM_HANDLE_ID).moduleName.sort()
-        and: "the CM-handle is upgraded with given moduleSetTag '${updatedModuleSetTag}'"
+
+        when: "the CM-handle is upgraded with given moduleSetTag '${updatedModuleSetTag}'"
             def cmHandlesToUpgrade = new UpgradedCmHandles(cmHandles: [CM_HANDLE_ID], moduleSetTag: updatedModuleSetTag)
             def dmiPluginRegistrationResponse = networkCmProxyDataService.updateDmiRegistrationAndSyncModule(
                     new DmiPluginRegistration(dmiPlugin: DMI_URL, upgradedCmHandles: cmHandlesToUpgrade))
@@ -72,14 +60,16 @@ class NcmpCmHandleUpgradeSpec extends CpsIntegrationSpecBase {
             assert cmHandleCompositeState.lockReason.lockReasonCategory == LockReasonCategory.MODULE_UPGRADE
             assert cmHandleCompositeState.lockReason.details == "Upgrade to ModuleSetTag: ${updatedModuleSetTag}"
 
-        when: 'module sync runs'
+        when: 'DMI will return different modules for upgrade: M1 and M3'
+            dmiDispatcher.moduleNamesPerCmHandleId[CM_HANDLE_ID] = ['M1', 'M3']
+        and: 'module sync runs'
             moduleSyncWatchdog.resetPreviouslyFailedCmHandles()
             moduleSyncWatchdog.moduleSyncAdvisedCmHandles()
 
         then: 'CM-handle goes to READY state'
-            new PollingConditions().within(3, () -> {
+            new PollingConditions().eventually {
                 assert CmHandleState.READY == objectUnderTest.getCmHandleCompositeState(CM_HANDLE_ID).cmHandleState
-            })
+            }
 
         and: 'the CM-handle has expected moduleSetTag'
             assert objectUnderTest.getNcmpServiceCmHandle(CM_HANDLE_ID).moduleSetTag == updatedModuleSetTag
@@ -87,9 +77,6 @@ class NcmpCmHandleUpgradeSpec extends CpsIntegrationSpecBase {
         and: 'CM-handle has expected updated modules: M1 and M3'
             assert ['M1', 'M3'] == objectUnderTest.getYangResourcesModuleReferences(CM_HANDLE_ID).moduleName.sort()
 
-        and: 'DMI received expected requests'
-            mockDmiServer.verify()
-
         cleanup: 'deregister CM-handle'
             deregisterCmHandle(DMI_URL, CM_HANDLE_ID)
 
@@ -103,8 +90,8 @@ class NcmpCmHandleUpgradeSpec extends CpsIntegrationSpecBase {
 
     def 'Upgrade CM-handle with existing moduleSetTag.'() {
         given: 'DMI will return modules for registration'
-            mockDmiResponsesForModuleSync(DMI_URL, CM_HANDLE_ID, INITIAL_MODULE_REFERENCES_RESPONSE, INITIAL_MODULE_RESOURCES_RESPONSE)
-            mockDmiResponsesForModuleSync(DMI_URL, CM_HANDLE_ID_WITH_EXISTING_MODULE_SET_TAG, UPDATED_MODULE_REFERENCES_RESPONSE, UPDATED_MODULE_RESOURCES_RESPONSE)
+            dmiDispatcher.moduleNamesPerCmHandleId[CM_HANDLE_ID] = ['M1', 'M2']
+            dmiDispatcher.moduleNamesPerCmHandleId[CM_HANDLE_ID_WITH_EXISTING_MODULE_SET_TAG] = ['M1', 'M3']
         and: "an existing CM-handle handle with moduleSetTag '${updatedModuleSetTag}'"
             registerCmHandle(DMI_URL, CM_HANDLE_ID_WITH_EXISTING_MODULE_SET_TAG, updatedModuleSetTag)
             assert ['M1', 'M3'] == objectUnderTest.getYangResourcesModuleReferences(CM_HANDLE_ID_WITH_EXISTING_MODULE_SET_TAG).moduleName.sort()
@@ -125,9 +112,9 @@ class NcmpCmHandleUpgradeSpec extends CpsIntegrationSpecBase {
             moduleSyncWatchdog.moduleSyncAdvisedCmHandles()
 
         then: 'CM-handle goes to READY state'
-            new PollingConditions().within(3, () -> {
+            new PollingConditions().eventually {
                 assert CmHandleState.READY == objectUnderTest.getCmHandleCompositeState(CM_HANDLE_ID).cmHandleState
-            })
+            }
 
         and: 'the CM-handle has expected moduleSetTag'
             assert objectUnderTest.getNcmpServiceCmHandle(CM_HANDLE_ID).moduleSetTag == updatedModuleSetTag
@@ -146,7 +133,7 @@ class NcmpCmHandleUpgradeSpec extends CpsIntegrationSpecBase {
 
     def 'Skip upgrade of CM-handle with same moduleSetTag as before.'() {
         given: 'an existing CM-handle with expected initial modules: M1 and M2'
-            mockDmiResponsesForModuleSync(DMI_URL, CM_HANDLE_ID, INITIAL_MODULE_REFERENCES_RESPONSE, INITIAL_MODULE_RESOURCES_RESPONSE)
+            dmiDispatcher.moduleNamesPerCmHandleId[CM_HANDLE_ID] = ['M1', 'M2']
             registerCmHandle(DMI_URL, CM_HANDLE_ID, 'same')
             assert ['M1', 'M2'] == objectUnderTest.getYangResourcesModuleReferences(CM_HANDLE_ID).moduleName.sort()
 
@@ -169,14 +156,13 @@ class NcmpCmHandleUpgradeSpec extends CpsIntegrationSpecBase {
     }
 
     def 'Upgrade of CM-handle fails due to DMI error.'() {
-        given: 'DMI will return modules for initial registration'
-            mockDmiResponsesForModuleSync(DMI_URL, CM_HANDLE_ID, INITIAL_MODULE_REFERENCES_RESPONSE, INITIAL_MODULE_RESOURCES_RESPONSE)
-        and: 'DMI returns error code for upgrade'
-            mockDmiServer.expect(anything()).andRespond(withStatus(HttpStatus.SERVICE_UNAVAILABLE))
-
-        when: 'a CM-handle is created'
+        given: 'a CM-handle exists'
+            dmiDispatcher.moduleNamesPerCmHandleId[CM_HANDLE_ID] = ['M1', 'M2']
             registerCmHandle(DMI_URL, CM_HANDLE_ID, 'oldTag')
-        and: 'the CM-handle is upgraded'
+        and: 'DMI is not available for upgrade'
+            dmiDispatcher.isAvailable = false
+
+        when: 'the CM-handle is upgraded'
             def cmHandlesToUpgrade = new UpgradedCmHandles(cmHandles: [CM_HANDLE_ID], moduleSetTag: 'newTag')
             networkCmProxyDataService.updateDmiRegistrationAndSyncModule(
                     new DmiPluginRegistration(dmiPlugin: DMI_URL, upgradedCmHandles: cmHandlesToUpgrade))
@@ -186,11 +172,11 @@ class NcmpCmHandleUpgradeSpec extends CpsIntegrationSpecBase {
             moduleSyncWatchdog.moduleSyncAdvisedCmHandles()
 
         then: 'CM-handle goes to LOCKED state with reason MODULE_UPGRADE_FAILED'
-            new PollingConditions().within(3, () -> {
+            new PollingConditions(timeout: 3).eventually {
                 def cmHandleCompositeState = objectUnderTest.getCmHandleCompositeState(CM_HANDLE_ID)
                 assert cmHandleCompositeState.cmHandleState == CmHandleState.LOCKED
                 assert cmHandleCompositeState.lockReason.lockReasonCategory == LockReasonCategory.MODULE_UPGRADE_FAILED
-            })
+            }
 
         and: 'the CM-handle has same moduleSetTag as before'
             assert objectUnderTest.getNcmpServiceCmHandle(CM_HANDLE_ID).moduleSetTag == 'oldTag'
index df74a05..302c7e5 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ *  ============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.functional
 
 import static org.onap.cps.ncmp.api.impl.operations.DatastoreType.PASSTHROUGH_RUNNING;
@@ -18,7 +38,7 @@ class NcmpCmNotificationSubscriptionSpec extends CpsIntegrationSpecBase {
             assert cmNotificationSubscriptionPersistenceService.
                 getOngoingCmNotificationSubscriptionIds(datastoreType,cmHandleId,xpath).size() == 0
         when: 'we add a new cm notification subscription'
-            cmNotificationSubscriptionPersistenceService.addOrUpdateCmNotificationSubscription(datastoreType,cmHandleId,xpath,
+            cmNotificationSubscriptionPersistenceService.addCmNotificationSubscription(datastoreType,cmHandleId,xpath,
                 'subId-1')
         then: 'there is an ongoing cm subscription for that CM handle and xpath'
             assert cmNotificationSubscriptionPersistenceService.isOngoingCmNotificationSubscription(datastoreType,cmHandleId,xpath)
@@ -27,15 +47,13 @@ class NcmpCmNotificationSubscriptionSpec extends CpsIntegrationSpecBase {
                 getOngoingCmNotificationSubscriptionIds(datastoreType,cmHandleId,xpath).size() == 1
     }
 
-    def 'Adding a cm notification subscription to an already existing'() {
-        given: 'an ongoing cm subscription'
+    def 'Adding a cm notification subscription to the already existing'() {
+        given: 'an ongoing cm subscription with the following details'
             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,
+            cmNotificationSubscriptionPersistenceService.addCmNotificationSubscription(datastoreType,cmHandleId,xpath,
                 'subId-2')
         then: 'it is added to the ongoing list of subscription ids'
             def subscriptionIds = cmNotificationSubscriptionPersistenceService.getOngoingCmNotificationSubscriptionIds(datastoreType,cmHandleId,xpath)
@@ -43,4 +61,35 @@ class NcmpCmNotificationSubscriptionSpec extends CpsIntegrationSpecBase {
         and: 'both subscription ids exists for the CM handle and xpath'
             assert subscriptionIds.contains("subId-1") && subscriptionIds.contains("subId-2")
     }
+
+    def 'Removing cm notification subscriber among other subscribers'() {
+        given: 'an ongoing cm subscription with the following details'
+            def datastoreType = PASSTHROUGH_RUNNING
+            def cmHandleId = 'ch-1'
+            def xpath = '/x/y'
+        and: 'the number of subscribers is as follows'
+            def originalNumberOfSubscribers =
+                cmNotificationSubscriptionPersistenceService.getOngoingCmNotificationSubscriptionIds(datastoreType,cmHandleId,xpath).size()
+        when: 'a subscriber is removed'
+            cmNotificationSubscriptionPersistenceService.removeCmNotificationSubscription(datastoreType,cmHandleId,xpath,'subId-2')
+        then: 'the number of subscribers is reduced by 1'
+            def updatedNumberOfSubscribers =
+                cmNotificationSubscriptionPersistenceService.getOngoingCmNotificationSubscriptionIds(datastoreType,cmHandleId,xpath).size()
+            assert updatedNumberOfSubscribers == originalNumberOfSubscribers-1
+    }
+
+    def 'Removing the LAST cm notification subscriber for a given cm handle, datastore and xpath'() {
+        given: 'an ongoing cm subscription with the following details'
+            def datastoreType = PASSTHROUGH_RUNNING
+            def cmHandleId = 'ch-1'
+            def xpath = '/x/y'
+        and: 'there is only one subscriber'
+            assert cmNotificationSubscriptionPersistenceService
+                .getOngoingCmNotificationSubscriptionIds(datastoreType,cmHandleId,xpath).size() == 1
+        when: 'only subscriber is removed'
+            cmNotificationSubscriptionPersistenceService.removeCmNotificationSubscription(datastoreType,cmHandleId,xpath,'subId-1')
+        then: 'there are no longer any subscriptions for the cm handle, datastore and xpath'
+            assert !cmNotificationSubscriptionPersistenceService.isOngoingCmNotificationSubscription(datastoreType, cmHandleId, xpath)
+    }
+
 }
index d7f8771..950cd65 100644 (file)
@@ -32,20 +32,13 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
 
 class NcmpRestApiSpec extends CpsIntegrationSpecBase {
 
-    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')
-    static final MODULE_RESOURCES_RESPONSE_B = readResourceDataFile('mock-dmi-responses/bookStoreBWithModules_M1_M3_ResourcesResponse.json')
-
-    def setup() {
-        mockDmiWillRespondToHealthChecks(DMI_URL)
-    }
-
     def 'Register CM Handles using REST API.'() {
         given: 'DMI will return modules'
-            mockDmiResponsesForModuleSync(DMI_URL, 'ch-1', MODULE_REFERENCES_RESPONSE_A, MODULE_RESOURCES_RESPONSE_A)
-            mockDmiResponsesForModuleSync(DMI_URL, 'ch-2', MODULE_REFERENCES_RESPONSE_A, MODULE_RESOURCES_RESPONSE_A)
-            mockDmiResponsesForModuleSync(DMI_URL, 'ch-3', MODULE_REFERENCES_RESPONSE_B, MODULE_RESOURCES_RESPONSE_B)
+            dmiDispatcher.moduleNamesPerCmHandleId = [
+                'ch-1': ['M1', 'M2'],
+                'ch-2': ['M1', 'M2'],
+                'ch-3': ['M1', 'M3']
+            ]
         and: 'a POST request is made to register the CM Handles'
             def requestBody = '{"dmiPlugin":"'+DMI_URL+'","createdCmHandles":[{"cmHandle":"ch-1"},{"cmHandle":"ch-2"},{"cmHandle":"ch-3"}]}'
             mvc.perform(post('/ncmpInventory/v1/ch').contentType(MediaType.APPLICATION_JSON).content(requestBody))
@@ -53,10 +46,12 @@ class NcmpRestApiSpec extends CpsIntegrationSpecBase {
         when: 'module sync runs'
             moduleSyncWatchdog.moduleSyncAdvisedCmHandles()
         then: 'CM-handles go to READY state'
-            new PollingConditions(timeout: 3, delay: 0.5).eventually {
-                mvc.perform(get('/ncmp/v1/ch/ch-1'))
-                        .andExpect(status().isOk())
-                        .andExpect(jsonPath('$.state.cmHandleState').value('READY'))
+            new PollingConditions().eventually {
+                (1..3).each {
+                    mvc.perform(get('/ncmp/v1/ch/ch-'+it))
+                            .andExpect(status().isOk())
+                            .andExpect(jsonPath('$.state.cmHandleState').value('READY'))
+                }
             }
     }
 
@@ -71,7 +66,7 @@ class NcmpRestApiSpec extends CpsIntegrationSpecBase {
                     ]
                 }""".formatted(moduleName)
         expect: "a search for module ${moduleName} returns expected CM handles"
-            mvc.perform(post(DMI_URL+'/ncmp/v1/ch/id-searches').contentType(MediaType.APPLICATION_JSON).content(requestBodyWithModuleCondition))
+            mvc.perform(post('/ncmp/v1/ch/id-searches').contentType(MediaType.APPLICATION_JSON).content(requestBodyWithModuleCondition))
                     .andExpect(status().is2xxSuccessful())
                     .andExpect(jsonPath('$[*]', containsInAnyOrder(expectedCmHandles.toArray())))
                     .andExpect(jsonPath('$', hasSize(expectedCmHandles.size())));
index 0c1e1f5..a4ee23a 100644 (file)
@@ -47,7 +47,7 @@ class CpsDataServiceLimitsPerfTest extends CpsPerfTestBase {
             resourceMeter.stop()
             def durationInSeconds = resourceMeter.getTotalTimeInSeconds()
         then: 'the operation completes within 12 seconds'
-            recordAndAssertResourceUsage("Creating 33,000 books", 12, durationInSeconds, 150, resourceMeter.getTotalMemoryUsageInMB())
+            recordAndAssertResourceUsage("Creating 33,000 books", 16, durationInSeconds, 150, resourceMeter.getTotalMemoryUsageInMB())
     }
 
     def 'Get data nodes from multiple xpaths 32K (2^15) limit exceeded.'() {
@@ -88,7 +88,7 @@ class CpsDataServiceLimitsPerfTest extends CpsPerfTestBase {
             resourceMeter.stop()
             def durationInSeconds = resourceMeter.getTotalTimeInSeconds()
         then: 'test data is deleted in 1 second'
-            recordAndAssertResourceUsage("Deleting test data", 1, durationInSeconds, 3, resourceMeter.getTotalMemoryUsageInMB())
+            recordAndAssertResourceUsage("Deleting test data", 0.1, durationInSeconds, 3, resourceMeter.getTotalMemoryUsageInMB())
     }
 
     def countDataNodes() {
index 2efbf7d..f76c3c5 100644 (file)
@@ -56,7 +56,7 @@ class DeletePerfTest extends CpsPerfTestBase {
             resourceMeter.stop()
             def deleteDurationInSeconds = resourceMeter.getTotalTimeInSeconds()
         then: 'delete duration is within expected time and memory used is within limit'
-            recordAndAssertResourceUsage('Delete 100 containers', 2.5, deleteDurationInSeconds, 20, resourceMeter.getTotalMemoryUsageInMB())
+            recordAndAssertResourceUsage('Delete 100 containers', 2.2, deleteDurationInSeconds, 20, resourceMeter.getTotalMemoryUsageInMB())
     }
 
     def 'Batch delete 100 container nodes'() {
@@ -70,7 +70,7 @@ class DeletePerfTest extends CpsPerfTestBase {
             resourceMeter.stop()
             def deleteDurationInSeconds = resourceMeter.getTotalTimeInSeconds()
         then: 'delete duration is within expected time and memory used is within limit'
-            recordAndAssertResourceUsage('Batch delete 100 containers', 0.6, deleteDurationInSeconds, 2, resourceMeter.getTotalMemoryUsageInMB())
+            recordAndAssertResourceUsage('Batch delete 100 containers', 0.7, deleteDurationInSeconds, 2, resourceMeter.getTotalMemoryUsageInMB())
     }
 
     def 'Delete 100 list elements'() {
@@ -86,7 +86,7 @@ class DeletePerfTest extends CpsPerfTestBase {
             resourceMeter.stop()
             def deleteDurationInSeconds = resourceMeter.getTotalTimeInSeconds()
         then: 'delete duration is within expected time and memory used is within limit'
-            recordAndAssertResourceUsage('Delete 100 lists elements', 2.5, deleteDurationInSeconds, 20, resourceMeter.getTotalMemoryUsageInMB())
+            recordAndAssertResourceUsage('Delete 100 lists elements', 2.1, deleteDurationInSeconds, 20, resourceMeter.getTotalMemoryUsageInMB())
     }
 
     def 'Batch delete 100 list elements'() {
@@ -100,7 +100,7 @@ class DeletePerfTest extends CpsPerfTestBase {
             resourceMeter.stop()
             def deleteDurationInSeconds = resourceMeter.getTotalTimeInSeconds()
         then: 'delete duration is within expected time and memory used is within limit'
-            recordAndAssertResourceUsage('Batch delete 100 lists elements', 0.6, deleteDurationInSeconds, 2, resourceMeter.getTotalMemoryUsageInMB())
+            recordAndAssertResourceUsage('Batch delete 100 lists elements', 0.7, deleteDurationInSeconds, 2, resourceMeter.getTotalMemoryUsageInMB())
     }
 
     def 'Delete 100 whole lists'() {
@@ -116,7 +116,7 @@ class DeletePerfTest extends CpsPerfTestBase {
             resourceMeter.stop()
             def deleteDurationInSeconds = resourceMeter.getTotalTimeInSeconds()
         then: 'delete duration is within expected time and memory used is within limit'
-            recordAndAssertResourceUsage('Delete 100 whole lists', 6, deleteDurationInSeconds, 20, resourceMeter.getTotalMemoryUsageInMB())
+            recordAndAssertResourceUsage('Delete 100 whole lists', 4, deleteDurationInSeconds, 20, resourceMeter.getTotalMemoryUsageInMB())
     }
 
     def 'Batch delete 100 whole lists'() {
@@ -130,7 +130,7 @@ class DeletePerfTest extends CpsPerfTestBase {
             resourceMeter.stop()
             def deleteDurationInSeconds = resourceMeter.getTotalTimeInSeconds()
         then: 'delete duration is within expected time and memory used is within limit'
-            recordAndAssertResourceUsage('Batch delete 100 whole lists', 5, deleteDurationInSeconds, 3, resourceMeter.getTotalMemoryUsageInMB())
+            recordAndAssertResourceUsage('Batch delete 100 whole lists', 3, deleteDurationInSeconds, 3, resourceMeter.getTotalMemoryUsageInMB())
     }
 
     def 'Delete 1 large data node'() {
@@ -160,7 +160,7 @@ class DeletePerfTest extends CpsPerfTestBase {
             resourceMeter.stop()
             def deleteDurationInSeconds = resourceMeter.getTotalTimeInSeconds()
         then: 'delete duration is within expected time and memory used is within limit'
-            recordAndAssertResourceUsage('Delete data nodes for anchor', 2, deleteDurationInSeconds, 1, resourceMeter.getTotalMemoryUsageInMB())
+            recordAndAssertResourceUsage('Delete data nodes for anchor', 1.9, deleteDurationInSeconds, 1, resourceMeter.getTotalMemoryUsageInMB())
     }
 
     def 'Batch delete 100 non-existing nodes'() {
@@ -174,7 +174,7 @@ class DeletePerfTest extends CpsPerfTestBase {
             resourceMeter.stop()
             def deleteDurationInSeconds = resourceMeter.getTotalTimeInSeconds()
         then: 'delete duration is within expected time and memory used is within limit'
-            recordAndAssertResourceUsage('Batch delete 100 non-existing', 7, deleteDurationInSeconds, 3, resourceMeter.getTotalMemoryUsageInMB())
+            recordAndAssertResourceUsage('Batch delete 100 non-existing', 1, deleteDurationInSeconds, 3, resourceMeter.getTotalMemoryUsageInMB())
     }
 
     def 'Clean up test data'() {
index 8a228a3..cb7680d 100644 (file)
@@ -44,9 +44,9 @@ class GetPerfTest extends CpsPerfTestBase {
             recordAndAssertResourceUsage("Read datatrees with ${scenario}", durationLimit, durationInSeconds, memoryLimit, resourceMeter.getTotalMemoryUsageInMB())
         where: 'the following parameters are used'
             scenario             | fetchDescendantsOption  || durationLimit | memoryLimit  | expectedNumberOfDataNodes
-            'no descendants'     | OMIT_DESCENDANTS        || 0.02          | 1            | 1
-            'direct descendants' | DIRECT_CHILDREN_ONLY    || 0.06          | 5            | 1 + OPENROADM_DEVICES_PER_ANCHOR
-            'all descendants'    | INCLUDE_ALL_DESCENDANTS || 2.5           | 250          | 1 + OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE
+            'no descendants'     | OMIT_DESCENDANTS        || 0.01          | 1            | 1
+            'direct descendants' | DIRECT_CHILDREN_ONLY    || 0.05          | 5            | 1 + OPENROADM_DEVICES_PER_ANCHOR
+            'all descendants'    | INCLUDE_ALL_DESCENDANTS || 1.2           | 250          | 1 + OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE
     }
 
     def 'Read data trees for multiple xpaths'() {
@@ -60,7 +60,7 @@ class GetPerfTest extends CpsPerfTestBase {
         then: 'requested nodes and their descendants are returned'
             assert countDataNodesInTree(result) == OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE
         and: 'all data is read within expected time and memory used is within limit'
-            recordAndAssertResourceUsage("Read datatrees for multiple xpaths", 4 , durationInSeconds, 300, resourceMeter.getTotalMemoryUsageInMB())
+            recordAndAssertResourceUsage("Read datatrees for multiple xpaths", 1.8 , durationInSeconds, 300, resourceMeter.getTotalMemoryUsageInMB())
     }
 
     def 'Read for multiple xpaths to non-existing datanodes'() {
@@ -74,7 +74,7 @@ class GetPerfTest extends CpsPerfTestBase {
         then: 'no data is returned'
             assert result.isEmpty()
         and: 'the operation completes within within expected time'
-            recordAndAssertResourceUsage("Read non-existing xpaths", 0.02, durationInSeconds, 2, resourceMeter.getTotalMemoryUsageInMB())
+            recordAndAssertResourceUsage("Read non-existing xpaths", 0.01, durationInSeconds, 2, resourceMeter.getTotalMemoryUsageInMB())
     }
 
     def 'Read complete data trees using #scenario.'() {
@@ -88,9 +88,9 @@ class GetPerfTest extends CpsPerfTestBase {
             recordAndAssertResourceUsage("Read datatrees using ${scenario}", durationLimit, durationInSeconds, memoryLimit, resourceMeter.getTotalMemoryUsageInMB())
         where: 'the following xpaths are used'
             scenario                | xpath                                  || durationLimit  | memoryLimit  | expectedNumberOfDataNodes
-            'openroadm root'        | '/'                                    || 2              | 250          | 1 + OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE
-            'openroadm top element' | '/openroadm-devices'                   || 2              | 250          | 1 + OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE
-            'openroadm whole list'  | '/openroadm-devices/openroadm-device'  || 3              | 250          | OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE
+            'openroadm root'        | '/'                                    || 1.0            | 250          | 1 + OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE
+            'openroadm top element' | '/openroadm-devices'                   || 1.0            | 250          | 1 + OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE
+            'openroadm whole list'  | '/openroadm-devices/openroadm-device'  || 1.7            | 250          | OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE
     }
 
 }
diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/ModuleQueryPerfTest.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/ModuleQueryPerfTest.groovy
new file mode 100644 (file)
index 0000000..add931a
--- /dev/null
@@ -0,0 +1,113 @@
+/*
+ *  ============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.performance.cps
+
+import org.onap.cps.integration.performance.base.CpsPerfTestBase
+import org.onap.cps.spi.model.ModuleReference
+
+class ModuleQueryPerfTest extends CpsPerfTestBase {
+
+    static final KILOBYTE = 1000
+    static final TOTAL_TEST_ANCHORS = 10_000
+    static final SCHEMA_SET_PREFIX = 'mySchemaSet'
+    static final ANCHOR_PREFIX = 'myAnchor'
+    static final MODULE_REVISION = '2024-04-25'
+    static final MODULE_TEMPLATE = """
+        module <MODULE_NAME> {
+            yang-version 1.1;
+            namespace "org:onap:cps:test:<MODULE_NAME>";
+            prefix tree;
+            revision "<MODULE_REVISION>" {
+                description "<DESCRIPTION>";
+            }
+            container tree {
+                list branch {
+                    key "name";
+                    leaf name {
+                        type string;
+                    }
+                }
+            }
+        }
+    """
+
+    def 'Module query - Preload test data (needed for other tests).'() {
+        given: 'a schema set with different sizes of Yang modules is created'
+            cpsModuleService.createSchemaSet(CPS_PERFORMANCE_TEST_DATASPACE, SCHEMA_SET_PREFIX + '0', [
+                    'module0.yang': makeYangModuleOfLength('module0', 1 * KILOBYTE),
+                    'module1.yang': makeYangModuleOfLength('module1', 1000 * KILOBYTE)
+            ])
+        and: 'these modules will be used again to create many schema sets'
+            def allModuleReferences = [
+                    new ModuleReference('module0', MODULE_REVISION),
+                    new ModuleReference('module1', MODULE_REVISION)
+            ]
+        when: 'many schema sets and anchors are created using those modules'
+            resourceMeter.start()
+            (1..TOTAL_TEST_ANCHORS).each {
+                def schemaSetName = SCHEMA_SET_PREFIX + it
+                def anchorName = ANCHOR_PREFIX + it
+                cpsModuleService.createSchemaSetFromModules(CPS_PERFORMANCE_TEST_DATASPACE, schemaSetName, [:], allModuleReferences)
+                cpsAnchorService.createAnchor(CPS_PERFORMANCE_TEST_DATASPACE, schemaSetName, anchorName)
+            }
+            resourceMeter.stop()
+        then: 'operation takes less than expected duration'
+            recordAndAssertResourceUsage('Module query test setup',
+                    45, resourceMeter.totalTimeInSeconds,
+                    500, resourceMeter.totalMemoryUsageInMB
+            )
+    }
+
+    def 'Querying anchors by module name is NOT dependant on the file size of the module.'() {
+        when: 'we search for anchors with given Yang module name'
+            resourceMeter.start()
+            def result = cpsAnchorService.queryAnchorNames(CPS_PERFORMANCE_TEST_DATASPACE, [yangModuleName])
+            resourceMeter.stop()
+        then: 'expected number of anchors is returned'
+            assert result.size() == TOTAL_TEST_ANCHORS
+        and: 'operation completes with expected resource usage'
+            recordAndAssertResourceUsage("Query for anchors with ${scenario}",
+                    expectedTimeInSeconds, resourceMeter.totalTimeInSeconds,
+                    5, resourceMeter.totalMemoryUsageInMB)
+        where: 'the following parameters are used'
+            scenario         | yangModuleName || expectedTimeInSeconds
+            '1 KB module'    | 'module0'      || 0.05
+            '1000 KB module' | 'module1'      || 0.05
+    }
+
+    def 'Module query - Clean up test data.'() {
+        cleanup:
+            // FIXME this API has extremely high memory usage, therefore external batching must be used
+            for (int i = 1; i <= TOTAL_TEST_ANCHORS; i += 100) {
+                cpsModuleService.deleteSchemaSetsWithCascade(CPS_PERFORMANCE_TEST_DATASPACE, (i..i+100).collect {SCHEMA_SET_PREFIX + it})
+            }
+            cpsModuleService.deleteSchemaSetsWithCascade(CPS_PERFORMANCE_TEST_DATASPACE, [SCHEMA_SET_PREFIX + '0'])
+    }
+
+    // This makes a Yang module of approximately target length in bytes by padding the description field with many '*'
+    private static def makeYangModuleOfLength(moduleName, targetLength) {
+        def padding = String.valueOf('*').repeat(targetLength - MODULE_TEMPLATE.size()) // not exact
+        return MODULE_TEMPLATE
+                .replaceAll('<MODULE_NAME>', moduleName)
+                .replaceAll('<MODULE_REVISION>', MODULE_REVISION)
+                .replaceAll('<DESCRIPTION>', padding)
+    }
+}
index 0ae018d..7b9bf62 100644 (file)
@@ -45,11 +45,11 @@ class QueryPerfTest extends CpsPerfTestBase {
             recordAndAssertResourceUsage("Query 1 anchor ${scenario}", durationLimit, durationInSeconds, memoryLimit, resourceMeter.getTotalMemoryUsageInMB())
         where: 'the following parameters are used'
             scenario                     | cpsPath                                                             || durationLimit  | memoryLimit  | expectedNumberOfDataNodes
-            'top element'                | '/openroadm-devices'                                                || 2.5            | 400          | OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE + 1
-            'leaf condition'             | '//openroadm-device[@ne-state="inservice"]'                         || 2.5            | 400          | OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE
-            'ancestors'                  | '//openroadm-device/ancestor::openroadm-devices'                    || 2.5            | 400          | OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE + 1
-            'leaf condition + ancestors' | '//openroadm-device[@status="success"]/ancestor::openroadm-devices' || 2.5            | 400          | OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE + 1
-            'non-existing data'          | '/path/to/non-existing/node[@id="1"]'                               || 0.1            | 1            | 0
+            'top element'                | '/openroadm-devices'                                                || 1.1            | 400          | OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE + 1
+            'leaf condition'             | '//openroadm-device[@ne-state="inservice"]'                         || 1.1            | 400          | OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE
+            'ancestors'                  | '//openroadm-device/ancestor::openroadm-devices'                    || 1.1            | 400          | OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE + 1
+            'leaf condition + ancestors' | '//openroadm-device[@status="success"]/ancestor::openroadm-devices' || 1.1            | 400          | OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE + 1
+            'non-existing data'          | '/path/to/non-existing/node[@id="1"]'                               || 0.009          | 1            | 0
     }
 
     def 'Query complete data trees across all anchors with #scenario.'() {
@@ -64,10 +64,10 @@ class QueryPerfTest extends CpsPerfTestBase {
             recordAndAssertResourceUsage("Query across anchors ${scenario}", durationLimit, durationInSeconds, memoryLimit, resourceMeter.getTotalMemoryUsageInMB())
         where: 'the following parameters are used'
             scenario                     | cpspath                                                             || durationLimit  | memoryLimit   | expectedNumberOfDataNodes
-            'top element'                | '/openroadm-devices'                                                || 7              | 600           | OPENROADM_ANCHORS * (OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE + 1)
-            'leaf condition'             | '//openroadm-device[@ne-state="inservice"]'                         || 7              | 600           | OPENROADM_ANCHORS * (OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE)
-            'ancestors'                  | '//openroadm-device/ancestor::openroadm-devices'                    || 7              | 600           | OPENROADM_ANCHORS * (OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE + 1)
-            'leaf condition + ancestors' | '//openroadm-device[@status="success"]/ancestor::openroadm-devices' || 7              | 600           | OPENROADM_ANCHORS * (OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE + 1)
+            'top element'                | '/openroadm-devices'                                                || 3              | 600           | OPENROADM_ANCHORS * (OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE + 1)
+            'leaf condition'             | '//openroadm-device[@ne-state="inservice"]'                         || 3              | 600           | OPENROADM_ANCHORS * (OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE)
+            'ancestors'                  | '//openroadm-device/ancestor::openroadm-devices'                    || 3              | 600           | OPENROADM_ANCHORS * (OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE + 1)
+            'leaf condition + ancestors' | '//openroadm-device[@status="success"]/ancestor::openroadm-devices' || 3              | 600           | OPENROADM_ANCHORS * (OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE + 1)
     }
 
     def 'Query with leaf condition and #scenario.'() {
@@ -83,8 +83,8 @@ class QueryPerfTest extends CpsPerfTestBase {
         where: 'the following parameters are used'
             scenario             | fetchDescendantsOption  || durationLimit  | memoryLimit   | expectedNumberOfDataNodes
             'no descendants'     | OMIT_DESCENDANTS        || 0.1            | 6             | OPENROADM_DEVICES_PER_ANCHOR
-            'direct descendants' | DIRECT_CHILDREN_ONLY    || 0.2            | 12            | OPENROADM_DEVICES_PER_ANCHOR * 2
-            'all descendants'    | INCLUDE_ALL_DESCENDANTS || 2.5            | 200           | OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE
+            'direct descendants' | DIRECT_CHILDREN_ONLY    || 0.1            | 12            | OPENROADM_DEVICES_PER_ANCHOR * 2
+            'all descendants'    | INCLUDE_ALL_DESCENDANTS || 1.1            | 200           | OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE
     }
 
     def 'Query ancestors with #scenario.'() {
@@ -99,9 +99,9 @@ class QueryPerfTest extends CpsPerfTestBase {
             recordAndAssertResourceUsage("Query ancestors with ${scenario}", durationLimit, durationInSeconds, memoryLimit, resourceMeter.getTotalMemoryUsageInMB())
         where: 'the following parameters are used'
             scenario             | fetchDescendantsOption  || durationLimit  | memoryLimit | expectedNumberOfDataNodes
-            'no descendants'     | OMIT_DESCENDANTS        || 0.           | 3           | 1
-            'direct descendants' | DIRECT_CHILDREN_ONLY    || 0.2            | 8           | 1 + OPENROADM_DEVICES_PER_ANCHOR
-            'all descendants'    | INCLUDE_ALL_DESCENDANTS || 2.5            | 400         | 1 + OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE
+            'no descendants'     | OMIT_DESCENDANTS        || 0.08           | 3           | 1
+            'direct descendants' | DIRECT_CHILDREN_ONLY    || 0.1            | 8           | 1 + OPENROADM_DEVICES_PER_ANCHOR
+            'all descendants'    | INCLUDE_ALL_DESCENDANTS || 1.1            | 400         | 1 + OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE
     }
 
 }
index 69f6477..360feca 100644 (file)
@@ -78,13 +78,13 @@ class UpdatePerfTest extends CpsPerfTestBase {
                     timeLimit, resourceMeter.getTotalTimeInSeconds(),
                     memoryLimit, resourceMeter.getTotalMemoryUsageInMB())
         where:
-            scenario                           | totalNodes | startId | changeLeaves || timeLimit | memoryLimit
-            'Replace 0 nodes with 100'         | 100        | 1       | false        ||       2.5 | 200
-            'Replace 100 using same data'      | 100        | 1       | false        ||       3.0 | 200
-            'Replace 100 with new leaf values' | 100        | 1       | true         ||       3.0 | 200
-            'Replace 100 with 100 new nodes'   | 100        | 101     | false        ||       6.0 | 200
-            'Replace 50 existing and 50 new'   | 100        | 151     | true         ||       4.5 | 200
-            'Replace 100 nodes with 0'         | 0          | 1       | false        ||       3.0 | 200
+            scenario                           | totalNodes | startId | changeLeaves || timeLimit  | memoryLimit
+            'Replace 0 nodes with 100'         | 100        | 1       | false        ||       3.2  | 200
+            'Replace 100 using same data'      | 100        | 1       | false        ||       5.6  | 200
+            'Replace 100 with new leaf values' | 100        | 1       | true         ||       5.5  | 200
+            'Replace 100 with 100 new nodes'   | 100        | 101     | false        ||       10.0 | 200
+            'Replace 50 existing and 50 new'   | 100        | 151     | true         ||       8.0  | 200
+            'Replace 100 nodes with 0'         | 0          | 1       | false        ||       7.0  | 200
     }
 
     def 'Replace list content: #scenario.'() {
@@ -105,12 +105,12 @@ class UpdatePerfTest extends CpsPerfTestBase {
                     memoryLimit, resourceMeter.getTotalMemoryUsageInMB())
         where:
             scenario                                   | totalNodes | startId | changeLeaves || timeLimit | memoryLimit
-            'Replace list of 0 with 100'               | 100        | 1       | false        ||       2.5 | 200
-            'Replace list of 100 using same data'      | 100        | 1       | false        ||       3.0 | 200
-            'Replace list of 100 with new leaf values' | 100        | 1       | true         ||       3.0 | 200
-            'Replace list with 100 new nodes'          | 100        | 101     | false        ||       6.0 | 200
-            'Replace list with 50 existing and 50 new' | 100        | 151     | true         ||       4.5 | 200
-            'Replace list of 100 nodes with 1'         | 1          | 1       | false        ||       3.0 | 200
+            'Replace list of 0 with 100'               | 100        | 1       | false        ||       3.0 | 200
+            'Replace list of 100 using same data'      | 100        | 1       | false        ||       5.4 | 200
+            'Replace list of 100 with new leaf values' | 100        | 1       | true         ||       5.6 | 200
+            'Replace list with 100 new nodes'          | 100        | 101     | false        ||       9.9 | 200
+            'Replace list with 50 existing and 50 new' | 100        | 151     | true         ||       8.0 | 200
+            'Replace list of 100 nodes with 1'         | 1          | 1       | false        ||       7.0 | 200
     }
 
     def 'Update leaves for 100 data nodes.'() {
@@ -127,7 +127,7 @@ class UpdatePerfTest extends CpsPerfTestBase {
             assert 100 == countDataNodes('/openroadm-devices/openroadm-device[@status="fail"]')
         and: 'update completes within expected time and memory used is within limit'
             recordAndAssertResourceUsage('Update leaves for 100 data nodes',
-                    0.4, resourceMeter.getTotalTimeInSeconds(),
+                    0.3, resourceMeter.getTotalTimeInSeconds(),
                     120, resourceMeter.getTotalMemoryUsageInMB())
     }
 
index 96f85ff..c3dd2af 100644 (file)
@@ -44,10 +44,10 @@ class WritePerfTest extends CpsPerfTestBase {
             cpsAnchorService.deleteAnchor(CPS_PERFORMANCE_TEST_DATASPACE, WRITE_TEST_ANCHOR)
         where:
             totalNodes || expectedDuration | memoryLimit
-            50         || 2                | 100
-            100        || 4                | 200
-            200        || 7                | 400
-            400        || 14               | 500
+            50         || 1.6              | 100
+            100        || 3.3              | 200
+            200        || 6.8              | 400
+            400        || 13.0             | 500
     }
 
     def 'Writing bookstore data has exponential time.'() {
@@ -69,10 +69,10 @@ class WritePerfTest extends CpsPerfTestBase {
             cpsAnchorService.deleteAnchor(CPS_PERFORMANCE_TEST_DATASPACE, WRITE_TEST_ANCHOR)
         where:
             totalBooks || expectedDuration | memoryLimit
-            800        || 0.5              | 50
-            1600       || 1.5              | 100
-            3200       || 6.0              | 150
-            6400       || 18.0             | 200
+            800        || 0.3              | 50
+            1600       || 0.8              | 100
+            3200       || 2.6              | 150
+            6400       || 6.7              | 200
     }
 
     def 'Writing openroadm list data using saveListElements.'() {
@@ -97,10 +97,10 @@ class WritePerfTest extends CpsPerfTestBase {
             cpsAnchorService.deleteAnchor(CPS_PERFORMANCE_TEST_DATASPACE, WRITE_TEST_ANCHOR)
         where:
             totalNodes || expectedDuration | memoryLimit
-            50         || 2                | 100
-            100        || 4                | 200
-            200        || 7                | 400
-            400        || 14               | 500
+            50         || 1.5              | 100
+            100        || 3.0              | 200
+            200        || 6.3              | 400
+            400        || 14.0             | 500
     }
 
 }
index 579394b..53b2194 100644 (file)
@@ -95,7 +95,7 @@ class CmDataSubscriptionsPerfTest extends NcmpPerfTestBase {
             def resultAfter = objectUnderTest.queryDataNodes(NCMP_PERFORMANCE_TEST_DATASPACE, CM_DATA_SUBSCRIPTIONS_ANCHOR, cpsPath, INCLUDE_ALL_DESCENDANTS)
             assert resultAfter.collect {it.leaves.subscribers.size()}.sum() == totalNumberOfEntries * (1 + numberOfCmDataSubscribers)
         and: 'update matching subscription within 15 seconds'
-            recordAndAssertResourceUsage("Update matching subscription", 15, durationInSeconds, 1000, resourceMeter.getTotalMemoryUsageInMB())
+            recordAndAssertResourceUsage("Update matching subscription", 11, durationInSeconds, 1000, resourceMeter.getTotalMemoryUsageInMB())
     }
 
     def 'Worst case new subscription (200x10 new entries).'() {
index d95ac73..91b28f9 100644 (file)
 
 package org.onap.cps.integration.performance.ncmp
 
-import org.apache.commons.lang3.StringUtils
-import org.onap.cps.ncmp.api.impl.inventory.CmHandleState
-import org.onap.cps.ncmp.api.impl.inventory.sync.ModuleSyncService
-import org.onap.cps.ncmp.api.impl.utils.YangDataConverter
-import org.onap.cps.spi.FetchDescendantsOption
-import org.onap.cps.spi.model.DataNode
-import org.springframework.beans.factory.annotation.Autowired
 
-import java.util.stream.Collectors
 import org.onap.cps.api.CpsQueryService
 import org.onap.cps.integration.ResourceMeter
 import org.onap.cps.integration.performance.base.NcmpPerfTestBase
 
-import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
+import java.util.stream.Collectors
+
 import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
+import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
 
 class CmHandleQueryPerfTest extends NcmpPerfTestBase {
 
@@ -74,7 +68,7 @@ class CmHandleQueryPerfTest extends NcmpPerfTestBase {
             resourceMeter.stop()
             def durationInSeconds = resourceMeter.getTotalTimeInSeconds()
         then: 'the required operations are performed within required time'
-            recordAndAssertResourceUsage("CpsPath Registry attributes Query", 2, durationInSeconds, 300, resourceMeter.getTotalMemoryUsageInMB())
+            recordAndAssertResourceUsage("CpsPath Registry attributes Query", 3.4, durationInSeconds, 300, resourceMeter.getTotalMemoryUsageInMB())
         and: 'all nodes are returned'
             result.size() == TOTAL_CM_HANDLES
         and: 'the tree contains all the expected descendants too'
@@ -98,7 +92,7 @@ class CmHandleQueryPerfTest extends NcmpPerfTestBase {
                     expectedAverageResponseTime, averageResponseTime,
                     15, resourceMeter.totalMemoryUsageInMB)
         where:
-            expectedAverageResponseTime = 1 * MILLISECONDS
+            expectedAverageResponseTime = 6 * MILLISECONDS
     }
 
     def 'CM-handle is looked up by alternate-id.'() {
@@ -118,7 +112,7 @@ class CmHandleQueryPerfTest extends NcmpPerfTestBase {
                     expectedAverageResponseTime, averageResponseTime,
                     15, resourceMeter.totalMemoryUsageInMB)
         where:
-            expectedAverageResponseTime = 10 * MILLISECONDS
+            expectedAverageResponseTime = 20 * MILLISECONDS
     }
 
     def 'A batch of CM-handles is looked up by alternate-id.'() {
@@ -157,7 +151,7 @@ class CmHandleQueryPerfTest extends NcmpPerfTestBase {
                     expectedAverageResponseTime, averageResponseTime,
                     500, resourceMeter.totalMemoryUsageInMB)
         where:
-            expectedAverageResponseTime = 100 * MILLISECONDS
+            expectedAverageResponseTime = 360 * MILLISECONDS
     }
 
 }
index 3d61bdb..6fd3bca 100644 (file)
@@ -48,7 +48,7 @@ spring:
       minimumIdle: 5
       maximumPoolSize: 80
       idleTimeout: 60000
-      connectionTimeout: 120000
+      connectionTimeout: 30000
       leakDetectionThreshold: 30000
       pool-name: CpsDatabasePool
 
@@ -120,7 +120,7 @@ notification:
       queue-capacity: 500
       wait-for-tasks-to-complete-on-shutdown: true
       thread-name-prefix: Async-
-      time-out-value-in-ms: 2000
+      time-out-value-in-ms: 60000
 
 springdoc:
   swagger-ui:
@@ -165,7 +165,7 @@ logging:
 ncmp:
   dmi:
     httpclient:
-      connectionTimeoutInSeconds: 180
+      connectionTimeoutInSeconds: 30
       maximumConnectionsPerRoute: 50
       maximumConnectionsTotal: 100
       idleConnectionEvictionThresholdInSeconds: 5
diff --git a/integration-test/src/test/resources/data/mock-dmi-responses/bookStoreAWithModules_M1_M2_ResourcesResponse.json b/integration-test/src/test/resources/data/mock-dmi-responses/bookStoreAWithModules_M1_M2_ResourcesResponse.json
deleted file mode 100644 (file)
index 5d71391..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-[
-       {
-               "moduleName": "M1",
-               "revision": "2024-01-01",
-               "yangSource": "module M1 {\n\n  namespace \"urn:ietf:params:xml:ns:yang:M1\";\n  prefix \"yang\";\n\n  organization\n   \"IETF NETMOD (NETCONF Data Modeling Language) Working Group\";\n\n  contact\n   \"WG Web:   <http://tools.ietf.org/wg/netmod/>\n    WG List:  <mailto:netmod@ietf.org>\n\n    WG Chair: David Kessens\n              <mailto:david.kessens@nsn.com>\n\n    WG Chair: Juergen Schoenwaelder\n              <mailto:j.schoenwaelder@jacobs-university.de>\n\n    Editor:   Juergen Schoenwaelder\n              <mailto:j.schoenwaelder@jacobs-university.de>\";\n\n  description\n   \"This module contains a collection of generally useful derived\n    YANG data types.\n\n    Copyright (c) 2013 IETF Trust and the persons identified as\n    authors of the code.  All rights reserved.\n\n    Redistribution and use in source and binary forms, with or\n    without modification, is permitted pursuant to, and subject\n    to the license terms contained in, the Simplified BSD License\n    set forth in Section 4.c of the IETF Trust's Legal Provisions\n    Relating to IETF Documents\n    (http://trustee.ietf.org/license-info).\n\n    This version of this YANG module is part of RFC 6991; see\n    the RFC itself for full legal notices.\";\n\n  revision 2024-01-01 {\n    description\n     \"This revision adds the following new data types:\n      - yang-identifier\n      - hex-string\n      - uuid\n      - dotted-quad\";\n    reference\n     \"RFC 6991: Common YANG Data Types\";\n  }\n\n  revision 2010-09-24 {\n    description\n     \"Initial revision.\";\n    reference\n     \"RFC 6021: Common YANG Data Types\";\n  }\n\n  /*** collection of counter and gauge types ***/\n\n  typedef counter32 {\n    type uint32;\n    description\n     \"The counter32 type represents a non-negative integer\n      that monotonically increases until it reaches a\n      maximum value of 2^32-1 (4294967295 decimal), when it\n      wraps around and starts increasing again from zero.\n\n      Counters have no defined 'initial' value, and thus, a\n      single value of a counter has (in general) no information\n      content.  Discontinuities in the monotonically increasing\n      value normally occur at re-initialization of the\n      management system, and at other times as specified in the\n      description of a schema node using this type.  If such\n      other times can occur, for example, the creation of\n      a schema node of type counter32 at times other than\n      re-initialization, then a corresponding schema node\n      should be defined, with an appropriate type, to indicate\n      the last discontinuity.\n\n      The counter32 type should not be used for configuration\n      schema nodes.  A default statement SHOULD NOT be used in\n      combination with the type counter32.\n\n      In the value set and its semantics, this type is equivalent\n      to the Counter32 type of the SMIv2.\";\n    reference\n     \"RFC 2578: Structure of Management Information Version 2\n                (SMIv2)\";\n  }\n}\n"
-       },
-       {
-               "moduleName": "M2",
-               "revision": "2024-01-02",
-               "yangSource": "module M2 {\n\n  namespace \"urn:ietf:params:xml:ns:yang:M2\";\n  prefix \"yang\";\n\n  organization\n   \"IETF NETMOD (NETCONF Data Modeling Language) Working Group\";\n\n  contact\n   \"WG Web:   <http://tools.ietf.org/wg/netmod/>\n    WG List:  <mailto:netmod@ietf.org>\n\n    WG Chair: David Kessens\n              <mailto:david.kessens@nsn.com>\n\n    WG Chair: Juergen Schoenwaelder\n              <mailto:j.schoenwaelder@jacobs-university.de>\n\n    Editor:   Juergen Schoenwaelder\n              <mailto:j.schoenwaelder@jacobs-university.de>\";\n\n  description\n   \"This module contains a collection of generally useful derived\n    YANG data types.\n\n    Copyright (c) 2013 IETF Trust and the persons identified as\n    authors of the code.  All rights reserved.\n\n    Redistribution and use in source and binary forms, with or\n    without modification, is permitted pursuant to, and subject\n    to the license terms contained in, the Simplified BSD License\n    set forth in Section 4.c of the IETF Trust's Legal Provisions\n    Relating to IETF Documents\n    (http://trustee.ietf.org/license-info).\n\n    This version of this YANG module is part of RFC 6991; see\n    the RFC itself for full legal notices.\";\n\n  revision 2024-01-02 {\n    description\n     \"This revision adds the following new data types:\n      - yang-identifier\n      - hex-string\n      - uuid\n      - dotted-quad\";\n    reference\n     \"RFC 6991: Common YANG Data Types\";\n  }\n\n  revision 2010-09-24 {\n    description\n     \"Initial revision.\";\n    reference\n     \"RFC 6021: Common YANG Data Types\";\n  }\n\n  /*** collection of counter and gauge types ***/\n\n  typedef counter32 {\n    type uint32;\n    description\n     \"The counter32 type represents a non-negative integer\n      that monotonically increases until it reaches a\n      maximum value of 2^32-1 (4294967295 decimal), when it\n      wraps around and starts increasing again from zero.\n\n      Counters have no defined 'initial' value, and thus, a\n      single value of a counter has (in general) no information\n      content.  Discontinuities in the monotonically increasing\n      value normally occur at re-initialization of the\n      management system, and at other times as specified in the\n      description of a schema node using this type.  If such\n      other times can occur, for example, the creation of\n      a schema node of type counter32 at times other than\n      re-initialization, then a corresponding schema node\n      should be defined, with an appropriate type, to indicate\n      the last discontinuity.\n\n      The counter32 type should not be used for configuration\n      schema nodes.  A default statement SHOULD NOT be used in\n      combination with the type counter32.\n\n      In the value set and its semantics, this type is equivalent\n      to the Counter32 type of the SMIv2.\";\n    reference\n     \"RFC 2578: Structure of Management Information Version 2\n                (SMIv2)\";\n  }\n}\n"
-       }
-]
\ No newline at end of file
diff --git a/integration-test/src/test/resources/data/mock-dmi-responses/bookStoreAWithModules_M1_M2_Response.json b/integration-test/src/test/resources/data/mock-dmi-responses/bookStoreAWithModules_M1_M2_Response.json
deleted file mode 100644 (file)
index 9f20564..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-{
-       "schemas": [
-               {
-                       "moduleName": "M1",
-                       "revision": "2024-01-01"
-               },
-               {
-                       "moduleName": "M2",
-                       "revision": "2024-01-02"
-               }
-       ]
-}
\ No newline at end of file
diff --git a/integration-test/src/test/resources/data/mock-dmi-responses/bookStoreBWithModules_M1_M3_ResourcesResponse.json b/integration-test/src/test/resources/data/mock-dmi-responses/bookStoreBWithModules_M1_M3_ResourcesResponse.json
deleted file mode 100644 (file)
index ef9b85f..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-[
-       {
-               "moduleName": "M1",
-               "revision": "2024-01-01",
-               "yangSource": "module M1 {\n\n  namespace \"urn:ietf:params:xml:ns:yang:M1\";\n  prefix \"yang\";\n\n  organization\n   \"IETF NETMOD (NETCONF Data Modeling Language) Working Group\";\n\n  contact\n   \"WG Web:   <http://tools.ietf.org/wg/netmod/>\n    WG List:  <mailto:netmod@ietf.org>\n\n    WG Chair: David Kessens\n              <mailto:david.kessens@nsn.com>\n\n    WG Chair: Juergen Schoenwaelder\n              <mailto:j.schoenwaelder@jacobs-university.de>\n\n    Editor:   Juergen Schoenwaelder\n              <mailto:j.schoenwaelder@jacobs-university.de>\";\n\n  description\n   \"This module contains a collection of generally useful derived\n    YANG data types.\n\n    Copyright (c) 2013 IETF Trust and the persons identified as\n    authors of the code.  All rights reserved.\n\n    Redistribution and use in source and binary forms, with or\n    without modification, is permitted pursuant to, and subject\n    to the license terms contained in, the Simplified BSD License\n    set forth in Section 4.c of the IETF Trust's Legal Provisions\n    Relating to IETF Documents\n    (http://trustee.ietf.org/license-info).\n\n    This version of this YANG module is part of RFC 6991; see\n    the RFC itself for full legal notices.\";\n\n  revision 2024-01-01 {\n    description\n     \"This revision adds the following new data types:\n      - yang-identifier\n      - hex-string\n      - uuid\n      - dotted-quad\";\n    reference\n     \"RFC 6991: Common YANG Data Types\";\n  }\n\n  revision 2010-09-24 {\n    description\n     \"Initial revision.\";\n    reference\n     \"RFC 6021: Common YANG Data Types\";\n  }\n\n  /*** collection of counter and gauge types ***/\n\n  typedef counter32 {\n    type uint32;\n    description\n     \"The counter32 type represents a non-negative integer\n      that monotonically increases until it reaches a\n      maximum value of 2^32-1 (4294967295 decimal), when it\n      wraps around and starts increasing again from zero.\n\n      Counters have no defined 'initial' value, and thus, a\n      single value of a counter has (in general) no information\n      content.  Discontinuities in the monotonically increasing\n      value normally occur at re-initialization of the\n      management system, and at other times as specified in the\n      description of a schema node using this type.  If such\n      other times can occur, for example, the creation of\n      a schema node of type counter32 at times other than\n      re-initialization, then a corresponding schema node\n      should be defined, with an appropriate type, to indicate\n      the last discontinuity.\n\n      The counter32 type should not be used for configuration\n      schema nodes.  A default statement SHOULD NOT be used in\n      combination with the type counter32.\n\n      In the value set and its semantics, this type is equivalent\n      to the Counter32 type of the SMIv2.\";\n    reference\n     \"RFC 2578: Structure of Management Information Version 2\n                (SMIv2)\";\n  }\n}\n"
-       },
-       {
-               "moduleName": "M3",
-               "revision": "2024-01-03",
-               "yangSource": "module M3 {\n\n  namespace \"urn:ietf:params:xml:ns:yang:M3\";\n  prefix \"yang\";\n\n  organization\n   \"IETF NETMOD (NETCONF Data Modeling Language) Working Group\";\n\n  contact\n   \"WG Web:   <http://tools.ietf.org/wg/netmod/>\n    WG List:  <mailto:netmod@ietf.org>\n\n    WG Chair: David Kessens\n              <mailto:david.kessens@nsn.com>\n\n    WG Chair: Juergen Schoenwaelder\n              <mailto:j.schoenwaelder@jacobs-university.de>\n\n    Editor:   Juergen Schoenwaelder\n              <mailto:j.schoenwaelder@jacobs-university.de>\";\n\n  description\n   \"This module contains a collection of generally useful derived\n    YANG data types.\n\n    Copyright (c) 2013 IETF Trust and the persons identified as\n    authors of the code.  All rights reserved.\n\n    Redistribution and use in source and binary forms, with or\n    without modification, is permitted pursuant to, and subject\n    to the license terms contained in, the Simplified BSD License\n    set forth in Section 4.c of the IETF Trust's Legal Provisions\n    Relating to IETF Documents\n    (http://trustee.ietf.org/license-info).\n\n    This version of this YANG module is part of RFC 6991; see\n    the RFC itself for full legal notices.\";\n\n  revision 2024-01-03 {\n    description\n     \"This revision adds the following new data types:\n      - yang-identifier\n      - hex-string\n      - uuid\n      - dotted-quad\";\n    reference\n     \"RFC 6991: Common YANG Data Types\";\n  }\n\n  revision 2010-09-24 {\n    description\n     \"Initial revision.\";\n    reference\n     \"RFC 6021: Common YANG Data Types\";\n  }\n\n  /*** collection of counter and gauge types ***/\n\n  typedef counter32 {\n    type uint32;\n    description\n     \"The counter32 type represents a non-negative integer\n      that monotonically increases until it reaches a\n      maximum value of 2^32-1 (4294967295 decimal), when it\n      wraps around and starts increasing again from zero.\n\n      Counters have no defined 'initial' value, and thus, a\n      single value of a counter has (in general) no information\n      content.  Discontinuities in the monotonically increasing\n      value normally occur at re-initialization of the\n      management system, and at other times as specified in the\n      description of a schema node using this type.  If such\n      other times can occur, for example, the creation of\n      a schema node of type counter32 at times other than\n      re-initialization, then a corresponding schema node\n      should be defined, with an appropriate type, to indicate\n      the last discontinuity.\n\n      The counter32 type should not be used for configuration\n      schema nodes.  A default statement SHOULD NOT be used in\n      combination with the type counter32.\n\n      In the value set and its semantics, this type is equivalent\n      to the Counter32 type of the SMIv2.\";\n    reference\n     \"RFC 2578: Structure of Management Information Version 2\n                (SMIv2)\";\n  }\n}\n"
-       }
-]
\ No newline at end of file
diff --git a/integration-test/src/test/resources/data/mock-dmi-responses/bookStoreBWithModules_M1_M3_Response.json b/integration-test/src/test/resources/data/mock-dmi-responses/bookStoreBWithModules_M1_M3_Response.json
deleted file mode 100644 (file)
index 513c749..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-{
-       "schemas": [
-               {
-                       "moduleName": "M1",
-                       "revision": "2024-01-01"
-               },
-               {
-                       "moduleName": "M3",
-                       "revision": "2024-01-03"
-               }
-       ]
-}
\ No newline at end of file
diff --git a/integration-test/src/test/resources/data/mock-dmi-responses/moduleReferencesTemplate.json b/integration-test/src/test/resources/data/mock-dmi-responses/moduleReferencesTemplate.json
new file mode 100644 (file)
index 0000000..2551ad4
--- /dev/null
@@ -0,0 +1,4 @@
+{
+       "moduleName": "<MODULE_NAME>",
+       "revision": "2024-01-01"
+}
diff --git a/integration-test/src/test/resources/data/mock-dmi-responses/moduleResourcesTemplate.json b/integration-test/src/test/resources/data/mock-dmi-responses/moduleResourcesTemplate.json
new file mode 100644 (file)
index 0000000..1e189f1
--- /dev/null
@@ -0,0 +1,5 @@
+{
+  "moduleName": "<MODULE_NAME>",
+  "revision": "2024-01-01",
+  "yangSource": "module <MODULE_NAME> {\n\n  namespace \"urn:ietf:params:xml:ns:yang:<MODULE_NAME>\";\n  prefix \"yang\";\n\n  organization\n   \"IETF NETMOD (NETCONF Data Modeling Language) Working Group\";\n\n  contact\n   \"WG Web:   <http://tools.ietf.org/wg/netmod/>\n    WG List:  <mailto:netmod@ietf.org>\n\n    WG Chair: David Kessens\n              <mailto:david.kessens@nsn.com>\n\n    WG Chair: Juergen Schoenwaelder\n              <mailto:j.schoenwaelder@jacobs-university.de>\n\n    Editor:   Juergen Schoenwaelder\n              <mailto:j.schoenwaelder@jacobs-university.de>\";\n\n  description\n   \"This module contains a collection of generally useful derived\n    YANG data types.\n\n    Copyright (c) 2013 IETF Trust and the persons identified as\n    authors of the code.  All rights reserved.\n\n    Redistribution and use in source and binary forms, with or\n    without modification, is permitted pursuant to, and subject\n    to the license terms contained in, the Simplified BSD License\n    set forth in Section 4.c of the IETF Trust's Legal Provisions\n    Relating to IETF Documents\n    (http://trustee.ietf.org/license-info).\n\n    This version of this YANG module is part of RFC 6991; see\n    the RFC itself for full legal notices.\";\n\n  revision 2024-01-01 {\n    description\n     \"This revision adds the following new data types:\n      - yang-identifier\n      - hex-string\n      - uuid\n      - dotted-quad\";\n    reference\n     \"RFC 6991: Common YANG Data Types\";\n  }\n\n  revision 2010-09-24 {\n    description\n     \"Initial revision.\";\n    reference\n     \"RFC 6021: Common YANG Data Types\";\n  }\n\n  /*** collection of counter and gauge types ***/\n\n  typedef counter32 {\n    type uint32;\n    description\n     \"The counter32 type represents a non-negative integer\n      that monotonically increases until it reaches a\n      maximum value of 2^32-1 (4294967295 decimal), when it\n      wraps around and starts increasing again from zero.\n\n      Counters have no defined 'initial' value, and thus, a\n      single value of a counter has (in general) no information\n      content.  Discontinuities in the monotonically increasing\n      value normally occur at re-initialization of the\n      management system, and at other times as specified in the\n      description of a schema node using this type.  If such\n      other times can occur, for example, the creation of\n      a schema node of type counter32 at times other than\n      re-initialization, then a corresponding schema node\n      should be defined, with an appropriate type, to indicate\n      the last discontinuity.\n\n      The counter32 type should not be used for configuration\n      schema nodes.  A default statement SHOULD NOT be used in\n      combination with the type counter32.\n\n      In the value set and its semantics, this type is equivalent\n      to the Counter32 type of the SMIv2.\";\n    reference\n     \"RFC 2578: Structure of Management Information Version 2\n                (SMIv2)\";\n  }\n}\n"
+}
index 9919e8c..585d45b 100644 (file)
@@ -5,7 +5,7 @@
     <parent>
         <groupId>org.onap.cps</groupId>
         <artifactId>cps-parent</artifactId>
-        <version>3.4.8-SNAPSHOT</version>
+        <version>3.4.9-SNAPSHOT</version>
         <relativePath>../cps-parent/pom.xml</relativePath>
     </parent>
     <modelVersion>4.0.0</modelVersion>
diff --git a/pom.xml b/pom.xml
index df89d96..767c230 100644 (file)
--- a/pom.xml
+++ b/pom.xml
@@ -32,7 +32,7 @@
 \r
     <groupId>org.onap.cps</groupId>\r
     <artifactId>cps-aggregator</artifactId>\r
-    <version>3.4.8-SNAPSHOT</version>\r
+    <version>3.4.9-SNAPSHOT</version>\r
     <packaging>pom</packaging>\r
 \r
     <name>cps</name>\r
diff --git a/releases/3.4.8-container.yaml b/releases/3.4.8-container.yaml
new file mode 100644 (file)
index 0000000..c5cccb4
--- /dev/null
@@ -0,0 +1,8 @@
+distribution_type: container
+container_release_tag: 3.4.8
+project: cps
+log_dir: cps-maven-docker-stage-master/940/
+ref: ad46c250eebd2eec9a99991371f825c778336182
+containers:
+  - name: 'cps-and-ncmp'
+    version: '3.4.8-20240501T114419Z'
diff --git a/releases/3.4.8.yaml b/releases/3.4.8.yaml
new file mode 100644 (file)
index 0000000..a711ff7
--- /dev/null
@@ -0,0 +1,4 @@
+distribution_type: maven
+log_dir: cps-maven-stage-master/948/
+project: cps
+version: 3.4.8
index 02c6079..7c93d55 100644 (file)
@@ -25,7 +25,7 @@
     <modelVersion>4.0.0</modelVersion>
     <groupId>org.onap.cps</groupId>
     <artifactId>spotbugs</artifactId>
-    <version>3.4.8-SNAPSHOT</version>
+    <version>3.4.9-SNAPSHOT</version>
 
     <properties>
         <nexusproxy>https://nexus.onap.org</nexusproxy>
index 767a534..edba7cf 100644 (file)
@@ -22,7 +22,7 @@
 
 major=3
 minor=4
-patch=8
+patch=9
 
 base_version=${major}.${minor}.${patch}