Merge "Add write performance tests"
authorToine Siebelink <toine.siebelink@est.tech>
Thu, 27 Jul 2023 15:14:02 +0000 (15:14 +0000)
committerGerrit Code Review <gerrit@onap.org>
Thu, 27 Jul 2023 15:14:02 +0000 (15:14 +0000)
52 files changed:
cps-application/src/main/resources/application.yml
cps-ncmp-events/src/main/resources/schemas/avc-subscription-event-outcome-v1.json [deleted file]
cps-ncmp-events/src/main/resources/schemas/avc-subscription-event-v1.json [deleted file]
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/NcmpEventResponseCode.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avcsubscription/ResponseTimeoutTask.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventConsumer.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventForwarder.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventMapper.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventResponseConsumer.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventResponseMapper.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventResponseOutcome.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionOutcomeMapper.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/subscriptions/SubscriptionPersistenceImpl.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/subscriptions/SubscriptionStatus.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/DataNodeHelper.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/SubscriptionEventCloudMapper.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/SubscriptionEventResponseCloudMapper.java [new file with mode: 0644]
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/SubscriptionOutcomeCloudMapper.java [new file with mode: 0644]
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/yangmodels/YangModelSubscriptionEvent.java
cps-ncmp-service/src/main/resources/model/subscription.yang
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventConsumerSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventForwarderSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventResponseConsumerSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventResponseMapperSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventResponseOutcomeSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionOutcomeMapperSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/subscriptions/SubscriptionPersistenceSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/DataNodeBaseSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/DataNodeHelperSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/SubscriptionEventCloudMapperSpec.groovy
cps-ncmp-service/src/test/resources/application.yml
cps-ncmp-service/src/test/resources/avcSubscriptionEventResponse.json
cps-ncmp-service/src/test/resources/avcSubscriptionOutcomeEvent.json
cps-ncmp-service/src/test/resources/avcSubscriptionOutcomeEvent2.json [new file with mode: 0644]
cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java
cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepository.java
cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy
cps-service/src/main/java/org/onap/cps/spi/exceptions/CloudEventConstructionException.java [new file with mode: 0644]
cps-service/src/main/java/org/onap/cps/spi/exceptions/SubscriptionOutcomeTypeNotFoundException.java [new file with mode: 0644]
docs/cm-handle-lcm-events.rst [new file with mode: 0644]
docs/cps-events.rst
docs/cps-ncmp-message-status-codes.rst [new file with mode: 0644]
docs/data-operation-events.rst [new file with mode: 0644]
docs/modeling.rst
docs/ncmp-async-events.rst [new file with mode: 0644]
docs/ncmp-data-operation.rst [new file with mode: 0644]
docs/release-notes.rst
docs/schemas/data-operation-event-schema-1.0.0.json [new file with mode: 0644]
integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsDataServiceIntegrationSpec.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/QueryPerfTest.groovy

index ed71339..47592b4 100644 (file)
@@ -98,10 +98,10 @@ app:
         async-m2m:
             topic: ${NCMP_ASYNC_M2M_TOPIC:ncmp-async-m2m}
         avc:
-            subscription-topic: ${NCMP_CM_AVC_SUBSCRIPTION:cm-avc-subscription}
+            subscription-topic: ${NCMP_CM_AVC_SUBSCRIPTION:subscription}
             subscription-forward-topic-prefix: ${NCMP_FORWARD_CM_AVC_SUBSCRIPTION:ncmp-dmi-cm-avc-subscription-}
             subscription-response-topic: ${NCMP_RESPONSE_CM_AVC_SUBSCRIPTION:dmi-ncmp-cm-avc-subscription}
-            subscription-outcome-topic: ${NCMP_OUTCOME_CM_AVC_SUBSCRIPTION:cm-avc-subscription-response}
+            subscription-outcome-topic: ${NCMP_OUTCOME_CM_AVC_SUBSCRIPTION:subscription-response}
             cm-events-topic: ${NCMP_CM_EVENTS_TOPIC:cm-events}
     lcm:
         events:
diff --git a/cps-ncmp-events/src/main/resources/schemas/avc-subscription-event-outcome-v1.json b/cps-ncmp-events/src/main/resources/schemas/avc-subscription-event-outcome-v1.json
deleted file mode 100644 (file)
index 34970ac..0000000
+++ /dev/null
@@ -1,81 +0,0 @@
-{
-  "$schema": "https://json-schema.org/draft/2019-09/schema",
-  "$id": "urn:cps:org.onap.cps.ncmp.events:avc-subscription-event-outcome:v1",
-  "$ref": "#/definitions/SubscriptionEventOutcome",
-  "definitions": {
-    "SubscriptionEventOutcome": {
-      "description": "The payload for avc subscription event outcome message.",
-      "type": "object",
-      "javaType" : "org.onap.cps.ncmp.events.avc.subscription.v1.SubscriptionEventOutcome",
-      "properties": {
-        "version": {
-          "description": "The outcome event type version",
-          "type": "string"
-        },
-        "eventType": {
-          "description": "The event type",
-          "type": "string",
-          "enum": [
-            "COMPLETE_OUTCOME",
-            "PARTIAL_OUTCOME"
-          ]
-        },
-        "event": {
-          "$ref": "#/definitions/event"
-        }
-      },
-      "required": [
-        "version",
-        "eventType",
-        "event"
-      ]
-    },
-    "event": {
-      "description": "The event content for outcome message.",
-      "type": "object",
-      "javaType": "InnerSubscriptionEventOutcome",
-      "properties": {
-        "subscription": {
-          "description": "The subscription details.",
-          "type": "object",
-          "properties": {
-            "clientID": {
-              "description": "The clientID",
-              "type": "string"
-            },
-            "name": {
-              "description": "The name of the subscription",
-              "type": "string"
-            }
-          },
-          "required": [
-            "clientID",
-            "name"
-          ]
-        },
-        "predicates": {
-          "description": "Additional values to be added into the subscription outcome",
-          "type": "object",
-          "properties": {
-            "rejectedTargets": {
-              "description": "Rejected CM Handles to be responded by the subscription",
-              "type": "array"
-            },
-            "acceptedTargets": {
-              "description": "Accepted CM Handles to be responded by the subscription",
-              "type": "array"
-            },
-            "pendingTargets": {
-              "description": "Pending CM Handles to be responded by the subscription",
-              "type": "array"
-            }
-          }
-        }
-      },
-      "required": [
-        "subscription",
-        "predicates"
-      ]
-    }
-  }
-}
\ No newline at end of file
diff --git a/cps-ncmp-events/src/main/resources/schemas/avc-subscription-event-v1.json b/cps-ncmp-events/src/main/resources/schemas/avc-subscription-event-v1.json
deleted file mode 100644 (file)
index feff48c..0000000
+++ /dev/null
@@ -1,116 +0,0 @@
-{
-  "$schema": "https://json-schema.org/draft/2019-09/schema",
-  "$id": "urn:cps:org.onap.cps.ncmp.events:avc-subscription-event:v1",
-  "$ref": "#/definitions/SubscriptionEvent",
-  "definitions": {
-    "SubscriptionEvent": {
-      "description": "The payload for avc subscription event.",
-      "type": "object",
-      "properties": {
-        "version": {
-          "description": "The event type version",
-          "type": "string"
-        },
-        "eventType": {
-          "description": "The event type",
-          "type": "string",
-          "enum": ["CREATE"]
-        },
-        "event": {
-          "$ref": "#/definitions/event"
-        }
-      },
-      "required": [
-        "version",
-        "eventContent"
-      ],
-      "additionalProperties": false
-    },
-    "event": {
-      "description": "The event content.",
-      "type": "object",
-      "javaType": "InnerSubscriptionEvent",
-      "properties": {
-        "subscription": {
-          "description": "The subscription details.",
-          "type": "object",
-          "properties": {
-            "clientID": {
-              "description": "The clientID",
-              "type": "string"
-            },
-            "name": {
-              "description": "The name of the subscription",
-              "type": "string"
-            },
-            "isTagged": {
-              "description": "optional parameter, default is no",
-              "type": "boolean",
-              "default": false
-            }
-          },
-          "required": [
-            "clientID",
-            "name"
-          ]
-        },
-        "dataType": {
-          "description": "The datatype content.",
-          "type": "object",
-          "properties": {
-            "dataspace": {
-              "description": "The dataspace name",
-              "type": "string"
-            },
-            "dataCategory": {
-              "description": "The category type of the data",
-              "type": "string"
-            },
-            "dataProvider": {
-              "description": "The provider name of the data",
-              "type": "string"
-            },
-            "schemaName": {
-              "description": "The name of the schema",
-              "type": "string"
-            },
-            "schemaVersion": {
-              "description": "The version of the schema",
-              "type": "string"
-            }
-          }
-        },
-        "required": [
-          "dataspace",
-          "dataCategory",
-          "dataProvider",
-          "schemaName",
-          "schemaVersion"
-        ],
-        "predicates": {
-          "description": "Additional values to be added into the subscription",
-          "type" : "object",
-          "properties": {
-            "targets": {
-              "description": "CM Handles to be targeted by the subscription",
-              "type" : "array"
-            },
-            "datastore": {
-              "description": "datastore which is to be used by the subscription",
-              "type": "string"
-            },
-            "xpath-filter": {
-              "description": "filter to be applied to the CM Handles through this event",
-              "type": "string"
-            }
-          },
-          "required": ["datastore"]
-        }
-      }
-    },
-    "required": [
-        "subscription",
-        "dataType"
-      ]
-    }
-}
\ No newline at end of file
index d250c36..3b11249 100644 (file)
@@ -26,10 +26,14 @@ import lombok.Getter;
 public enum NcmpEventResponseCode {
 
     SUCCESS("0", "Successfully applied changes"),
+    SUCCESSFULLY_APPLIED_SUBSCRIPTION("1", "successfully applied subscription"),
     CM_HANDLES_NOT_FOUND("100", "cm handle id(s) not found"),
     CM_HANDLES_NOT_READY("101", "cm handle(s) not ready"),
     DMI_SERVICE_NOT_RESPONDING("102", "dmi plugin service is not responding"),
-    UNABLE_TO_READ_RESOURCE_DATA("103", "dmi plugin service is not able to read resource data");
+    UNABLE_TO_READ_RESOURCE_DATA("103", "dmi plugin service is not able to read resource data"),
+    PARTIALLY_APPLIED_SUBSCRIPTION("104", "partially applied subscription"),
+    SUBSCRIPTION_NOT_APPLICABLE("105", "subscription not applicable for all cm handles"),
+    SUBSCRIPTION_PENDING("106", "subscription pending for all cm handles");
 
     private final String statusCode;
     private final String statusMessage;
index c178700..176e644 100644 (file)
@@ -24,6 +24,7 @@ import com.hazelcast.map.IMap;
 import java.util.Set;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.onap.cps.ncmp.events.avcsubscription1_0_0.dmi_to_ncmp.SubscriptionEventResponse;
 
 @Slf4j
 @RequiredArgsConstructor
@@ -31,8 +32,7 @@ public class ResponseTimeoutTask implements Runnable {
 
     private final IMap<String, Set<String>> forwardedSubscriptionEventCache;
     private final SubscriptionEventResponseOutcome subscriptionEventResponseOutcome;
-    private final String subscriptionClientId;
-    private final String subscriptionName;
+    private final SubscriptionEventResponse subscriptionEventResponse;
 
     @Override
     public void run() {
@@ -47,9 +47,12 @@ public class ResponseTimeoutTask implements Runnable {
     }
 
     private void generateAndSendResponse() {
+        final String subscriptionClientId = subscriptionEventResponse.getData().getClientId();
+        final String subscriptionName = subscriptionEventResponse.getData().getSubscriptionName();
         final String subscriptionEventId = subscriptionClientId + subscriptionName;
         if (forwardedSubscriptionEventCache.containsKey(subscriptionEventId)) {
-            subscriptionEventResponseOutcome.sendResponse(subscriptionClientId, subscriptionName);
+            subscriptionEventResponseOutcome.sendResponse(subscriptionEventResponse,
+                    "subscriptionCreatedStatus");
             forwardedSubscriptionEventCache.remove(subscriptionEventId);
         }
     }
index f511965..5afc52d 100644 (file)
@@ -58,22 +58,23 @@ public class SubscriptionEventConsumer {
             containerFactory = "cloudEventConcurrentKafkaListenerContainerFactory")
     public void consumeSubscriptionEvent(final ConsumerRecord<String, CloudEvent> subscriptionEventConsumerRecord) {
         final CloudEvent cloudEvent = subscriptionEventConsumerRecord.value();
+        final String eventType = subscriptionEventConsumerRecord.value().getType();
         final SubscriptionEvent subscriptionEvent = SubscriptionEventCloudMapper.toSubscriptionEvent(cloudEvent);
         final String eventDatastore = subscriptionEvent.getData().getPredicates().getDatastore();
-        if (!(eventDatastore.equals("passthrough-running") || eventDatastore.equals("passthrough-operational"))) {
+        if (!eventDatastore.equals("passthrough-running")) {
             throw new OperationNotYetSupportedException(
-                "passthrough datastores are currently only supported for event subscriptions");
+                "passthrough-running datastores are currently only supported for event subscriptions");
         }
         if ("CM".equals(subscriptionEvent.getData().getDataType().getDataCategory())) {
             if (subscriptionModelLoaderEnabled) {
                 persistSubscriptionEvent(subscriptionEvent);
             }
-            if ("CREATE".equals(cloudEvent.getType())) {
+            if ("subscriptionCreated".equals(cloudEvent.getType())) {
                 log.info("Subscription for ClientID {} with name {} ...",
                         subscriptionEvent.getData().getSubscription().getClientID(),
                         subscriptionEvent.getData().getSubscription().getName());
                 if (notificationFeatureEnabled) {
-                    subscriptionEventForwarder.forwardCreateSubscriptionEvent(subscriptionEvent);
+                    subscriptionEventForwarder.forwardCreateSubscriptionEvent(subscriptionEvent, eventType);
                 }
             }
         } else {
index 1fe963a..f196cb0 100644 (file)
@@ -44,6 +44,8 @@ import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle;
 import org.onap.cps.ncmp.api.impl.yangmodels.YangModelSubscriptionEvent;
 import org.onap.cps.ncmp.api.inventory.InventoryPersistence;
 import org.onap.cps.ncmp.events.avcsubscription1_0_0.client_to_ncmp.SubscriptionEvent;
+import org.onap.cps.ncmp.events.avcsubscription1_0_0.dmi_to_ncmp.Data;
+import org.onap.cps.ncmp.events.avcsubscription1_0_0.dmi_to_ncmp.SubscriptionEventResponse;
 import org.onap.cps.ncmp.events.avcsubscription1_0_0.ncmp_to_dmi.CmHandle;
 import org.onap.cps.spi.exceptions.OperationNotYetSupportedException;
 import org.springframework.beans.factory.annotation.Value;
@@ -74,7 +76,7 @@ public class SubscriptionEventForwarder {
      *
      * @param subscriptionEvent the event to be forwarded
      */
-    public void forwardCreateSubscriptionEvent(final SubscriptionEvent subscriptionEvent) {
+    public void forwardCreateSubscriptionEvent(final SubscriptionEvent subscriptionEvent, final String eventType) {
         final List<String> cmHandleTargets = subscriptionEvent.getData().getPredicates().getTargets();
         if (cmHandleTargets == null || cmHandleTargets.isEmpty()
                 || cmHandleTargets.stream().anyMatch(id -> (id).contains("*"))) {
@@ -85,13 +87,19 @@ public class SubscriptionEventForwarder {
                 inventoryPersistence.getYangModelCmHandles(cmHandleTargets);
         final Map<String, Map<String, Map<String, String>>> dmiPropertiesPerCmHandleIdPerServiceName
                 = DmiServiceNameOrganizer.getDmiPropertiesPerCmHandleIdPerServiceName(yangModelCmHandles);
-        findDmisAndRespond(subscriptionEvent, cmHandleTargets, dmiPropertiesPerCmHandleIdPerServiceName);
+        findDmisAndRespond(subscriptionEvent, eventType, cmHandleTargets, dmiPropertiesPerCmHandleIdPerServiceName);
     }
 
-    private void findDmisAndRespond(final SubscriptionEvent subscriptionEvent,
+    private void findDmisAndRespond(final SubscriptionEvent subscriptionEvent, final String eventType,
                                     final List<String> cmHandleTargetsAsStrings,
                            final Map<String, Map<String, Map<String, String>>>
                                             dmiPropertiesPerCmHandleIdPerServiceName) {
+        final SubscriptionEventResponse emptySubscriptionEventResponse =
+                new SubscriptionEventResponse().withData(new Data());
+        emptySubscriptionEventResponse.getData().setSubscriptionName(
+                subscriptionEvent.getData().getSubscription().getName());
+        emptySubscriptionEventResponse.getData().setClientId(
+                subscriptionEvent.getData().getSubscription().getClientID());
         final List<String> cmHandlesThatExistsInDb = dmiPropertiesPerCmHandleIdPerServiceName.entrySet().stream()
                 .map(Map.Entry::getValue).map(Map::keySet).flatMap(Set::stream).collect(Collectors.toList());
 
@@ -104,27 +112,27 @@ public class SubscriptionEventForwarder {
             updatesCmHandlesToRejectedAndPersistSubscriptionEvent(subscriptionEvent, targetCmHandlesDoesNotExistInDb);
         }
         if (dmisToRespond.isEmpty()) {
-            final String clientID = subscriptionEvent.getData().getSubscription().getClientID();
-            final String subscriptionName = subscriptionEvent.getData().getSubscription().getName();
-            subscriptionEventResponseOutcome.sendResponse(clientID, subscriptionName);
+            subscriptionEventResponseOutcome.sendResponse(emptySubscriptionEventResponse,
+                    "subscriptionCreatedStatus");
         } else {
-            startResponseTimeout(subscriptionEvent, dmisToRespond);
+            startResponseTimeout(emptySubscriptionEventResponse, dmisToRespond);
             final org.onap.cps.ncmp.events.avcsubscription1_0_0.ncmp_to_dmi.SubscriptionEvent ncmpSubscriptionEvent =
                     clientSubscriptionEventMapper.toNcmpSubscriptionEvent(subscriptionEvent);
-            forwardEventToDmis(dmiPropertiesPerCmHandleIdPerServiceName, ncmpSubscriptionEvent);
+            forwardEventToDmis(dmiPropertiesPerCmHandleIdPerServiceName, ncmpSubscriptionEvent, eventType);
         }
     }
 
-    private void startResponseTimeout(final SubscriptionEvent subscriptionEvent, final Set<String> dmisToRespond) {
-        final String subscriptionClientId = subscriptionEvent.getData().getSubscription().getClientID();
-        final String subscriptionName = subscriptionEvent.getData().getSubscription().getName();
+    private void startResponseTimeout(final SubscriptionEventResponse emptySubscriptionEventResponse,
+                                      final Set<String> dmisToRespond) {
+        final String subscriptionClientId = emptySubscriptionEventResponse.getData().getClientId();
+        final String subscriptionName = emptySubscriptionEventResponse.getData().getSubscriptionName();
         final String subscriptionEventId = subscriptionClientId + subscriptionName;
 
         forwardedSubscriptionEventCache.put(subscriptionEventId, dmisToRespond,
                 ForwardedSubscriptionEventCacheConfig.SUBSCRIPTION_FORWARD_STARTED_TTL_SECS, TimeUnit.SECONDS);
         final ResponseTimeoutTask responseTimeoutTask =
             new ResponseTimeoutTask(forwardedSubscriptionEventCache, subscriptionEventResponseOutcome,
-                    subscriptionClientId, subscriptionName);
+                    emptySubscriptionEventResponse);
         try {
             executorService.schedule(responseTimeoutTask, dmiResponseTimeoutInMs, TimeUnit.MILLISECONDS);
         } catch (final RuntimeException ex) {
@@ -135,7 +143,7 @@ public class SubscriptionEventForwarder {
 
     private void forwardEventToDmis(final Map<String, Map<String, Map<String, String>>> dmiNameCmHandleMap,
                                     final org.onap.cps.ncmp.events.avcsubscription1_0_0.ncmp_to_dmi.SubscriptionEvent
-                                            ncmpSubscriptionEvent) {
+                                            ncmpSubscriptionEvent, final String eventType) {
         dmiNameCmHandleMap.forEach((dmiName, cmHandlePropertiesMap) -> {
             final List<CmHandle> cmHandleTargets = cmHandlePropertiesMap.entrySet().stream().map(
                     cmHandleAndProperties -> {
@@ -150,7 +158,7 @@ public class SubscriptionEventForwarder {
             final String dmiAvcSubscriptionTopic = dmiAvcSubscriptionTopicPrefix + dmiName;
 
             final CloudEvent ncmpSubscriptionCloudEvent =
-                    SubscriptionEventCloudMapper.toCloudEvent(ncmpSubscriptionEvent, eventKey);
+                    SubscriptionEventCloudMapper.toCloudEvent(ncmpSubscriptionEvent, eventKey, eventType);
             eventsPublisher.publishCloudEvent(dmiAvcSubscriptionTopic, eventKey, ncmpSubscriptionCloudEvent);
         });
     }
@@ -182,6 +190,7 @@ public class SubscriptionEventForwarder {
         return yangModelSubscriptionEvent.getPredicates().getTargetCmHandles().stream()
                     .filter(targetCmHandle -> targetCmHandlesDoesNotExistInDb.contains(targetCmHandle.getCmHandleId()))
                     .map(target -> new YangModelSubscriptionEvent.TargetCmHandle(target.getCmHandleId(),
-                                    SubscriptionStatus.REJECTED)).collect(Collectors.toList());
+                                    SubscriptionStatus.REJECTED, "Targets not found"))
+                .collect(Collectors.toList());
     }
 }
index bf9ceb1..35d94cc 100644 (file)
@@ -25,7 +25,6 @@ import java.util.stream.Collectors;
 import org.mapstruct.Mapper;
 import org.mapstruct.Mapping;
 import org.mapstruct.Named;
-import org.onap.cps.ncmp.api.impl.subscriptions.SubscriptionStatus;
 import org.onap.cps.ncmp.api.impl.yangmodels.YangModelSubscriptionEvent;
 import org.onap.cps.ncmp.events.avcsubscription1_0_0.client_to_ncmp.SubscriptionEvent;
 
@@ -47,8 +46,7 @@ public interface SubscriptionEventMapper {
      */
     @Named("mapTargetsToCmHandleTargets")
     default List<YangModelSubscriptionEvent.TargetCmHandle> mapTargetsToCmHandleTargets(List<String> targets) {
-        return targets.stream().map(target -> new YangModelSubscriptionEvent.TargetCmHandle(target,
-                        SubscriptionStatus.PENDING))
+        return targets.stream().map(YangModelSubscriptionEvent.TargetCmHandle::new)
                 .collect(Collectors.toList());
     }
 }
index 20df706..ddb9fd6 100644 (file)
@@ -21,6 +21,7 @@
 package org.onap.cps.ncmp.api.impl.events.avcsubscription;
 
 import com.hazelcast.map.IMap;
+import io.cloudevents.CloudEvent;
 import java.util.Collection;
 import java.util.Map;
 import java.util.Set;
@@ -32,8 +33,9 @@ import org.onap.cps.ncmp.api.impl.config.embeddedcache.ForwardedSubscriptionEven
 import org.onap.cps.ncmp.api.impl.subscriptions.SubscriptionPersistence;
 import org.onap.cps.ncmp.api.impl.subscriptions.SubscriptionStatus;
 import org.onap.cps.ncmp.api.impl.utils.DataNodeHelper;
+import org.onap.cps.ncmp.api.impl.utils.SubscriptionEventResponseCloudMapper;
 import org.onap.cps.ncmp.api.impl.yangmodels.YangModelSubscriptionEvent;
-import org.onap.cps.ncmp.api.models.SubscriptionEventResponse;
+import org.onap.cps.ncmp.events.avcsubscription1_0_0.dmi_to_ncmp.SubscriptionEventResponse;
 import org.onap.cps.spi.model.DataNode;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.kafka.annotation.KafkaListener;
@@ -61,19 +63,21 @@ public class SubscriptionEventResponseConsumer {
      * @param subscriptionEventResponseConsumerRecord the event to be consumed
      */
     @KafkaListener(topics = "${app.ncmp.avc.subscription-response-topic}",
-        properties = {"spring.json.value.default.type=org.onap.cps.ncmp.api.models.SubscriptionEventResponse"})
+            containerFactory = "cloudEventConcurrentKafkaListenerContainerFactory")
     public void consumeSubscriptionEventResponse(
-            final ConsumerRecord<String, SubscriptionEventResponse> subscriptionEventResponseConsumerRecord) {
-        final SubscriptionEventResponse subscriptionEventResponse = subscriptionEventResponseConsumerRecord.value();
-        final String clientId = subscriptionEventResponse.getClientId();
+            final ConsumerRecord<String, CloudEvent> subscriptionEventResponseConsumerRecord) {
+        final CloudEvent cloudEvent = subscriptionEventResponseConsumerRecord.value();
+        final String eventType = subscriptionEventResponseConsumerRecord.value().getType();
+        final SubscriptionEventResponse subscriptionEventResponse =
+                SubscriptionEventResponseCloudMapper.toSubscriptionEventResponse(cloudEvent);
+        final String clientId = subscriptionEventResponse.getData().getClientId();
         log.info("subscription event response of clientId: {} is received.", clientId);
-        final String subscriptionName = subscriptionEventResponse.getSubscriptionName();
+        final String subscriptionName = subscriptionEventResponse.getData().getSubscriptionName();
         final String subscriptionEventId = clientId + subscriptionName;
         boolean createOutcomeResponse = false;
         if (forwardedSubscriptionEventCache.containsKey(subscriptionEventId)) {
             final Set<String> dmiNames = forwardedSubscriptionEventCache.get(subscriptionEventId);
-
-            dmiNames.remove(subscriptionEventResponse.getDmiName());
+            dmiNames.remove(subscriptionEventResponse.getData().getDmiName());
             forwardedSubscriptionEventCache.put(subscriptionEventId, dmiNames,
                     ForwardedSubscriptionEventCacheConfig.SUBSCRIPTION_FORWARD_STARTED_TTL_SECS, TimeUnit.SECONDS);
             createOutcomeResponse = forwardedSubscriptionEventCache.get(subscriptionEventId).isEmpty();
@@ -84,7 +88,7 @@ public class SubscriptionEventResponseConsumer {
         if (createOutcomeResponse
                 && notificationFeatureEnabled
                 && hasNoPendingCmHandles(clientId, subscriptionName)) {
-            subscriptionEventResponseOutcome.sendResponse(clientId, subscriptionName);
+            subscriptionEventResponseOutcome.sendResponse(subscriptionEventResponse, eventType);
             forwardedSubscriptionEventCache.remove(subscriptionEventId);
         }
     }
@@ -92,10 +96,15 @@ public class SubscriptionEventResponseConsumer {
     private boolean hasNoPendingCmHandles(final String clientId, final String subscriptionName) {
         final Collection<DataNode> dataNodeSubscription = subscriptionPersistence.getCmHandlesForSubscriptionEvent(
                 clientId, subscriptionName);
-        final Map<String, SubscriptionStatus> cmHandleIdToStatusMap =
-                DataNodeHelper.getCmHandleIdToStatusMapFromDataNodes(
-                dataNodeSubscription);
-        return !cmHandleIdToStatusMap.values().contains(SubscriptionStatus.PENDING);
+        final Map<String, Map<String, String>> cmHandleIdToStatusAndDetailsAsMapOriginal =
+                DataNodeHelper.cmHandleIdToStatusAndDetailsAsMapFromDataNode(dataNodeSubscription);
+        for (final Map<String, String> statusAndDetailsMap : cmHandleIdToStatusAndDetailsAsMapOriginal.values()) {
+            final String status = statusAndDetailsMap.get("status");
+            if (SubscriptionStatus.PENDING.toString().equals(status)) {
+                return false;
+            }
+        }
+        return true;
     }
 
     private void updateSubscriptionEvent(final SubscriptionEventResponse subscriptionEventResponse) {
index 44181c5..dc122ee 100644 (file)
 package org.onap.cps.ncmp.api.impl.events.avcsubscription;
 
 import java.util.List;
-import java.util.Map;
 import java.util.stream.Collectors;
 import org.mapstruct.Mapper;
 import org.mapstruct.Mapping;
 import org.mapstruct.Named;
-import org.onap.cps.ncmp.api.impl.subscriptions.SubscriptionStatus;
 import org.onap.cps.ncmp.api.impl.yangmodels.YangModelSubscriptionEvent;
-import org.onap.cps.ncmp.api.models.SubscriptionEventResponse;
+import org.onap.cps.ncmp.events.avcsubscription1_0_0.dmi_to_ncmp.SubscriptionEventResponse;
+import org.onap.cps.ncmp.events.avcsubscription1_0_0.dmi_to_ncmp.SubscriptionStatus;
 
 @Mapper(componentModel = "spring")
 public interface SubscriptionEventResponseMapper {
 
-    @Mapping(source = "clientId", target = "clientId")
-    @Mapping(source = "subscriptionName", target = "subscriptionName")
-    @Mapping(source = "cmHandleIdToStatus", target = "predicates.targetCmHandles",
-            qualifiedByName = "mapStatusToCmHandleTargets")
+    @Mapping(source = "data.clientId", target = "clientId")
+    @Mapping(source = "data.subscriptionName", target = "subscriptionName")
+    @Mapping(source = "data.subscriptionStatus", target = "predicates.targetCmHandles",
+            qualifiedByName = "mapSubscriptionStatusToCmHandleTargets")
     YangModelSubscriptionEvent toYangModelSubscriptionEvent(
             SubscriptionEventResponse subscriptionEventResponse);
 
     /**
-     * Maps StatusToCMHandle to list of TargetCmHandle.
+     * Maps SubscriptionStatus to list of TargetCmHandle.
      *
-     * @param targets as a map
+     * @param subscriptionStatus as a list
      * @return TargetCmHandle list
      */
-    @Named("mapStatusToCmHandleTargets")
-    default List<YangModelSubscriptionEvent.TargetCmHandle> mapStatusToCmHandleTargets(
-            Map<String, SubscriptionStatus> targets) {
-        return targets.entrySet().stream().map(target ->
-                new YangModelSubscriptionEvent.TargetCmHandle(target.getKey(), target.getValue())).collect(
-                Collectors.toList());
+    @Named("mapSubscriptionStatusToCmHandleTargets")
+    default List<YangModelSubscriptionEvent.TargetCmHandle> mapSubscriptionStatusToCmHandleTargets(
+            List<SubscriptionStatus> subscriptionStatus) {
+        return subscriptionStatus.stream().map(status -> new YangModelSubscriptionEvent.TargetCmHandle(status.getId(),
+                org.onap.cps.ncmp.api.impl.subscriptions.SubscriptionStatus.fromString(status.getStatus().value()),
+                        status.getDetails())).collect(Collectors.toList());
     }
 }
index 8fdff17..9ed6865 100644 (file)
 
 package org.onap.cps.ncmp.api.impl.events.avcsubscription;
 
-import java.io.Serializable;
-import java.util.Collection;
+import io.cloudevents.CloudEvent;
 import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
-import org.apache.kafka.common.header.Headers;
-import org.apache.kafka.common.header.internals.RecordHeaders;
+import org.onap.cps.ncmp.api.NcmpEventResponseCode;
 import org.onap.cps.ncmp.api.impl.events.EventsPublisher;
 import org.onap.cps.ncmp.api.impl.subscriptions.SubscriptionPersistence;
 import org.onap.cps.ncmp.api.impl.subscriptions.SubscriptionStatus;
 import org.onap.cps.ncmp.api.impl.utils.DataNodeHelper;
-import org.onap.cps.ncmp.api.models.SubscriptionEventResponse;
-import org.onap.cps.ncmp.events.avc.subscription.v1.SubscriptionEventOutcome;
-import org.onap.cps.spi.model.DataNode;
+import org.onap.cps.ncmp.api.impl.utils.SubscriptionOutcomeCloudMapper;
+import org.onap.cps.ncmp.events.avcsubscription1_0_0.dmi_to_ncmp.SubscriptionEventResponse;
+import org.onap.cps.ncmp.events.avcsubscription1_0_0.ncmp_to_client.SubscriptionEventOutcome;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Component;
 
@@ -45,75 +44,106 @@ public class SubscriptionEventResponseOutcome {
 
     private final SubscriptionPersistence subscriptionPersistence;
 
-    private final EventsPublisher<SubscriptionEventOutcome> outcomeEventsPublisher;
+    private final EventsPublisher<CloudEvent> outcomeEventsPublisher;
 
     private final SubscriptionOutcomeMapper subscriptionOutcomeMapper;
 
-    @Value("${app.ncmp.avc.subscription-outcome-topic:cm-avc-subscription-response}")
+    @Value("${app.ncmp.avc.subscription-outcome-topic:subscription-response}")
     private String subscriptionOutcomeEventTopic;
 
     /**
      * This is for construction of outcome message to be published for client apps.
      *
-     * @param subscriptionClientId client id of the subscription.
-     * @param subscriptionName name of the subscription.
+     * @param subscriptionEventResponse event produced by Dmi Plugin
      */
-    public void sendResponse(final String subscriptionClientId, final String subscriptionName) {
-        final SubscriptionEventOutcome subscriptionEventOutcome = generateResponse(
-                subscriptionClientId, subscriptionName);
-        final Headers headers = new RecordHeaders();
+    public void sendResponse(final SubscriptionEventResponse subscriptionEventResponse, final String eventKey) {
+        final SubscriptionEventOutcome subscriptionEventOutcome =
+                formSubscriptionOutcomeMessage(subscriptionEventResponse);
+        final String subscriptionClientId = subscriptionEventResponse.getData().getClientId();
+        final String subscriptionName = subscriptionEventResponse.getData().getSubscriptionName();
         final String subscriptionEventId = subscriptionClientId + subscriptionName;
-        outcomeEventsPublisher.publishEvent(subscriptionOutcomeEventTopic,
-                subscriptionEventId, headers, subscriptionEventOutcome);
+        final CloudEvent subscriptionOutcomeCloudEvent =
+                SubscriptionOutcomeCloudMapper.toCloudEvent(subscriptionEventOutcome,
+                subscriptionEventId, eventKey);
+        outcomeEventsPublisher.publishCloudEvent(subscriptionOutcomeEventTopic,
+                subscriptionEventId, subscriptionOutcomeCloudEvent);
     }
 
-    private SubscriptionEventOutcome generateResponse(final String subscriptionClientId,
-                                                      final String subscriptionName) {
-        final Collection<DataNode> dataNodes =
-                subscriptionPersistence.getCmHandlesForSubscriptionEvent(subscriptionClientId, subscriptionName);
-        final List<Map<String, Serializable>> dataNodeLeaves = DataNodeHelper.getDataNodeLeaves(dataNodes);
-        final List<Collection<Serializable>> cmHandleIdToStatus =
-                DataNodeHelper.getCmHandleIdToStatus(dataNodeLeaves);
-        final Map<String, SubscriptionStatus> cmHandleIdToStatusMap =
-                DataNodeHelper.getCmHandleIdToStatusMap(cmHandleIdToStatus);
-        return formSubscriptionOutcomeMessage(cmHandleIdToStatus, subscriptionClientId, subscriptionName,
-                isFullOutcomeResponse(cmHandleIdToStatusMap));
+    private SubscriptionEventOutcome formSubscriptionOutcomeMessage(
+            final SubscriptionEventResponse subscriptionEventResponse) {
+        final Map<String, Map<String, String>> cmHandleIdToStatusAndDetailsAsMap =
+                DataNodeHelper.cmHandleIdToStatusAndDetailsAsMapFromDataNode(
+                subscriptionPersistence.getCmHandlesForSubscriptionEvent(
+                        subscriptionEventResponse.getData().getClientId(),
+                        subscriptionEventResponse.getData().getSubscriptionName()));
+        final List<org.onap.cps.ncmp.events.avcsubscription1_0_0.dmi_to_ncmp.SubscriptionStatus>
+                subscriptionStatusList = mapCmHandleIdStatusDetailsMapToSubscriptionStatusList(
+                        cmHandleIdToStatusAndDetailsAsMap);
+        subscriptionEventResponse.getData().setSubscriptionStatus(subscriptionStatusList);
+        return fromSubscriptionEventResponse(subscriptionEventResponse,
+                decideOnNcmpEventResponseCodeForSubscription(cmHandleIdToStatusAndDetailsAsMap));
     }
 
-    private boolean isFullOutcomeResponse(final Map<String, SubscriptionStatus> cmHandleIdToStatusMap) {
-        return !cmHandleIdToStatusMap.values().contains(SubscriptionStatus.PENDING);
+    private static List<org.onap.cps.ncmp.events.avcsubscription1_0_0.dmi_to_ncmp.SubscriptionStatus>
+        mapCmHandleIdStatusDetailsMapToSubscriptionStatusList(
+            final Map<String, Map<String, String>> cmHandleIdToStatusAndDetailsAsMap) {
+        return cmHandleIdToStatusAndDetailsAsMap.entrySet()
+                .stream().map(entryset -> {
+                    final org.onap.cps.ncmp.events.avcsubscription1_0_0.dmi_to_ncmp.SubscriptionStatus
+                            subscriptionStatus = new org.onap.cps.ncmp.events.avcsubscription1_0_0
+                            .dmi_to_ncmp.SubscriptionStatus();
+                    final String cmHandleId = entryset.getKey();
+                    final Map<String, String> statusAndDetailsMap = entryset.getValue();
+                    final String status = statusAndDetailsMap.get("status");
+                    final String details = statusAndDetailsMap.get("details");
+                    subscriptionStatus.setId(cmHandleId);
+                    subscriptionStatus.setStatus(
+                            org.onap.cps.ncmp.events.avcsubscription1_0_0.dmi_to_ncmp
+                                    .SubscriptionStatus.Status.fromValue(status));
+                    subscriptionStatus.setDetails(details);
+                    return subscriptionStatus;
+                }).collect(Collectors.toList());
     }
 
-    private SubscriptionEventOutcome formSubscriptionOutcomeMessage(
-            final List<Collection<Serializable>> cmHandleIdToStatus, final String subscriptionClientId,
-            final String subscriptionName, final boolean isFullOutcomeResponse) {
+    private NcmpEventResponseCode decideOnNcmpEventResponseCodeForSubscription(
+            final Map<String, Map<String, String>> cmHandleIdToStatusAndDetailsAsMap) {
 
-        final SubscriptionEventResponse subscriptionEventResponse = toSubscriptionEventResponse(
-                cmHandleIdToStatus, subscriptionClientId, subscriptionName);
+        final boolean isAllTargetsPending = isAllTargetCmHandleStatusMatch(cmHandleIdToStatusAndDetailsAsMap,
+                SubscriptionStatus.PENDING);
 
-        final SubscriptionEventOutcome subscriptionEventOutcome =
-                subscriptionOutcomeMapper.toSubscriptionEventOutcome(subscriptionEventResponse);
+        final boolean isAllTargetsRejected = isAllTargetCmHandleStatusMatch(cmHandleIdToStatusAndDetailsAsMap,
+                SubscriptionStatus.REJECTED);
+
+        final boolean isAllTargetsAccepted = isAllTargetCmHandleStatusMatch(cmHandleIdToStatusAndDetailsAsMap,
+                SubscriptionStatus.ACCEPTED);
 
-        if (isFullOutcomeResponse) {
-            subscriptionEventOutcome.setEventType(SubscriptionEventOutcome.EventType.COMPLETE_OUTCOME);
+        if (isAllTargetsAccepted) {
+            return NcmpEventResponseCode.SUCCESSFULLY_APPLIED_SUBSCRIPTION;
+        } else if (isAllTargetsRejected) {
+            return NcmpEventResponseCode.SUBSCRIPTION_NOT_APPLICABLE;
+        } else if (isAllTargetsPending) {
+            return NcmpEventResponseCode.SUBSCRIPTION_PENDING;
         } else {
-            subscriptionEventOutcome.setEventType(SubscriptionEventOutcome.EventType.PARTIAL_OUTCOME);
+            return NcmpEventResponseCode.PARTIALLY_APPLIED_SUBSCRIPTION;
         }
+    }
 
-        return subscriptionEventOutcome;
+    private boolean isAllTargetCmHandleStatusMatch(
+            final Map<String, Map<String, String>> cmHandleIdToStatusAndDetailsAsMap,
+            final SubscriptionStatus subscriptionStatus) {
+        return cmHandleIdToStatusAndDetailsAsMap.values().stream()
+                .allMatch(entryset -> entryset.containsValue(subscriptionStatus.toString()));
     }
 
-    private SubscriptionEventResponse toSubscriptionEventResponse(
-            final List<Collection<Serializable>> cmHandleIdToStatus, final String subscriptionClientId,
-            final String subscriptionName) {
-        final Map<String, SubscriptionStatus> cmHandleIdToStatusMap =
-                DataNodeHelper.getCmHandleIdToStatusMap(cmHandleIdToStatus);
+    private SubscriptionEventOutcome fromSubscriptionEventResponse(
+            final SubscriptionEventResponse subscriptionEventResponse,
+            final NcmpEventResponseCode ncmpEventResponseCode) {
 
-        final SubscriptionEventResponse subscriptionEventResponse = new SubscriptionEventResponse();
-        subscriptionEventResponse.setClientId(subscriptionClientId);
-        subscriptionEventResponse.setSubscriptionName(subscriptionName);
-        subscriptionEventResponse.setCmHandleIdToStatus(cmHandleIdToStatusMap);
+        final SubscriptionEventOutcome subscriptionEventOutcome =
+                subscriptionOutcomeMapper.toSubscriptionEventOutcome(subscriptionEventResponse);
+        subscriptionEventOutcome.getData().setStatusCode(Integer.parseInt(ncmpEventResponseCode.getStatusCode()));
+        subscriptionEventOutcome.getData().setStatusMessage(ncmpEventResponseCode.getStatusMessage());
 
-        return subscriptionEventResponse;
+        return subscriptionEventOutcome;
     }
 }
index cecde5f..7803b98 100644 (file)
@@ -26,63 +26,80 @@ import java.util.stream.Collectors;
 import org.mapstruct.Mapper;
 import org.mapstruct.Mapping;
 import org.mapstruct.Named;
-import org.onap.cps.ncmp.api.impl.subscriptions.SubscriptionStatus;
-import org.onap.cps.ncmp.api.models.SubscriptionEventResponse;
-import org.onap.cps.ncmp.events.avc.subscription.v1.SubscriptionEventOutcome;
+import org.onap.cps.ncmp.events.avcsubscription1_0_0.dmi_to_ncmp.SubscriptionEventResponse;
+import org.onap.cps.ncmp.events.avcsubscription1_0_0.dmi_to_ncmp.SubscriptionStatus;
+import org.onap.cps.ncmp.events.avcsubscription1_0_0.ncmp_to_client.AdditionalInfo;
+import org.onap.cps.ncmp.events.avcsubscription1_0_0.ncmp_to_client.AdditionalInfoDetail;
+import org.onap.cps.ncmp.events.avcsubscription1_0_0.ncmp_to_client.SubscriptionEventOutcome;
+import org.onap.cps.spi.exceptions.DataValidationException;
 
 @Mapper(componentModel = "spring")
 public interface SubscriptionOutcomeMapper {
 
-    @Mapping(source = "clientId", target = "event.subscription.clientID")
-    @Mapping(source = "subscriptionName", target = "event.subscription.name")
-    @Mapping(source = "cmHandleIdToStatus", target = "event.predicates.rejectedTargets",
-            qualifiedByName = "mapStatusToCmHandleRejected")
-    @Mapping(source = "cmHandleIdToStatus", target = "event.predicates.acceptedTargets",
-            qualifiedByName = "mapStatusToCmHandleAccepted")
-    @Mapping(source = "cmHandleIdToStatus", target = "event.predicates.pendingTargets",
-            qualifiedByName = "mapStatusToCmHandlePending")
-    SubscriptionEventOutcome toSubscriptionEventOutcome(
-            SubscriptionEventResponse subscriptionEventResponse);
+    @Mapping(source = "data.subscriptionStatus", target = "data.additionalInfo",
+            qualifiedByName = "mapListOfSubscriptionStatusToAdditionalInfo")
+    SubscriptionEventOutcome toSubscriptionEventOutcome(SubscriptionEventResponse subscriptionEventResponse);
 
     /**
-     * Maps StatusToCMHandle to list of TargetCmHandle rejected.
+     * Maps list of SubscriptionStatus to an AdditionalInfo.
      *
-     * @param targets as a map
-     * @return TargetCmHandle list
+     * @param subscriptionStatusList containing details
+     * @return an AdditionalInfo
      */
-    @Named("mapStatusToCmHandleRejected")
-    default List<Object> mapStatusToCmHandleRejected(Map<String, SubscriptionStatus> targets) {
-        return targets.entrySet()
-                .stream().filter(target -> SubscriptionStatus.REJECTED.equals(target.getValue()))
-                .map(Map.Entry::getKey)
-                .collect(Collectors.toList());
+    @Named("mapListOfSubscriptionStatusToAdditionalInfo")
+    default AdditionalInfo mapListOfSubscriptionStatusToAdditionalInfo(
+            final List<SubscriptionStatus> subscriptionStatusList) {
+        if (subscriptionStatusList == null || subscriptionStatusList.isEmpty()) {
+            throw new DataValidationException("Invalid subscriptionStatusList",
+                    "SubscriptionStatus list cannot be null or empty");
+        }
+
+        final Map<String, List<SubscriptionStatus>> rejectedSubscriptionsPerDetails = getSubscriptionsPerDetails(
+                subscriptionStatusList, SubscriptionStatus.Status.REJECTED);
+        final Map<String, List<String>> rejectedCmHandlesPerDetails =
+                getCmHandlesPerDetails(rejectedSubscriptionsPerDetails);
+        final List<AdditionalInfoDetail> rejectedCmHandles = getAdditionalInfoDetailList(rejectedCmHandlesPerDetails);
+
+
+        final Map<String, List<SubscriptionStatus>> pendingSubscriptionsPerDetails = getSubscriptionsPerDetails(
+                subscriptionStatusList, SubscriptionStatus.Status.PENDING);
+        final Map<String, List<String>> pendingCmHandlesPerDetails =
+                getCmHandlesPerDetails(pendingSubscriptionsPerDetails);
+        final List<AdditionalInfoDetail> pendingCmHandles = getAdditionalInfoDetailList(pendingCmHandlesPerDetails);
+
+        final AdditionalInfo additionalInfo = new AdditionalInfo();
+        additionalInfo.setRejected(rejectedCmHandles);
+        additionalInfo.setPending(pendingCmHandles);
+
+        return additionalInfo;
     }
 
-    /**
-     * Maps StatusToCMHandle to list of TargetCmHandle accepted.
-     *
-     * @param targets as a map
-     * @return TargetCmHandle list
-     */
-    @Named("mapStatusToCmHandleAccepted")
-    default List<Object> mapStatusToCmHandleAccepted(Map<String, SubscriptionStatus> targets) {
-        return targets.entrySet()
-                .stream().filter(target -> SubscriptionStatus.ACCEPTED.equals(target.getValue()))
-                .map(Map.Entry::getKey)
-                .collect(Collectors.toList());
+    private static Map<String, List<SubscriptionStatus>> getSubscriptionsPerDetails(
+            final List<SubscriptionStatus> subscriptionStatusList, final SubscriptionStatus.Status status) {
+        return subscriptionStatusList.stream()
+                .filter(subscriptionStatus -> subscriptionStatus.getStatus() == status)
+                .collect(Collectors.groupingBy(SubscriptionStatus::getDetails));
     }
 
-    /**
-     * Maps StatusToCMHandle to list of TargetCmHandle pending.
-     *
-     * @param targets as a map
-     * @return TargetCmHandle list
-     */
-    @Named("mapStatusToCmHandlePending")
-    default List<Object> mapStatusToCmHandlePending(Map<String, SubscriptionStatus> targets) {
-        return targets.entrySet()
-                .stream().filter(target -> SubscriptionStatus.PENDING.equals(target.getValue()))
-                .map(Map.Entry::getKey)
-                .collect(Collectors.toList());
+    private static Map<String, List<String>> getCmHandlesPerDetails(
+            final Map<String, List<SubscriptionStatus>> subscriptionsPerDetails) {
+        return subscriptionsPerDetails.entrySet().stream()
+                .collect(Collectors.toMap(
+                        Map.Entry::getKey,
+                        entry -> entry.getValue().stream()
+                                .map(SubscriptionStatus::getId)
+                                .collect(Collectors.toList())
+                ));
+    }
+
+    private static List<AdditionalInfoDetail> getAdditionalInfoDetailList(
+            final Map<String, List<String>> cmHandlesPerDetails) {
+        return cmHandlesPerDetails.entrySet().stream()
+                .map(entry -> {
+                    final AdditionalInfoDetail detail = new AdditionalInfoDetail();
+                    detail.setDetails(entry.getKey());
+                    detail.setTargets(entry.getValue());
+                    return detail;
+                }).collect(Collectors.toList());
     }
 }
index d2b1237..83a375b 100644 (file)
@@ -22,7 +22,6 @@ package org.onap.cps.ncmp.api.impl.subscriptions;
 
 import static org.onap.cps.ncmp.api.impl.constants.DmiRegistryConstants.NO_TIMESTAMP;
 
-import java.io.Serializable;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashMap;
@@ -70,33 +69,46 @@ public class SubscriptionPersistenceImpl implements SubscriptionPersistence {
     private void findDeltaCmHandlesAddOrUpdateInDatabase(final YangModelSubscriptionEvent yangModelSubscriptionEvent,
                                                          final String clientId, final String subscriptionName,
                                                          final Collection<DataNode> dataNodes) {
-        final Map<String, SubscriptionStatus> cmHandleIdsFromYangModel =
+        final Map<String, Map<String, String>> cmHandleIdToStatusAndDetailsAsMapNew =
                 extractCmHandleFromYangModelAsMap(yangModelSubscriptionEvent);
-        final Map<String, SubscriptionStatus> cmHandleIdsFromDatabase =
-                extractCmHandleFromDbAsMap(dataNodes);
+        final Map<String, Map<String, String>> cmHandleIdToStatusAndDetailsAsMapOriginal =
+                DataNodeHelper.cmHandleIdToStatusAndDetailsAsMapFromDataNode(dataNodes);
 
-        final Map<String, SubscriptionStatus> newCmHandles =
-                mapDifference(cmHandleIdsFromYangModel, cmHandleIdsFromDatabase);
-        traverseCmHandleList(newCmHandles, clientId, subscriptionName, true);
+        final Map<String, Map<String, String>> newTargetCmHandles =
+                mapDifference(cmHandleIdToStatusAndDetailsAsMapNew,
+                        cmHandleIdToStatusAndDetailsAsMapOriginal);
+        traverseCmHandleList(newTargetCmHandles, clientId, subscriptionName, true);
 
-        final Map<String, SubscriptionStatus> existingCmHandles =
-                mapDifference(cmHandleIdsFromYangModel, newCmHandles);
-        traverseCmHandleList(existingCmHandles, clientId, subscriptionName, false);
+        final Map<String, Map<String, String>> existingTargetCmHandles =
+                mapDifference(cmHandleIdToStatusAndDetailsAsMapNew, newTargetCmHandles);
+        traverseCmHandleList(existingTargetCmHandles, clientId, subscriptionName, false);
     }
 
-    private boolean isSubscriptionRegistryEmptyOrNonExist(final Collection<DataNode> dataNodes,
-                                                          final String clientId, final String subscriptionName) {
-        final Optional<DataNode> dataNodeFirst = dataNodes.stream().findFirst();
-        return ((dataNodeFirst.isPresent() && dataNodeFirst.get().getChildDataNodes().isEmpty())
-                || getCmHandlesForSubscriptionEvent(clientId, subscriptionName).isEmpty());
-    }
-
-    private void traverseCmHandleList(final Map<String, SubscriptionStatus> cmHandleMap,
+    private static Map<String, Map<String, String>> extractCmHandleFromYangModelAsMap(
+            final YangModelSubscriptionEvent yangModelSubscriptionEvent) {
+        return yangModelSubscriptionEvent.getPredicates().getTargetCmHandles()
+                .stream().collect(
+                        HashMap<String, Map<String, String>>::new,
+                        (result, cmHandle) -> {
+                            final String cmHandleId = cmHandle.getCmHandleId();
+                            final SubscriptionStatus status = cmHandle.getStatus();
+                            final String details = cmHandle.getDetails();
+
+                            if (cmHandleId != null && status != null) {
+                                result.put(cmHandleId, new HashMap<>());
+                                result.get(cmHandleId).put("status", status.toString());
+                                result.get(cmHandleId).put("details", details == null ? "" : details);
+                            }
+                        },
+                        HashMap::putAll
+                );
+    }
+
+    private void traverseCmHandleList(final Map<String, Map<String, String>> cmHandleMap,
                                       final String clientId,
                                       final String subscriptionName,
                                       final boolean isAddListElementOperation) {
-        final List<YangModelSubscriptionEvent.TargetCmHandle> cmHandleList =
-                targetCmHandlesAsList(cmHandleMap);
+        final List<YangModelSubscriptionEvent.TargetCmHandle> cmHandleList = targetCmHandlesAsList(cmHandleMap);
         for (final YangModelSubscriptionEvent.TargetCmHandle targetCmHandle : cmHandleList) {
             final String targetCmHandleAsJson =
                     createTargetCmHandleJsonData(jsonObjectMapper.asJsonString(targetCmHandle));
@@ -105,6 +117,13 @@ public class SubscriptionPersistenceImpl implements SubscriptionPersistence {
         }
     }
 
+    private boolean isSubscriptionRegistryEmptyOrNonExist(final Collection<DataNode> dataNodes,
+                                                          final String clientId, final String subscriptionName) {
+        final Optional<DataNode> dataNodeFirst = dataNodes.stream().findFirst();
+        return ((dataNodeFirst.isPresent() && dataNodeFirst.get().getChildDataNodes().isEmpty())
+                || getCmHandlesForSubscriptionEvent(clientId, subscriptionName).isEmpty());
+    }
+
     private void addOrReplaceCmHandlePredicateListElement(final String targetCmHandleAsJson,
                                                           final String clientId,
                                                           final String subscriptionName,
@@ -142,25 +161,16 @@ public class SubscriptionPersistenceImpl implements SubscriptionPersistence {
                 FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS);
     }
 
-    private static Map<String, SubscriptionStatus> extractCmHandleFromDbAsMap(final Collection<DataNode> dataNodes) {
-        final List<Map<String, Serializable>> dataNodeLeaves = DataNodeHelper.getDataNodeLeaves(dataNodes);
-        final List<Collection<Serializable>> cmHandleIdToStatus = DataNodeHelper.getCmHandleIdToStatus(dataNodeLeaves);
-        return DataNodeHelper.getCmHandleIdToStatusMap(cmHandleIdToStatus);
-    }
-
-    private static Map<String, SubscriptionStatus> extractCmHandleFromYangModelAsMap(
-            final YangModelSubscriptionEvent yangModelSubscriptionEvent) {
-        return yangModelSubscriptionEvent.getPredicates().getTargetCmHandles()
-                .stream().collect(Collectors.toMap(
-                        YangModelSubscriptionEvent.TargetCmHandle::getCmHandleId,
-                        YangModelSubscriptionEvent.TargetCmHandle::getStatus));
-    }
-
     private static List<YangModelSubscriptionEvent.TargetCmHandle> targetCmHandlesAsList(
-            final Map<String, SubscriptionStatus> newCmHandles) {
-        return newCmHandles.entrySet().stream().map(entry ->
-                new YangModelSubscriptionEvent.TargetCmHandle(entry.getKey(),
-                        entry.getValue())).collect(Collectors.toList());
+            final Map<String, Map<String, String>> newCmHandles) {
+        return newCmHandles.entrySet().stream().map(entry -> {
+            final String cmHandleId = entry.getKey();
+            final Map<String, String> statusAndDetailsMap = entry.getValue();
+            final String status = statusAndDetailsMap.get("status");
+            final String details = statusAndDetailsMap.get("details");
+            return new YangModelSubscriptionEvent.TargetCmHandle(cmHandleId,
+                    SubscriptionStatus.fromString(status), details);
+        }).collect(Collectors.toList());
     }
 
     private static String createSubscriptionEventJsonData(final String yangModelSubscriptionAsJson) {
@@ -181,9 +191,9 @@ public class SubscriptionPersistenceImpl implements SubscriptionPersistence {
                 + "' and @subscriptionName='" + subscriptionName + "']";
     }
 
-    private static <K, V> Map<K, V> mapDifference(final Map<? extends K, ? extends V> left,
-                                                  final Map<? extends K, ? extends V> right) {
-        final Map<K, V> difference = new HashMap<>();
+    private static <K, L, M> Map<K, Map<L, M>> mapDifference(final Map<K, Map<L, M>> left,
+                                                             final Map<K, Map<L, M>> right) {
+        final Map<K, Map<L, M>> difference = new HashMap<>();
         difference.putAll(left);
         difference.putAll(right);
         difference.entrySet().removeAll(right.entrySet());
index ce3b88b..63ab102 100644 (file)
 
 package org.onap.cps.ncmp.api.impl.subscriptions;
 
-import java.io.Serializable;
-import java.util.Iterator;
-import java.util.Map;
 
 public enum SubscriptionStatus {
-    ACCEPTED,
-    REJECTED,
-    PENDING;
+    ACCEPTED("ACCEPTED"),
+    REJECTED("REJECTED"),
+    PENDING("PENDING");
 
+    private final String subscriptionStatusValue;
+
+    SubscriptionStatus(final String subscriptionStatusValue) {
+        this.subscriptionStatusValue = subscriptionStatusValue;
+    }
 
     /**
-     * Populates a map with a key of cm handle id and a value of subscription status.
+     * Finds the value of the given enum.
      *
-     * @param resultMap the map is being populated
-     * @param bucketIterator to iterate over the collection
+     * @param statusValue value of the enum
+     * @return a SubscriptionStatus
      */
-    public static void populateCmHandleToSubscriptionStatusMap(final Map<String, SubscriptionStatus> resultMap,
-                                                          final Iterator<Serializable> bucketIterator) {
-        final String item = (String) bucketIterator.next();
-        if ("PENDING".equals(item)) {
-            resultMap.put((String) bucketIterator.next(),
-                    SubscriptionStatus.PENDING);
-        }
-        if ("REJECTED".equals(item)) {
-            resultMap.put((String) bucketIterator.next(),
-                    SubscriptionStatus.REJECTED);
-        }
-        if ("ACCEPTED".equals(item)) {
-            resultMap.put((String) bucketIterator.next(),
-                    SubscriptionStatus.ACCEPTED);
+    public static SubscriptionStatus fromString(final String statusValue) {
+        for (final SubscriptionStatus subscriptionStatusType : SubscriptionStatus.values()) {
+            if (subscriptionStatusType.subscriptionStatusValue.equalsIgnoreCase(statusValue)) {
+                return subscriptionStatusType;
+            }
         }
+        return null;
     }
 }
index f42a378..c032d1e 100644 (file)
@@ -23,14 +23,12 @@ package org.onap.cps.ncmp.api.impl.utils;
 import java.io.Serializable;
 import java.util.Collection;
 import java.util.HashMap;
-import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import lombok.AccessLevel;
 import lombok.NoArgsConstructor;
-import org.onap.cps.ncmp.api.impl.subscriptions.SubscriptionStatus;
 import org.onap.cps.spi.model.DataNode;
 
 @NoArgsConstructor(access = AccessLevel.PRIVATE)
@@ -50,8 +48,8 @@ public class DataNodeHelper {
     /**
      * The leaves for each DataNode is listed as map.
      *
-     * @param dataNodes as collection.
-     * @return list of map for the all leaves.
+     * @param dataNodes as collection
+     * @return list of map for the all leaves
      */
     public static List<Map<String, Serializable>> getDataNodeLeaves(final Collection<DataNode> dataNodes) {
         return dataNodes.stream()
@@ -61,47 +59,42 @@ public class DataNodeHelper {
     }
 
     /**
-     * The cm handle and status is listed as a collection.
+     * Extracts the mapping of cm handle id to status with details from nodes leaves.
      *
-     * @param dataNodeLeaves as a list of map.
-     * @return list of collection containing cm handle id and statuses.
+     * @param dataNodeLeaves as a list of map
+     * @return cm handle id to status and details mapping
      */
-    public static List<Collection<Serializable>> getCmHandleIdToStatus(
+    public static Map<String, Map<String, String>> cmHandleIdToStatusAndDetailsAsMap(
             final List<Map<String, Serializable>> dataNodeLeaves) {
         return dataNodeLeaves.stream()
-                .map(Map::values)
-                .filter(col -> col.contains("PENDING")
-                        || col.contains("ACCEPTED")
-                        || col.contains("REJECTED"))
-                .collect(Collectors.toList());
-    }
+                .filter(entryset -> entryset.values().contains("PENDING")
+                        || entryset.values().contains("ACCEPTED")
+                        || entryset.values().contains("REJECTED"))
+                .collect(
+                        HashMap<String, Map<String, String>>::new,
+                        (result, entry) -> {
+                            final String cmHandleId = (String) entry.get("cmHandleId");
+                            final String status = (String) entry.get("status");
+                            final String details = (String) entry.get("details");
 
-    /**
-     * The cm handle and status is returned as a map.
-     *
-     * @param cmHandleIdToStatus as a list of collection
-     * @return a map of cm handle id to status
-     */
-    public static Map<String, SubscriptionStatus> getCmHandleIdToStatusMap(
-            final List<Collection<Serializable>> cmHandleIdToStatus) {
-        final Map<String, SubscriptionStatus> resultMap = new HashMap<>();
-        for (final Collection<Serializable> cmHandleToStatusBucket: cmHandleIdToStatus) {
-            final Iterator<Serializable> bucketIterator = cmHandleToStatusBucket.iterator();
-            while (bucketIterator.hasNext()) {
-                SubscriptionStatus.populateCmHandleToSubscriptionStatusMap(resultMap, bucketIterator);
-            }
-        }
-        return resultMap;
+                            if (cmHandleId != null && status != null) {
+                                result.put(cmHandleId, new HashMap<>());
+                                result.get(cmHandleId).put("status", status);
+                                result.get(cmHandleId).put("details", details == null ? "" : details);
+                            }
+                        },
+                        HashMap::putAll
+                );
     }
 
     /**
-     * Extracts the mapping of cm handle id to status from data node collection.
+     * Extracts the mapping of cm handle id to status with details from data node collection.
      *
      * @param dataNodes as a collection
-     * @return cm handle id to status mapping
+     * @return cm handle id to status and details mapping
      */
-    public static Map<String, SubscriptionStatus> getCmHandleIdToStatusMapFromDataNodes(
+    public static Map<String, Map<String, String>> cmHandleIdToStatusAndDetailsAsMapFromDataNode(
             final Collection<DataNode> dataNodes) {
-        return getCmHandleIdToStatusMap(getCmHandleIdToStatus(getDataNodeLeaves(dataNodes)));
+        return cmHandleIdToStatusAndDetailsAsMap(getDataNodeLeaves(dataNodes));
     }
 }
index a7de479..df3998f 100644 (file)
@@ -27,10 +27,12 @@ import io.cloudevents.core.builder.CloudEventBuilder;
 import io.cloudevents.core.data.PojoCloudEventData;
 import io.cloudevents.jackson.PojoCloudEventDataMapper;
 import java.net.URI;
+import java.util.UUID;
 import lombok.AccessLevel;
 import lombok.NoArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.onap.cps.ncmp.events.avcsubscription1_0_0.client_to_ncmp.SubscriptionEvent;
+import org.onap.cps.spi.exceptions.CloudEventConstructionException;
 
 @NoArgsConstructor(access = AccessLevel.PRIVATE)
 @Slf4j
@@ -38,6 +40,8 @@ public class SubscriptionEventCloudMapper {
 
     private static final ObjectMapper objectMapper = new ObjectMapper();
 
+    private static String randomId = UUID.randomUUID().toString();
+
     /**
      * Maps CloudEvent object to SubscriptionEvent.
      *
@@ -62,18 +66,24 @@ public class SubscriptionEventCloudMapper {
      *
      * @param ncmpSubscriptionEvent object.
      * @param eventKey as String.
-     * @return CloudEvent builded.
+     * @return CloudEvent built.
      */
     public static CloudEvent toCloudEvent(
             final org.onap.cps.ncmp.events.avcsubscription1_0_0.ncmp_to_dmi.SubscriptionEvent ncmpSubscriptionEvent,
-            final String eventKey) {
+            final String eventKey, final String eventType) {
         try {
             return CloudEventBuilder.v1()
-                    .withData(objectMapper.writeValueAsBytes(ncmpSubscriptionEvent))
-                    .withId(eventKey).withType("CREATE").withSource(
-                            URI.create(ncmpSubscriptionEvent.getData().getSubscription().getClientID())).build();
+                    .withId(randomId)
+                    .withSource(URI.create(ncmpSubscriptionEvent.getData().getSubscription().getClientID()))
+                    .withType(eventType)
+                    .withExtension("correlationid", eventKey)
+                    .withDataSchema(URI.create("urn:cps:"
+                            + org.onap.cps.ncmp.events.avcsubscription1_0_0.ncmp_to_dmi
+                                    .SubscriptionEvent.class.getName() + ":1.0.0"))
+                    .withData(objectMapper.writeValueAsBytes(ncmpSubscriptionEvent)).build();
         } catch (final Exception ex) {
-            throw new RuntimeException("The Cloud Event could not be constructed.", ex);
+            throw new CloudEventConstructionException("The Cloud Event could not be constructed", "Invalid object to "
+                    + "serialize or required headers is missing", ex);
         }
     }
 }
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/SubscriptionEventResponseCloudMapper.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/SubscriptionEventResponseCloudMapper.java
new file mode 100644 (file)
index 0000000..17aba65
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2023 Nordix Foundation
+ *  ================================================================================
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ *  SPDX-License-Identifier: Apache-2.0
+ *  ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.ncmp.api.impl.utils;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.cloudevents.CloudEvent;
+import io.cloudevents.core.CloudEventUtils;
+import io.cloudevents.core.data.PojoCloudEventData;
+import io.cloudevents.jackson.PojoCloudEventDataMapper;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.onap.cps.ncmp.events.avcsubscription1_0_0.dmi_to_ncmp.SubscriptionEventResponse;
+
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+@Slf4j
+public class SubscriptionEventResponseCloudMapper {
+
+    private static final ObjectMapper objectMapper = new ObjectMapper();
+
+    /**
+     * Maps CloudEvent object to SubscriptionEventResponse.
+     *
+     * @param cloudEvent object
+     * @return SubscriptionEventResponse deserialized
+     */
+    public static SubscriptionEventResponse toSubscriptionEventResponse(final CloudEvent cloudEvent) {
+        final PojoCloudEventData<SubscriptionEventResponse> deserializedCloudEvent = CloudEventUtils
+                .mapData(cloudEvent, PojoCloudEventDataMapper.from(objectMapper, SubscriptionEventResponse.class));
+        if (deserializedCloudEvent == null) {
+            log.debug("No data found in the consumed subscription response event");
+            return null;
+        } else {
+            final SubscriptionEventResponse subscriptionEventResponse = deserializedCloudEvent.getValue();
+            log.debug("Consuming subscription response event {}", subscriptionEventResponse);
+            return subscriptionEventResponse;
+        }
+    }
+}
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/SubscriptionOutcomeCloudMapper.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/SubscriptionOutcomeCloudMapper.java
new file mode 100644 (file)
index 0000000..92c5656
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2023 Nordix Foundation
+ *  ================================================================================
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ *  SPDX-License-Identifier: Apache-2.0
+ *  ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.ncmp.api.impl.utils;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.cloudevents.CloudEvent;
+import io.cloudevents.core.builder.CloudEventBuilder;
+import java.net.URI;
+import java.util.UUID;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.onap.cps.ncmp.events.avcsubscription1_0_0.ncmp_to_client.SubscriptionEventOutcome;
+import org.onap.cps.spi.exceptions.CloudEventConstructionException;
+
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+@Slf4j
+public class SubscriptionOutcomeCloudMapper {
+
+    private static final ObjectMapper objectMapper = new ObjectMapper();
+
+    private static String randomId = UUID.randomUUID().toString();
+
+    /**
+     * Maps SubscriptionEventOutcome to a CloudEvent.
+     *
+     * @param subscriptionEventOutcome object
+     * @return CloudEvent
+     */
+    public static CloudEvent toCloudEvent(final SubscriptionEventOutcome subscriptionEventOutcome,
+                                          final String eventKey, final String eventType) {
+        try {
+            return CloudEventBuilder.v1()
+                    .withId(randomId)
+                    .withSource(URI.create("NCMP"))
+                    .withType(eventType)
+                    .withExtension("correlationid", eventKey)
+                    .withDataSchema(URI.create("urn:cps:" + SubscriptionEventOutcome.class.getName() + ":1.0.0"))
+                    .withData(objectMapper.writeValueAsBytes(subscriptionEventOutcome)).build();
+        } catch (final Exception ex) {
+            throw new CloudEventConstructionException("The Cloud Event could not be constructed", "Invalid object to "
+                    + "serialize or required headers is missing", ex);
+        }
+    }
+}
index 4dcc579..866bfd4 100644 (file)
@@ -81,9 +81,18 @@ public class YangModelSubscriptionEvent {
         @JsonProperty()
         private final SubscriptionStatus status;
 
+        @JsonProperty()
+        private final String details;
+
+        /**
+         * Constructor with single parameter for TargetCmHandle.
+         *
+         * @param cmHandleId as cm handle id
+         */
         public TargetCmHandle(final String cmHandleId) {
             this.cmHandleId = cmHandleId;
             this.status = SubscriptionStatus.PENDING;
+            this.details = "Subscription forwarded to dmi plugin";
         }
     }
 }
index e332a28..7096c18 100644 (file)
@@ -41,6 +41,10 @@ module subscription {
                     leaf status {
                       type string;
                     }
+
+                    leaf details {
+                        type string;
+                    }
                 }
 
                 leaf datastore {
index d4ab1e8..5f60773 100644 (file)
@@ -58,7 +58,7 @@ class SubscriptionEventConsumerSpec extends MessagingBaseSpec {
             testEventSent.getData().getDataType().setDataCategory(dataCategory)
             def testCloudEventSent = CloudEventBuilder.v1()
                 .withData(objectMapper.writeValueAsBytes(testEventSent))
-                .withId('some-event-id')
+                .withId('subscriptionCreated')
                 .withType(dataType)
                 .withSource(URI.create('some-resource'))
                 .withExtension('correlationid', 'test-cmhandle1').build()
@@ -74,34 +74,35 @@ class SubscriptionEventConsumerSpec extends MessagingBaseSpec {
         and: 'the event is persisted'
             numberOfTimesToPersist * mockSubscriptionPersistence.saveSubscriptionEvent(yangModelSubscriptionEvent)
         and: 'the event is forwarded'
-            numberOfTimesToForward * mockSubscriptionEventForwarder.forwardCreateSubscriptionEvent(testEventSent)
+            numberOfTimesToForward * mockSubscriptionEventForwarder.forwardCreateSubscriptionEvent(testEventSent, 'subscriptionCreated')
         where: 'given values are used'
-            scenario                                            |  dataCategory  |   dataType     |  isNotificationEnabled     |   isModelLoaderEnabled      ||     numberOfTimesToForward        ||      numberOfTimesToPersist
-            'Both model loader and notification are enabled'    |       'CM'     |   'CREATE'     |     true                   |        true                 ||         1                         ||             1
-            'Both model loader and notification are disabled'   |       'CM'     |   'CREATE'     |     false                  |        false                ||         0                         ||             0
-            'Model loader enabled and notification  disabled'   |       'CM'     |   'CREATE'     |     false                  |        true                 ||         0                         ||             1
-            'Model loader disabled and notification enabled'    |       'CM'     |   'CREATE'     |     true                   |        false                ||         1                         ||             0
-            'Flags are enabled but data category is FM'         |       'FM'     |   'CREATE'     |     true                   |        true                 ||         0                         ||             0
-            'Flags are enabled but data type is UPDATE'         |       'CM'     |   'UPDATE'     |     true                   |        true                 ||         0                         ||             1
+            scenario                                            |  dataCategory  |   dataType                  |  isNotificationEnabled     |   isModelLoaderEnabled      ||     numberOfTimesToForward        ||      numberOfTimesToPersist
+            'Both model loader and notification are enabled'    |       'CM'     |   'subscriptionCreated'     |     true                   |        true                 ||         1                         ||             1
+            'Both model loader and notification are disabled'   |       'CM'     |   'subscriptionCreated'     |     false                  |        false                ||         0                         ||             0
+            'Model loader enabled and notification  disabled'   |       'CM'     |   'subscriptionCreated'     |     false                  |        true                 ||         0                         ||             1
+            'Model loader disabled and notification enabled'    |       'CM'     |   'subscriptionCreated'     |     true                   |        false                ||         1                         ||             0
+            'Flags are enabled but data category is FM'         |       'FM'     |   'subscriptionCreated'     |     true                   |        true                 ||         0                         ||             0
+            'Flags are enabled but data type is UPDATE'         |       'CM'     |   'subscriptionUpdated'     |     true                   |        true                 ||         0                         ||             1
     }
 
     def 'Consume event with wrong datastore causes an exception'() {
         given: 'an event'
             def jsonData = TestUtils.getResourceFileContent('avcSubscriptionCreationEvent.json')
             def testEventSent = jsonObjectMapper.convertJsonString(jsonData, SubscriptionEvent.class)
-        and: 'datastore is set to a non passthrough datastore'
+        and: 'datastore is set to a passthrough-running datastore'
             testEventSent.getData().getPredicates().setDatastore('operational')
             def testCloudEventSent = CloudEventBuilder.v1()
                 .withData(objectMapper.writeValueAsBytes(testEventSent))
                 .withId('some-event-id')
-                .withType('CREATE')
+                .withType('some-event-type')
                 .withSource(URI.create('some-resource'))
                 .withExtension('correlationid', 'test-cmhandle1').build()
             def consumerRecord = new ConsumerRecord<String, SubscriptionEvent>('topic-name', 0, 0, 'event-key', testCloudEventSent)
         when: 'the valid event is consumed'
             objectUnderTest.consumeSubscriptionEvent(consumerRecord)
         then: 'an operation not yet supported exception is thrown'
-            thrown(OperationNotYetSupportedException)
+            def exception = thrown(OperationNotYetSupportedException)
+            exception.details == 'passthrough-running datastores are currently only supported for event subscriptions'
     }
 
 }
index 2af32c2..4343c23 100644 (file)
@@ -35,6 +35,8 @@ import org.onap.cps.ncmp.api.impl.yangmodels.YangModelSubscriptionEvent.TargetCm
 import org.onap.cps.ncmp.api.inventory.InventoryPersistence
 import org.onap.cps.ncmp.api.kafka.MessagingBaseSpec
 import org.onap.cps.ncmp.events.avcsubscription1_0_0.client_to_ncmp.SubscriptionEvent
+import org.onap.cps.ncmp.events.avcsubscription1_0_0.dmi_to_ncmp.Data
+import org.onap.cps.ncmp.events.avcsubscription1_0_0.dmi_to_ncmp.SubscriptionEventResponse
 import org.onap.cps.ncmp.events.avcsubscription1_0_0.ncmp_to_dmi.CmHandle;
 import org.onap.cps.ncmp.utils.TestUtils
 import org.onap.cps.spi.exceptions.OperationNotYetSupportedException
@@ -75,13 +77,6 @@ class SubscriptionEventForwarderSpec extends MessagingBaseSpec {
         given: 'an event'
             def jsonData = TestUtils.getResourceFileContent('avcSubscriptionCreationEvent.json')
             def testEventSent = jsonObjectMapper.convertJsonString(jsonData, SubscriptionEvent.class)
-        and: 'the some of the cm handles will be accepted and some of rejected'
-            def cmHandlesToBeSavedInDb = [new TargetCmHandle('CMHandle1', SubscriptionStatus.ACCEPTED),
-                                     new TargetCmHandle('CMHandle2',SubscriptionStatus.ACCEPTED),
-                                     new TargetCmHandle('CMHandle3',SubscriptionStatus.REJECTED)]
-        and: 'a yang model subscription event will be saved into the db'
-            def yangModelSubscriptionEventWithAcceptedAndRejectedCmHandles = subscriptionEventMapper.toYangModelSubscriptionEvent(testEventSent)
-            yangModelSubscriptionEventWithAcceptedAndRejectedCmHandles.getPredicates().setTargetCmHandles(cmHandlesToBeSavedInDb)
         and: 'the InventoryPersistence returns private properties for the supplied CM Handles'
             1 * mockInventoryPersistence.getYangModelCmHandles(["CMHandle1", "CMHandle2", "CMHandle3"]) >> [
                 createYangModelCmHandleWithDmiProperty(1, 1,"shape","circle"),
@@ -92,7 +87,7 @@ class SubscriptionEventForwarderSpec extends MessagingBaseSpec {
         and: 'a Blocking Variable is used for the Asynchronous call with a timeout of 5 seconds'
             def block = new BlockingVariable<Object>(5)
         when: 'the valid event is forwarded'
-            objectUnderTest.forwardCreateSubscriptionEvent(testEventSent)
+            objectUnderTest.forwardCreateSubscriptionEvent(testEventSent, 'subscriptionCreated')
         then: 'An asynchronous call is made to the blocking variable'
             block.get()
         then: 'the event is added to the forwarded subscription event cache'
@@ -106,8 +101,6 @@ class SubscriptionEventForwarderSpec extends MessagingBaseSpec {
                     targets == [cmHandle2, cmHandle1]
                 }
             )
-        and: 'the persistence service save the yang model subscription event'
-            1 * mockSubscriptionPersistence.saveSubscriptionEvent(yangModelSubscriptionEventWithAcceptedAndRejectedCmHandles)
         and: 'a separate thread has been created where the map is polled'
             1 * mockForwardedSubscriptionEventCache.containsKey("SCO-9989752cm-subscription-001") >> true
             1 * mockSubscriptionEventResponseOutcome.sendResponse(*_)
@@ -122,7 +115,7 @@ class SubscriptionEventForwarderSpec extends MessagingBaseSpec {
         and: 'the target CMHandles are set to #scenario'
             testEventSent.getData().getPredicates().setTargets(invalidTargets)
         when: 'the event is forwarded'
-            objectUnderTest.forwardCreateSubscriptionEvent(testEventSent)
+            objectUnderTest.forwardCreateSubscriptionEvent(testEventSent, 'some-event-type')
         then: 'an operation not yet supported exception is thrown'
             thrown(OperationNotYetSupportedException)
         where:
@@ -136,13 +129,17 @@ class SubscriptionEventForwarderSpec extends MessagingBaseSpec {
         given: 'an event'
             def jsonData = TestUtils.getResourceFileContent('avcSubscriptionCreationEvent.json')
             def testEventSent = jsonObjectMapper.convertJsonString(jsonData, SubscriptionEvent.class)
+        and: 'a subscription event response'
+            def emptySubscriptionEventResponse = new SubscriptionEventResponse().withData(new Data());
+            emptySubscriptionEventResponse.getData().setSubscriptionName('cm-subscription-001');
+            emptySubscriptionEventResponse.getData().setClientId('SCO-9989752');
         and: 'the cm handles will be rejected'
-            def rejectedCmHandles = [new TargetCmHandle('CMHandle1', SubscriptionStatus.REJECTED),
-                                     new TargetCmHandle('CMHandle2',SubscriptionStatus.REJECTED),
-                                     new TargetCmHandle('CMHandle3',SubscriptionStatus.REJECTED)]
+            def rejectedCmHandles = [new TargetCmHandle('CMHandle1', SubscriptionStatus.REJECTED, 'Cm handle does not exist'),
+                                     new TargetCmHandle('CMHandle2',SubscriptionStatus.REJECTED, 'Cm handle does not exist'),
+                                     new TargetCmHandle('CMHandle3',SubscriptionStatus.REJECTED, 'Cm handle does not exist')]
         and: 'a yang model subscription event will be saved into the db with rejected cm handles'
-            def yangModelSubscriptionEventWithRejectedCmHandles = subscriptionEventMapper.toYangModelSubscriptionEvent(testEventSent)
-            yangModelSubscriptionEventWithRejectedCmHandles.getPredicates().setTargetCmHandles(rejectedCmHandles)
+            def yangModelSubscriptionEvent = subscriptionEventMapper.toYangModelSubscriptionEvent(testEventSent)
+            yangModelSubscriptionEvent.getPredicates().setTargetCmHandles(rejectedCmHandles)
         and: 'the InventoryPersistence returns no private properties for the supplied CM Handles'
             1 * mockInventoryPersistence.getYangModelCmHandles(["CMHandle1", "CMHandle2", "CMHandle3"]) >> []
         and: 'the thread creation delay is reduced to 2 seconds for testing'
@@ -150,7 +147,7 @@ class SubscriptionEventForwarderSpec extends MessagingBaseSpec {
         and: 'a Blocking Variable is used for the Asynchronous call with a timeout of 5 seconds'
             def block = new BlockingVariable<Object>(5)
         when: 'the valid event is forwarded'
-            objectUnderTest.forwardCreateSubscriptionEvent(testEventSent)
+            objectUnderTest.forwardCreateSubscriptionEvent(testEventSent, 'subscriptionCreatedStatus')
         then: 'the event is not added to the forwarded subscription event cache'
             0 * mockForwardedSubscriptionEventCache.put("SCO-9989752cm-subscription-001", ["DMIName1", "DMIName2"] as Set)
         and: 'the event is not being forwarded with the CMHandle private properties and does not provides a valid listenable future'
@@ -175,9 +172,9 @@ class SubscriptionEventForwarderSpec extends MessagingBaseSpec {
         and: 'the subscription id is removed from the event cache map returning the asynchronous blocking variable'
             0 * mockForwardedSubscriptionEventCache.remove("SCO-9989752cm-subscription-001") >> {block.set(_)}
         and: 'the persistence service save target cm handles of the yang model subscription event as rejected '
-            1 * mockSubscriptionPersistence.saveSubscriptionEvent(yangModelSubscriptionEventWithRejectedCmHandles)
+            1 * mockSubscriptionPersistence.saveSubscriptionEvent(yangModelSubscriptionEvent)
         and: 'subscription outcome has been sent'
-            1 * mockSubscriptionEventResponseOutcome.sendResponse('SCO-9989752', 'cm-subscription-001')
+            1 * mockSubscriptionEventResponseOutcome.sendResponse(emptySubscriptionEventResponse, 'subscriptionCreatedStatus')
     }
 
     static def createYangModelCmHandleWithDmiProperty(id, dmiId,propertyName, propertyValue) {
index 5355dd8..7cc40cc 100644 (file)
@@ -22,17 +22,27 @@ package org.onap.cps.ncmp.api.impl.events.avcsubscription
 
 import com.fasterxml.jackson.databind.ObjectMapper
 import com.hazelcast.map.IMap
+import io.cloudevents.CloudEvent
+import io.cloudevents.core.builder.CloudEventBuilder
 import org.apache.kafka.clients.consumer.ConsumerRecord
 import org.onap.cps.ncmp.api.impl.subscriptions.SubscriptionPersistenceImpl
 import org.onap.cps.ncmp.api.kafka.MessagingBaseSpec
-import org.onap.cps.ncmp.api.models.SubscriptionEventResponse
+import org.onap.cps.ncmp.events.avcsubscription1_0_0.dmi_to_ncmp.SubscriptionEventResponse
+import org.onap.cps.ncmp.utils.TestUtils
 import org.onap.cps.spi.model.DataNodeBuilder
 import org.onap.cps.utils.JsonObjectMapper
+import org.springframework.beans.factory.annotation.Autowired
 import org.springframework.boot.test.context.SpringBootTest
 
 @SpringBootTest(classes = [ObjectMapper, JsonObjectMapper])
 class SubscriptionEventResponseConsumerSpec extends MessagingBaseSpec {
 
+    @Autowired
+    JsonObjectMapper jsonObjectMapper
+
+    @Autowired
+    ObjectMapper objectMapper
+
     IMap<String, Set<String>> mockForwardedSubscriptionEventCache = Mock(IMap<String, Set<String>>)
     def mockSubscriptionPersistence = Mock(SubscriptionPersistenceImpl)
     def mockSubscriptionEventResponseMapper  = Mock(SubscriptionEventResponseMapper)
@@ -41,72 +51,90 @@ class SubscriptionEventResponseConsumerSpec extends MessagingBaseSpec {
     def objectUnderTest = new SubscriptionEventResponseConsumer(mockForwardedSubscriptionEventCache,
         mockSubscriptionPersistence, mockSubscriptionEventResponseMapper, mockSubscriptionEventResponseOutcome)
 
-    def cmHandleToStatusMap = [CMHandle1: 'PENDING', CMHandle2: 'ACCEPTED'] as Map
-    def testEventReceived = new SubscriptionEventResponse(clientId: 'some-client-id',
-        subscriptionName: 'some-subscription-name', dmiName: 'some-dmi-name', cmHandleIdToStatus: cmHandleToStatusMap)
-    def consumerRecord = new ConsumerRecord<String, SubscriptionEventResponse>('topic-name', 0, 0, 'event-key', testEventReceived)
-
     def 'Consume Subscription Event Response where all DMIs have responded'() {
-        given: 'a subscription event response and notifications are enabled'
-            objectUnderTest.notificationFeatureEnabled = isNotificationFeatureEnabled
+        given: 'a consumer record including cloud event having subscription response'
+            def consumerRecordWithCloudEventAndSubscriptionResponse = getConsumerRecord()
+        and: 'a subscription response event'
+            def subscriptionResponseEvent = getSubscriptionResponseEvent()
+        and: 'a subscription event response and notifications are enabled'
+            objectUnderTest.notificationFeatureEnabled = notificationEnabled
         and: 'subscription model loader is enabled'
-            objectUnderTest.subscriptionModelLoaderEnabled = true
-        and: 'a data node exist in db'
-            def leaves1 = [status:'ACCEPTED', cmHandleId:'cmhandle1'] as Map
-            def dataNode = new DataNodeBuilder().withDataspace('NCMP-Admin')
-                .withAnchor('AVC-Subscriptions').withXpath('/subscription-registry/subscription')
-                .withLeaves(leaves1).build()
-        and: 'subscription persistence service returns data node'
-            mockSubscriptionPersistence.getCmHandlesForSubscriptionEvent(*_) >> [dataNode]
+            objectUnderTest.subscriptionModelLoaderEnabled = modelLoaderEnabled
+        and: 'subscription persistence service returns data node includes no pending cm handle'
+            mockSubscriptionPersistence.getCmHandlesForSubscriptionEvent(*_) >> [getDataNode()]
         when: 'the valid event is consumed'
-            objectUnderTest.consumeSubscriptionEventResponse(consumerRecord)
+            objectUnderTest.consumeSubscriptionEventResponse(consumerRecordWithCloudEventAndSubscriptionResponse)
         then: 'the forwarded subscription event cache returns only the received dmiName existing for the subscription create event'
-            1 * mockForwardedSubscriptionEventCache.containsKey('some-client-idsome-subscription-name') >> true
-            1 * mockForwardedSubscriptionEventCache.get('some-client-idsome-subscription-name') >> (['some-dmi-name'] as Set)
+            1 * mockForwardedSubscriptionEventCache.containsKey('SCO-9989752cm-subscription-001') >> true
+            1 * mockForwardedSubscriptionEventCache.get('SCO-9989752cm-subscription-001') >> (['some-dmi-name'] as Set)
         and: 'the forwarded subscription event cache returns an empty Map when the dmiName has been removed'
-            1 * mockForwardedSubscriptionEventCache.get('some-client-idsome-subscription-name') >> ([] as Set)
+            1 * mockForwardedSubscriptionEventCache.get('SCO-9989752cm-subscription-001') >> ([] as Set)
+        and: 'the response event is map to yang model'
+            numberOfTimeToPersist * mockSubscriptionEventResponseMapper.toYangModelSubscriptionEvent(_)
+        and: 'the response event is persisted into the db'
+            numberOfTimeToPersist * mockSubscriptionPersistence.saveSubscriptionEvent(_)
         and: 'the subscription event is removed from the map'
-            numberOfExpectedCallToRemove * mockForwardedSubscriptionEventCache.remove('some-client-idsome-subscription-name')
+            numberOfTimeToRemove * mockForwardedSubscriptionEventCache.remove('SCO-9989752cm-subscription-001')
         and: 'a response outcome has been created'
-            numberOfExpectedCallToSendResponse * mockSubscriptionEventResponseOutcome.sendResponse('some-client-id', 'some-subscription-name')
+            numberOfTimeToResponse * mockSubscriptionEventResponseOutcome.sendResponse(subscriptionResponseEvent, 'subscriptionCreated')
         where: 'the following values are used'
-            scenario             | isNotificationFeatureEnabled  ||  numberOfExpectedCallToRemove  || numberOfExpectedCallToSendResponse
-            'Response sent'      | true                          ||   1                            || 1
-            'Response not sent'  | false                         ||   0                            || 0
+            scenario                                              | modelLoaderEnabled  |   notificationEnabled  ||  numberOfTimeToPersist  ||  numberOfTimeToRemove  || numberOfTimeToResponse
+            'Both model loader and notification are enabled'      |    true             |     true               ||   1                     ||      1                 ||       1
+            'Both model loader and notification are disabled'     |    false            |     false              ||   0                     ||      0                 ||       0
+            'Model loader enabled and notification  disabled'     |    true             |     false              ||   1                     ||      0                 ||       0
+            'Model loader disabled and notification enabled'      |    false            |     true               ||   0                     ||      1                 ||       1
     }
 
     def 'Consume Subscription Event Response where another DMI has not yet responded'() {
         given: 'a subscription event response and notifications are enabled'
-            objectUnderTest.notificationFeatureEnabled = true
+            objectUnderTest.notificationFeatureEnabled = notificationEnabled
         and: 'subscription model loader is enabled'
-            objectUnderTest.subscriptionModelLoaderEnabled = true
+            objectUnderTest.subscriptionModelLoaderEnabled = modelLoaderEnabled
         when: 'the valid event is consumed'
-            objectUnderTest.consumeSubscriptionEventResponse(consumerRecord)
+            objectUnderTest.consumeSubscriptionEventResponse(getConsumerRecord())
         then: 'the forwarded subscription event cache returns only the received dmiName existing for the subscription create event'
-            1 * mockForwardedSubscriptionEventCache.containsKey('some-client-idsome-subscription-name') >> true
-            1 * mockForwardedSubscriptionEventCache.get('some-client-idsome-subscription-name') >> (['some-dmi-name', 'non-responded-dmi'] as Set)
+            1 * mockForwardedSubscriptionEventCache.containsKey('SCO-9989752cm-subscription-001') >> true
+            1 * mockForwardedSubscriptionEventCache.get('SCO-9989752cm-subscription-001') >> (['responded-dmi', 'non-responded-dmi'] as Set)
         and: 'the forwarded subscription event cache returns an empty Map when the dmiName has been removed'
-            1 * mockForwardedSubscriptionEventCache.get('some-client-idsome-subscription-name') >> (['non-responded-dmi'] as Set)
+            1 * mockForwardedSubscriptionEventCache.get('SCO-9989752cm-subscription-001') >> (['non-responded-dmi'] as Set)
+        and: 'the response event is map to yang model'
+            numberOfTimeToPersist * mockSubscriptionEventResponseMapper.toYangModelSubscriptionEvent(_)
+        and: 'the response event is persisted into the db'
+            numberOfTimeToPersist * mockSubscriptionPersistence.saveSubscriptionEvent(_)
+        and: 'the subscription event is removed from the map'
         and: 'the subscription event is not removed from the map'
             0 * mockForwardedSubscriptionEventCache.remove(_)
         and: 'a response outcome has not been created'
             0 * mockSubscriptionEventResponseOutcome.sendResponse(*_)
+        where: 'the following values are used'
+            scenario                                              | modelLoaderEnabled  |   notificationEnabled  ||  numberOfTimeToPersist
+            'Both model loader and notification are enabled'      |    true             |     true               ||   1
+            'Both model loader and notification are disabled'     |    false            |     false              ||   0
+            'Model loader enabled and notification  disabled'     |    true             |     false              ||   1
+            'Model loader disabled and notification enabled'      |    false            |     true               ||   0
     }
 
-    def 'Update subscription event when the model loader flag is enabled'() {
-        given: 'subscription model loader is enabled as per #scenario'
-            objectUnderTest.subscriptionModelLoaderEnabled = isSubscriptionModelLoaderEnabled
-        when: 'the valid event is consumed'
-            objectUnderTest.consumeSubscriptionEventResponse(consumerRecord)
-        then: 'the forwarded subscription event cache does not return dmiName for the subscription create event'
-            1 * mockForwardedSubscriptionEventCache.containsKey('some-client-idsome-subscription-name') >> false
-        and: 'the mapper returns yang model subscription event with #numberOfExpectedCall'
-            numberOfExpectedCall * mockSubscriptionEventResponseMapper.toYangModelSubscriptionEvent(_)
-        and: 'subscription event has been updated into DB with #numberOfExpectedCall'
-            numberOfExpectedCall * mockSubscriptionPersistence.saveSubscriptionEvent(_)
-        where: 'the following values are used'
-            scenario                   | isSubscriptionModelLoaderEnabled || numberOfExpectedCall
-            'The event is updated'     | true                             || 1
-            'The event is not updated' | false                            || 0
+    def getSubscriptionResponseEvent() {
+        def subscriptionResponseJsonData = TestUtils.getResourceFileContent('avcSubscriptionEventResponse.json')
+        return jsonObjectMapper.convertJsonString(subscriptionResponseJsonData, SubscriptionEventResponse.class)
+    }
+
+    def getCloudEventHavingSubscriptionResponseEvent() {
+        return CloudEventBuilder.v1()
+            .withData(objectMapper.writeValueAsBytes(getSubscriptionResponseEvent()))
+            .withId('some-id')
+            .withType('subscriptionCreated')
+            .withSource(URI.create('NCMP')).build()
+    }
+
+    def getConsumerRecord() {
+        return new ConsumerRecord<String, CloudEvent>('topic-name', 0, 0, 'event-key', getCloudEventHavingSubscriptionResponseEvent())
+    }
+
+    def getDataNode() {
+        def leaves = [status:'ACCEPTED', cmHandleId:'cmhandle1'] as Map
+        return new DataNodeBuilder().withDataspace('NCMP-Admin')
+            .withAnchor('AVC-Subscriptions').withXpath('/subscription-registry/subscription')
+            .withLeaves(leaves).build()
     }
 }
index 00412aa..4c60281 100644 (file)
@@ -22,9 +22,8 @@ package org.onap.cps.ncmp.api.impl.events.avcsubscription
 
 import com.fasterxml.jackson.databind.ObjectMapper
 import org.mapstruct.factory.Mappers
-import org.onap.cps.ncmp.api.impl.events.avcsubscription.SubscriptionEventResponseMapper
 import org.onap.cps.ncmp.api.impl.subscriptions.SubscriptionStatus
-import org.onap.cps.ncmp.api.models.SubscriptionEventResponse
+import org.onap.cps.ncmp.events.avcsubscription1_0_0.dmi_to_ncmp.SubscriptionEventResponse;
 import org.onap.cps.ncmp.utils.TestUtils
 import org.onap.cps.utils.JsonObjectMapper
 import org.springframework.beans.factory.annotation.Autowired
@@ -50,13 +49,12 @@ class SubscriptionEventResponseMapperSpec extends Specification {
             assert result.clientId == "SCO-9989752"
         and: 'subscription name'
             assert result.subscriptionName == "cm-subscription-001"
-        and: 'predicate targets '
-            assert result.predicates.targetCmHandles.cmHandleId == ["CMHandle1", "CMHandle3", "CMHandle4", "CMHandle5"]
+        and: 'predicate targets cm handle size as expected'
+            assert result.predicates.targetCmHandles.size() == 7
+        and: 'predicate targets cm handle ids as expected'
+            assert result.predicates.targetCmHandles.cmHandleId == ["CMHandle1", "CMHandle2", "CMHandle3", "CMHandle4", "CMHandle5", "CMHandle6", "CMHandle7"]
         and: 'the status for these targets is set to expected values'
-            assert result.predicates.targetCmHandles.status == [SubscriptionStatus.ACCEPTED, SubscriptionStatus.REJECTED,
-            SubscriptionStatus.PENDING, SubscriptionStatus.PENDING]
-        and: 'the topic is null'
-            assert result.topic == null
+            assert result.predicates.targetCmHandles.status == [SubscriptionStatus.REJECTED, SubscriptionStatus.REJECTED, SubscriptionStatus.REJECTED, SubscriptionStatus.REJECTED, SubscriptionStatus.PENDING, SubscriptionStatus.PENDING, SubscriptionStatus.PENDING]
     }
 
 }
\ No newline at end of file
index bb0e7b7..c1c428b 100644 (file)
 package org.onap.cps.ncmp.api.impl.events.avcsubscription
 
 import com.fasterxml.jackson.databind.ObjectMapper
-import org.apache.kafka.common.header.internals.RecordHeaders
+import io.cloudevents.CloudEvent
+import io.cloudevents.core.builder.CloudEventBuilder
 import org.mapstruct.factory.Mappers
+import org.onap.cps.ncmp.api.NcmpEventResponseCode
 import org.onap.cps.ncmp.api.impl.events.EventsPublisher
 import org.onap.cps.ncmp.api.impl.subscriptions.SubscriptionPersistence
-import org.onap.cps.ncmp.api.impl.subscriptions.SubscriptionStatus
 import org.onap.cps.ncmp.api.impl.utils.DataNodeBaseSpec
-import org.onap.cps.ncmp.events.avc.subscription.v1.SubscriptionEventOutcome
+import org.onap.cps.ncmp.api.impl.utils.SubscriptionOutcomeCloudMapper
+import org.onap.cps.ncmp.events.avcsubscription1_0_0.dmi_to_ncmp.SubscriptionEventResponse
+import org.onap.cps.ncmp.events.avcsubscription1_0_0.ncmp_to_client.SubscriptionEventOutcome;
 import org.onap.cps.ncmp.utils.TestUtils
 import org.onap.cps.utils.JsonObjectMapper
 import org.spockframework.spring.SpringBean
@@ -43,72 +46,77 @@ class SubscriptionEventResponseOutcomeSpec extends DataNodeBaseSpec {
     @SpringBean
     SubscriptionPersistence mockSubscriptionPersistence = Mock(SubscriptionPersistence)
     @SpringBean
-    EventsPublisher<SubscriptionEventOutcome> mockSubscriptionEventOutcomePublisher = Mock(EventsPublisher<SubscriptionEventOutcome>)
+    EventsPublisher<CloudEvent> mockSubscriptionEventOutcomePublisher = Mock(EventsPublisher<CloudEvent>)
     @SpringBean
     SubscriptionOutcomeMapper subscriptionOutcomeMapper = Mappers.getMapper(SubscriptionOutcomeMapper)
 
     @Autowired
     JsonObjectMapper jsonObjectMapper
 
+    @Autowired
+    ObjectMapper objectMapper
+
     def 'Send response to the client apps successfully'() {
-        given: 'a subscription client id and subscription name'
-            def clientId = 'some-client-id'
-            def subscriptionName = 'some-subscription-name'
-        and: 'the persistence service return a data node'
+        given: 'a subscription response event'
+            def subscriptionResponseJsonData = TestUtils.getResourceFileContent('avcSubscriptionEventResponse.json')
+            def subscriptionResponseEvent = jsonObjectMapper.convertJsonString(subscriptionResponseJsonData, SubscriptionEventResponse.class)
+        and: 'a subscription outcome event'
+            def subscriptionOutcomeJsonData = TestUtils.getResourceFileContent('avcSubscriptionOutcomeEvent2.json')
+            def subscriptionOutcomeEvent = jsonObjectMapper.convertJsonString(subscriptionOutcomeJsonData, SubscriptionEventOutcome.class)
+        and: 'a random id for the cloud event'
+            SubscriptionOutcomeCloudMapper.randomId = 'some-id'
+        and: 'a cloud event containing the outcome event'
+            def testCloudEventSent = CloudEventBuilder.v1()
+                .withData(objectMapper.writeValueAsBytes(subscriptionOutcomeEvent))
+                .withId('some-id')
+                .withType('subscriptionCreatedStatus')
+                .withDataSchema(URI.create('urn:cps:' + 'org.onap.cps.ncmp.events.avcsubscription1_0_0.ncmp_to_client.SubscriptionEventOutcome' + ':1.0.0'))
+                .withExtension("correlationid", 'SCO-9989752cm-subscription-001')
+                .withSource(URI.create('NCMP')).build()
+        and: 'the persistence service return a data node that includes pending cm handles that makes it partial success'
             mockSubscriptionPersistence.getCmHandlesForSubscriptionEvent(*_) >> [dataNode4]
-        and: 'the response is being generated from the db'
-            def eventOutcome = objectUnderTest.generateResponse(clientId, subscriptionName)
         when: 'the response is being sent'
-            objectUnderTest.sendResponse(clientId, subscriptionName)
-        then: 'the publisher publish the response with expected parameters'
-            1 * mockSubscriptionEventOutcomePublisher.publishEvent('cm-avc-subscription-response', clientId + subscriptionName, new RecordHeaders(), eventOutcome)
+            objectUnderTest.sendResponse(subscriptionResponseEvent, 'subscriptionCreatedStatus')
+        then: 'the publisher publish the cloud event with itself and expected parameters'
+            1 * mockSubscriptionEventOutcomePublisher.publishCloudEvent('subscription-response', 'SCO-9989752cm-subscription-001', testCloudEventSent)
+    }
+
+    def 'Create subscription outcome message as expected'() {
+        given: 'a subscription response event'
+            def subscriptionResponseJsonData = TestUtils.getResourceFileContent('avcSubscriptionEventResponse.json')
+            def subscriptionResponseEvent = jsonObjectMapper.convertJsonString(subscriptionResponseJsonData, SubscriptionEventResponse.class)
+        and: 'a subscription outcome event'
+            def subscriptionOutcomeJsonData = TestUtils.getResourceFileContent('avcSubscriptionOutcomeEvent.json')
+            def subscriptionOutcomeEvent = jsonObjectMapper.convertJsonString(subscriptionOutcomeJsonData, SubscriptionEventOutcome.class)
+        and: 'a status code and status message a per #scenarios'
+            subscriptionOutcomeEvent.getData().setStatusCode(statusCode)
+            subscriptionOutcomeEvent.getData().setStatusMessage(statusMessage)
+        when: 'a subscription event outcome message is being formed'
+            def result = objectUnderTest.fromSubscriptionEventResponse(subscriptionResponseEvent, ncmpEventResponseCode)
+        then: 'the result will be equal to event outcome'
+            result == subscriptionOutcomeEvent
+        where: 'the following values are used'
+            scenario             | ncmpEventResponseCode                                        || statusMessage                          ||  statusCode
+            'is full outcome'    | NcmpEventResponseCode.SUCCESSFULLY_APPLIED_SUBSCRIPTION      || 'successfully applied subscription'    ||  1
+            'is partial outcome' | NcmpEventResponseCode.PARTIALLY_APPLIED_SUBSCRIPTION         || 'partially applied subscription'       ||  104
     }
 
     def 'Check cm handle id to status map to see if it is a full outcome response'() {
         when: 'is full outcome response evaluated'
-            def response = objectUnderTest.isFullOutcomeResponse(cmHandleIdToStatusMap)
+            def response = objectUnderTest.decideOnNcmpEventResponseCodeForSubscription(cmHandleIdToStatusAndDetailsAsMap)
         then: 'the result will be as expected'
-            response == expectedResult
+            response == expectedOutcomeResponseDecision
         where: 'the following values are used'
-            scenario                                          | cmHandleIdToStatusMap                                                                       || expectedResult
-            'The map contains PENDING status'                 | ['CMHandle1': SubscriptionStatus.PENDING] as Map                                            || false
-            'The map contains ACCEPTED status'                | ['CMHandle1': SubscriptionStatus.ACCEPTED] as Map                                           || true
-            'The map contains REJECTED status'                | ['CMHandle1': SubscriptionStatus.REJECTED] as Map                                           || true
-            'The map contains PENDING and ACCEPTED statuses'  | ['CMHandle1': SubscriptionStatus.PENDING,'CMHandle2': SubscriptionStatus.ACCEPTED] as Map   || false
-            'The map contains REJECTED and ACCEPTED statuses' | ['CMHandle1': SubscriptionStatus.REJECTED,'CMHandle2': SubscriptionStatus.ACCEPTED] as Map  || true
-            'The map contains PENDING and REJECTED statuses'  | ['CMHandle1': SubscriptionStatus.PENDING,'CMHandle2': SubscriptionStatus.REJECTED] as Map   || false
+            scenario                                          | cmHandleIdToStatusAndDetailsAsMap                                                                                                                                                   || expectedOutcomeResponseDecision
+            'The map contains PENDING status'                 | [CMHandle1: [details:'Subscription forwarded to dmi plugin',status:'PENDING'] as Map] as Map                                                                                        || NcmpEventResponseCode.SUBSCRIPTION_PENDING
+            'The map contains ACCEPTED status'                | [CMHandle1: [details:'',status:'ACCEPTED'] as Map] as Map                                                                                                                           || NcmpEventResponseCode.SUCCESSFULLY_APPLIED_SUBSCRIPTION
+            'The map contains REJECTED status'                | [CMHandle1: [details:'Cm handle does not exist',status:'REJECTED'] as Map] as Map                                                                                                   || NcmpEventResponseCode.SUBSCRIPTION_NOT_APPLICABLE
+            'The map contains PENDING and PENDING statuses'   | [CMHandle1: [details:'Some details',status:'PENDING'] as Map, CMHandle2: [details:'Some details',status:'PENDING'] as Map as Map] as Map                                            || NcmpEventResponseCode.SUBSCRIPTION_PENDING
+            'The map contains ACCEPTED and ACCEPTED statuses' | [CMHandle1: [details:'',status:'ACCEPTED'] as Map, CMHandle2: [details:'',status:'ACCEPTED'] as Map as Map] as Map                                                                  || NcmpEventResponseCode.SUCCESSFULLY_APPLIED_SUBSCRIPTION
+            'The map contains REJECTED and REJECTED statuses' | [CMHandle1: [details:'Reject details',status:'REJECTED'] as Map, CMHandle2: [details:'Reject details',status:'REJECTED'] as Map as Map] as Map                                      || NcmpEventResponseCode.SUBSCRIPTION_NOT_APPLICABLE
+            'The map contains PENDING and ACCEPTED statuses'  | [CMHandle1: [details:'Some details',status:'PENDING'] as Map, CMHandle2: [details:'',status:'ACCEPTED'] as Map as Map] as Map                                                       || NcmpEventResponseCode.PARTIALLY_APPLIED_SUBSCRIPTION
+            'The map contains REJECTED and ACCEPTED statuses' | [CMHandle1: [details:'Cm handle does not exist',status:'REJECTED'] as Map, CMHandle2: [details:'',status:'ACCEPTED'] as Map as Map] as Map                                          || NcmpEventResponseCode.PARTIALLY_APPLIED_SUBSCRIPTION
+            'The map contains PENDING and REJECTED statuses'  | [CMHandle1: [details:'Subscription forwarded to dmi plugin',status:'PENDING'] as Map, CMHandle2: [details:'Cm handle does not exist',status:'REJECTED'] as Map as Map] as Map       || NcmpEventResponseCode.PARTIALLY_APPLIED_SUBSCRIPTION
     }
 
-    def 'Generate response via fetching data nodes from database.'() {
-        given: 'a db call to get data nodes for subscription event'
-            1 * mockSubscriptionPersistence.getCmHandlesForSubscriptionEvent(*_) >> [dataNode4]
-        when: 'a response is generated'
-            def result = objectUnderTest.generateResponse('some-client-id', 'some-subscription-name')
-        then: 'the result will have the same values as same as in dataNode4'
-            result.eventType == SubscriptionEventOutcome.EventType.PARTIAL_OUTCOME
-            result.getEvent().getSubscription().getClientID() == 'some-client-id'
-            result.getEvent().getSubscription().getName() == 'some-subscription-name'
-            result.getEvent().getPredicates().getPendingTargets() == ['CMHandle3']
-            result.getEvent().getPredicates().getRejectedTargets() == ['CMHandle1']
-            result.getEvent().getPredicates().getAcceptedTargets() == ['CMHandle2']
-    }
-
-    def 'Form subscription outcome message with a list of cm handle id to status mapping'() {
-        given: 'a list of collection including cm handle id to status'
-            def cmHandleIdToStatus = [['PENDING', 'CMHandle5'], ['PENDING', 'CMHandle4'], ['ACCEPTED', 'CMHandle1'], ['REJECTED', 'CMHandle3']]
-        and: 'an outcome event'
-            def jsonData = TestUtils.getResourceFileContent('avcSubscriptionOutcomeEvent.json')
-            def eventOutcome = jsonObjectMapper.convertJsonString(jsonData, SubscriptionEventOutcome.class)
-            eventOutcome.setEventType(expectedEventType)
-        when: 'a subscription outcome message formed'
-            def result = objectUnderTest.formSubscriptionOutcomeMessage(cmHandleIdToStatus, 'SCO-9989752',
-                'cm-subscription-001', isFullOutcomeResponse)
-            result.getEvent().getPredicates().getPendingTargets().sort()
-        then: 'the result will be equal to event outcome'
-            result == eventOutcome
-        where: 'the following values are used'
-            scenario             | isFullOutcomeResponse || expectedEventType
-            'is full outcome'    | true                  || SubscriptionEventOutcome.EventType.COMPLETE_OUTCOME
-            'is partial outcome' | false                 || SubscriptionEventOutcome.EventType.PARTIAL_OUTCOME
-    }
 }
index 7f1a628..f5fbdfc 100644 (file)
@@ -22,9 +22,10 @@ package org.onap.cps.ncmp.api.impl.events.avcsubscription
 
 import com.fasterxml.jackson.databind.ObjectMapper
 import org.mapstruct.factory.Mappers
-import org.onap.cps.ncmp.api.models.SubscriptionEventResponse
-import org.onap.cps.ncmp.events.avc.subscription.v1.SubscriptionEventOutcome
+import org.onap.cps.ncmp.events.avcsubscription1_0_0.dmi_to_ncmp.SubscriptionEventResponse
+import org.onap.cps.ncmp.events.avcsubscription1_0_0.dmi_to_ncmp.SubscriptionStatus
 import org.onap.cps.ncmp.utils.TestUtils
+import org.onap.cps.spi.exceptions.DataValidationException
 import org.onap.cps.utils.JsonObjectMapper
 import org.springframework.beans.factory.annotation.Autowired
 import org.springframework.boot.test.context.SpringBootTest
@@ -43,19 +44,44 @@ class SubscriptionOutcomeMapperSpec extends Specification {
         given: 'a Subscription Response Event'
             def subscriptionResponseJsonData = TestUtils.getResourceFileContent('avcSubscriptionEventResponse.json')
             def subscriptionResponseEvent = jsonObjectMapper.convertJsonString(subscriptionResponseJsonData, SubscriptionEventResponse.class)
-        and: 'a Subscription Outcome Event'
-            def jsonDataOutcome = TestUtils.getResourceFileContent('avcSubscriptionOutcomeEvent.json')
-            def expectedEventOutcome = jsonObjectMapper.convertJsonString(jsonDataOutcome, SubscriptionEventOutcome.class)
-            expectedEventOutcome.setEventType(expectedEventType)
         when: 'the subscription response event is mapped to a subscription event outcome'
             def result = objectUnderTest.toSubscriptionEventOutcome(subscriptionResponseEvent)
-            result.setEventType(expectedEventType)
-        then: 'the resulting subscription event outcome contains the correct clientId'
-            assert result == expectedEventOutcome
+        then: 'the resulting subscription event outcome contains expected pending targets per details grouping'
+            def pendingCmHandleTargetsPerDetails = result.getData().getAdditionalInfo().getPending()
+            assert pendingCmHandleTargetsPerDetails.get(0).getDetails() == 'EMS or node connectivity issues, retrying'
+            assert pendingCmHandleTargetsPerDetails.get(0).getTargets() == ['CMHandle5', 'CMHandle6','CMHandle7']
+        and: 'the resulting subscription event outcome contains expected rejected targets per details grouping'
+            def rejectedCmHandleTargetsPerDetails = result.getData().getAdditionalInfo().getRejected()
+            assert rejectedCmHandleTargetsPerDetails.get(0).getDetails() == 'Target(s) do not exist'
+            assert rejectedCmHandleTargetsPerDetails.get(0).getTargets() == ['CMHandle4']
+            assert rejectedCmHandleTargetsPerDetails.get(1).getDetails() == 'Faulty subscription format for target(s)'
+            assert rejectedCmHandleTargetsPerDetails.get(1).getTargets() == ['CMHandle1', 'CMHandle2','CMHandle3']
+    }
+
+    def 'Map subscription event response with null of subscription status list to subscription event outcome causes an exception'() {
+        given: 'a Subscription Response Event'
+            def subscriptionResponseJsonData = TestUtils.getResourceFileContent('avcSubscriptionEventResponse.json')
+            def subscriptionResponseEvent = jsonObjectMapper.convertJsonString(subscriptionResponseJsonData, SubscriptionEventResponse.class)
+        and: 'set subscription status list to null'
+            subscriptionResponseEvent.getData().setSubscriptionStatus(subscriptionStatusList)
+        when: 'the subscription response event is mapped to a subscription event outcome'
+            objectUnderTest.toSubscriptionEventOutcome(subscriptionResponseEvent)
+        then: 'a DataValidationException is thrown with an expected exception details'
+            def exception = thrown(DataValidationException)
+            exception.details == 'SubscriptionStatus list cannot be null or empty'
         where: 'the following values are used'
-            scenario              || expectedEventType
-            'is full outcome'     || SubscriptionEventOutcome.EventType.COMPLETE_OUTCOME
-            'is partial outcome'  || SubscriptionEventOutcome.EventType.PARTIAL_OUTCOME
+            scenario                            ||     subscriptionStatusList
+            'A null subscription status list'   ||      null
+            'An empty subscription status list' ||      new ArrayList<SubscriptionStatus>()
     }
 
+    def 'Map subscription event response with subscription status list to subscription event outcome without any exception'() {
+        given: 'a Subscription Response Event'
+            def subscriptionResponseJsonData = TestUtils.getResourceFileContent('avcSubscriptionEventResponse.json')
+            def subscriptionResponseEvent = jsonObjectMapper.convertJsonString(subscriptionResponseJsonData, SubscriptionEventResponse.class)
+        when: 'the subscription response event is mapped to a subscription event outcome'
+            objectUnderTest.toSubscriptionEventOutcome(subscriptionResponseEvent)
+        then: 'no exception thrown'
+            noExceptionThrown()
+    }
 }
\ No newline at end of file
index ec54e89..7116a17 100644 (file)
@@ -59,14 +59,15 @@ class SubscriptionPersistenceSpec extends Specification {
                 SUBSCRIPTION_REGISTRY_PARENT,
                 '{"subscription":[{' +
                     '"topic":"some-topic",' +
-                    '"predicates":{"datastore":"some-datastore","targetCmHandles":[{"cmHandleId":"cmhandle1","status":"PENDING"},{"cmHandleId":"cmhandle2","status":"PENDING"}]},' +
+                    '"predicates":{"datastore":"some-datastore","targetCmHandles":[{"cmHandleId":"cmhandle1","status":"PENDING","details":"Subscription forwarded to dmi plugin"},' +
+                    '{"cmHandleId":"cmhandle2","status":"PENDING","details":"Subscription forwarded to dmi plugin"}]},' +
                     '"clientID":"some-client-id","subscriptionName":"some-subscription-name","isTagged":true}]}',
                 NO_TIMESTAMP)
    }
 
     def 'add or replace cm handle list element into db' () {
         given: 'a data node with child node exist in db'
-            def leaves1 = [status:'PENDING', cmHandleId:'cmhandle1'] as Map
+            def leaves1 = [status:'REJECTED', cmHandleId:'cmhandle1', details:'Cm handle does not exist'] as Map
             def childDataNode = new DataNodeBuilder().withDataspace('NCMP-Admin')
                 .withAnchor('AVC-Subscriptions').withXpath('/subscription-registry/subscription')
                 .withLeaves(leaves1).build()
@@ -81,11 +82,11 @@ class SubscriptionPersistenceSpec extends Specification {
             objectUnderTest.saveSubscriptionEvent(yangModelSubscriptionEvent)
         then: 'the cpsDataService save non-existing cm handle with the correct data'
             1 * mockCpsDataService.saveListElements(SUBSCRIPTION_DATASPACE_NAME, SUBSCRIPTION_ANCHOR_NAME,
-                SUBSCRIPTION_REGISTRY_PREDICATES_XPATH, '{"targetCmHandles":[{"cmHandleId":"cmhandle2","status":"PENDING"}]}',
+                SUBSCRIPTION_REGISTRY_PREDICATES_XPATH, '{"targetCmHandles":[{"cmHandleId":"cmhandle2","status":"PENDING","details":"Subscription forwarded to dmi plugin"}]}',
                 NO_TIMESTAMP)
         and: 'the cpsDataService update existing cm handle with the correct data'
             1 * mockCpsDataService.updateNodeLeaves(SUBSCRIPTION_DATASPACE_NAME, SUBSCRIPTION_ANCHOR_NAME,
-                SUBSCRIPTION_REGISTRY_PREDICATES_XPATH, '{"targetCmHandles":[{"cmHandleId":"cmhandle1","status":"PENDING"}]}',
+                SUBSCRIPTION_REGISTRY_PREDICATES_XPATH, '{"targetCmHandles":[{"cmHandleId":"cmhandle1","status":"PENDING","details":"Subscription forwarded to dmi plugin"}]}',
                 NO_TIMESTAMP)
     }
 
index 7474166..e28a102 100644 (file)
@@ -25,13 +25,13 @@ import spock.lang.Specification
 
 class DataNodeBaseSpec extends Specification {
 
-    def leaves1 = [status:'PENDING', cmHandleId:'CMHandle3'] as Map
+    def leaves1 = [status:'PENDING', cmHandleId:'CMHandle3', details:'Subscription forwarded to dmi plugin'] as Map
     def dataNode1 = createDataNodeWithLeaves(leaves1)
 
-    def leaves2 = [status:'ACCEPTED', cmHandleId:'CMHandle2'] as Map
+    def leaves2 = [status:'ACCEPTED', cmHandleId:'CMHandle2', details:''] as Map
     def dataNode2 = createDataNodeWithLeaves(leaves2)
 
-    def leaves3 = [status:'REJECTED', cmHandleId:'CMHandle1'] as Map
+    def leaves3 = [status:'REJECTED', cmHandleId:'CMHandle1', details:'Cm handle does not exist'] as Map
     def dataNode3 = createDataNodeWithLeaves(leaves3)
 
     def leaves4 = [datastore:'passthrough-running'] as Map
index 819f1fa..28db7ba 100644 (file)
@@ -20,7 +20,6 @@
 
 package org.onap.cps.ncmp.api.impl.utils
 
-import org.onap.cps.ncmp.api.impl.subscriptions.SubscriptionStatus
 import org.onap.cps.spi.model.DataNodeBuilder
 
 class DataNodeHelperSpec extends DataNodeBaseSpec {
@@ -38,9 +37,9 @@ class DataNodeHelperSpec extends DataNodeBaseSpec {
         and: 'all the leaves result list are equal to given leaves of data nodes'
             result[0] == [clientID:'SCO-9989752', isTagged:false, subscriptionName:'cm-subscription-001']
             result[1] == [datastore:'passthrough-running']
-            result[2] == [status:'PENDING', cmHandleId:'CMHandle3']
-            result[3] == [status:'ACCEPTED', cmHandleId:'CMHandle2']
-            result[4] == [status:'REJECTED', cmHandleId:'CMHandle1']
+            result[2] == [status:'PENDING', cmHandleId:'CMHandle3', details:'Subscription forwarded to dmi plugin']
+            result[3] == [status:'ACCEPTED', cmHandleId:'CMHandle2', details:'']
+            result[4] == [status:'REJECTED', cmHandleId:'CMHandle1', details:'Cm handle does not exist']
     }
 
     def 'Get cm handle id to status as expected from a nested data node.'() {
@@ -52,26 +51,18 @@ class DataNodeHelperSpec extends DataNodeBaseSpec {
         and: 'the nested data node is flatten and retrieves the leaves '
             def leaves = DataNodeHelper.getDataNodeLeaves([dataNode])
         when:'cm handle id to status is retrieved'
-            def result = DataNodeHelper.getCmHandleIdToStatus(leaves)
+            def result = DataNodeHelper.cmHandleIdToStatusAndDetailsAsMap(leaves)
         then: 'the result list size is 3'
             result.size() == 3
         and: 'the result contains expected values'
-            result[0] as List == ['PENDING', 'CMHandle3']
-            result[1] as List == ['ACCEPTED', 'CMHandle2']
-            result[2] as List == ['REJECTED', 'CMHandle1']
-    }
+            result == [
+                CMHandle3: [details:'Subscription forwarded to dmi plugin',status:'PENDING'] as Map,
+                CMHandle2: [details:'',status:'ACCEPTED'] as Map,
+                CMHandle1: [details:'Cm handle does not exist',status:'REJECTED'] as Map
+            ] as Map
 
-    def 'Get cm handle id to status map as expected from list of collection' () {
-        given: 'a list of collection'
-            def cmHandleCollection = [['PENDING', 'CMHandle3'], ['ACCEPTED', 'CMHandle2'], ['REJECTED', 'CMHandle1']]
-        when: 'the map is formed up with a method call'
-            def result = DataNodeHelper.getCmHandleIdToStatusMap(cmHandleCollection)
-        then: 'the map values are as expected'
-            result.keySet() == ['CMHandle3', 'CMHandle2', 'CMHandle1'] as Set
-            result.values() as List == [SubscriptionStatus.PENDING, SubscriptionStatus.ACCEPTED, SubscriptionStatus.REJECTED]
     }
 
-
     def 'Get cm handle id to status map as expected from a nested data node.'() {
         given: 'a nested data node'
             def dataNode = new DataNodeBuilder().withDataspace('NCMP-Admin')
@@ -79,8 +70,14 @@ class DataNodeHelperSpec extends DataNodeBaseSpec {
                 .withLeaves([clientID:'SCO-9989752', isTagged:false, subscriptionName:'cm-subscription-001'])
                 .withChildDataNodes([dataNode4]).build()
         when:'cm handle id to status is being extracted'
-            def result = DataNodeHelper.getCmHandleIdToStatusMapFromDataNodes([dataNode]);
-        then: 'the keys are retrieved as expected'
-            result.keySet() == ['CMHandle3','CMHandle2','CMHandle1'] as Set
+            def result = DataNodeHelper.cmHandleIdToStatusAndDetailsAsMapFromDataNode([dataNode]);
+        then: 'the result list size is 3'
+            result.size() == 3
+        and: 'the result contains expected values'
+            result == [
+                CMHandle3: [details:'Subscription forwarded to dmi plugin',status:'PENDING'] as Map,
+                CMHandle2: [details:'',status:'ACCEPTED'] as Map,
+                CMHandle1: [details:'Cm handle does not exist',status:'REJECTED'] as Map
+            ] as Map
     }
 }
index 61eb319..bc19e2d 100644 (file)
@@ -24,6 +24,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
 import io.cloudevents.core.builder.CloudEventBuilder
 import org.onap.cps.ncmp.events.avcsubscription1_0_0.client_to_ncmp.SubscriptionEvent
 import org.onap.cps.ncmp.utils.TestUtils
+import org.onap.cps.spi.exceptions.CloudEventConstructionException
 import org.onap.cps.utils.JsonObjectMapper
 import org.springframework.beans.factory.annotation.Autowired
 import org.springframework.boot.test.context.SpringBootTest
@@ -45,7 +46,7 @@ class SubscriptionEventCloudMapperSpec extends Specification {
             def testCloudEvent = CloudEventBuilder.v1()
                 .withData(objectMapper.writeValueAsBytes(testEventData))
                 .withId('some-event-id')
-                .withType('CREATE')
+                .withType('subscriptionCreated')
                 .withSource(URI.create('some-resource'))
                 .withExtension('correlationid', 'test-cmhandle1').build()
         when: 'the cloud event map to subscription event'
@@ -59,7 +60,7 @@ class SubscriptionEventCloudMapperSpec extends Specification {
             def testCloudEvent = CloudEventBuilder.v1()
                 .withData(null)
                 .withId('some-event-id')
-                .withType('CREATE')
+                .withType('subscriptionCreated')
                 .withSource(URI.create('some-resource'))
                 .withExtension('correlationid', 'test-cmhandle1').build()
         when: 'the cloud event map to subscription event'
@@ -75,30 +76,29 @@ class SubscriptionEventCloudMapperSpec extends Specification {
                                 org.onap.cps.ncmp.events.avcsubscription1_0_0.ncmp_to_dmi.SubscriptionEvent.class)
             def testCloudEvent = CloudEventBuilder.v1()
                 .withData(objectMapper.writeValueAsBytes(testEventData))
-                .withId('some-event-key')
-                .withType('CREATE')
-                .withSource(URI.create('some-resource'))
+                .withId('some-id')
+                .withType('subscriptionCreated')
+                .withSource(URI.create('SCO-9989752'))
                 .withExtension('correlationid', 'test-cmhandle1').build()
         when: 'the subscription event map to data of cloud event'
-            def resultCloudEvent = SubscriptionEventCloudMapper.toCloudEvent(testEventData, 'some-event-key')
+            SubscriptionEventCloudMapper.randomId = 'some-id'
+            def resultCloudEvent = SubscriptionEventCloudMapper.toCloudEvent(testEventData, 'some-event-key', 'subscriptionCreated')
         then: 'the subscription event resulted having expected values'
             resultCloudEvent.getData() == testCloudEvent.getData()
             resultCloudEvent.getId() == testCloudEvent.getId()
             resultCloudEvent.getType() == testCloudEvent.getType()
+            resultCloudEvent.getSource() == URI.create('SCO-9989752')
+            resultCloudEvent.getDataSchema() == URI.create('urn:cps:org.onap.cps.ncmp.events.avcsubscription1_0_0.ncmp_to_dmi.SubscriptionEvent:1.0.0')
     }
 
     def 'Map the subscription event to data of the cloud event with wrong content causes an exception'() {
         given: 'an empty ncmp subscription event'
             def testNcmpSubscriptionEvent = new org.onap.cps.ncmp.events.avcsubscription1_0_0.ncmp_to_dmi.SubscriptionEvent()
         when: 'the subscription event map to data of cloud event'
-            def thrownException = null
-            try {
-                SubscriptionEventCloudMapper.toCloudEvent(testNcmpSubscriptionEvent, 'some-key')
-            } catch (Exception e) {
-                thrownException  = e
-            }
+            SubscriptionEventCloudMapper.toCloudEvent(testNcmpSubscriptionEvent, 'some-key', 'some-event-type')
         then: 'a run time exception is thrown'
-            assert thrownException instanceof RuntimeException
+            def exception = thrown(CloudEventConstructionException)
+            exception.details == 'Invalid object to serialize or required headers is missing'
     }
 
 }
index edbd702..7442670 100644 (file)
@@ -30,7 +30,7 @@ app:
         async-m2m:
             topic: ncmp-async-m2m
         avc:
-            subscription-topic: cm-avc-subscription
+            subscription-topic: subscription
             cm-events-topic: cm-events
             subscription-forward-topic-prefix: ${NCMP_FORWARD_CM_AVC_SUBSCRIPTION:ncmp-dmi-cm-avc-subscription-}
 
index 3244f05..52ca1df 100644 (file)
@@ -1,11 +1,44 @@
 {
-  "clientId": "SCO-9989752",
-  "subscriptionName": "cm-subscription-001",
-  "dmiName": "ncmp-dmi-plugin",
-  "cmHandleIdToStatus": {
-    "CMHandle1": "ACCEPTED",
-    "CMHandle3": "REJECTED",
-    "CMHandle4": "PENDING",
-    "CMHandle5": "PENDING"
+  "data": {
+    "clientId": "SCO-9989752",
+    "subscriptionName": "cm-subscription-001",
+    "dmiName": "dminame1",
+    "subscriptionStatus": [
+      {
+        "id": "CMHandle1",
+        "status": "REJECTED",
+        "details": "Faulty subscription format for target(s)"
+      },
+      {
+        "id": "CMHandle2",
+        "status": "REJECTED",
+        "details": "Faulty subscription format for target(s)"
+      },
+      {
+        "id": "CMHandle3",
+        "status": "REJECTED",
+        "details": "Faulty subscription format for target(s)"
+      },
+      {
+        "id": "CMHandle4",
+        "status": "REJECTED",
+        "details": "Target(s) do not exist"
+      },
+      {
+        "id": "CMHandle5",
+        "status": "PENDING",
+        "details": "EMS or node connectivity issues, retrying"
+      },
+      {
+        "id": "CMHandle6",
+        "status": "PENDING",
+        "details": "EMS or node connectivity issues, retrying"
+      },
+      {
+        "id": "CMHandle7",
+        "status": "PENDING",
+        "details": "EMS or node connectivity issues, retrying"
+      }
+    ]
   }
 }
\ No newline at end of file
index 6bfa36b..2d83bdd 100644 (file)
@@ -1,20 +1,23 @@
 {
-  "eventType": "PARTIAL_OUTCOME",
-  "event": {
-    "subscription": {
-      "clientID": "SCO-9989752",
-      "name": "cm-subscription-001"
-    },
-    "predicates": {
-      "rejectedTargets": [
-        "CMHandle3"
+  "data": {
+    "statusCode": 104,
+    "statusMessage": "partially applied subscription",
+    "additionalInfo": {
+      "rejected": [
+        {
+          "details": "Target(s) do not exist",
+          "targets": ["CMHandle4"]
+        },
+        {
+          "details": "Faulty subscription format for target(s)",
+          "targets": ["CMHandle1", "CMHandle2", "CMHandle3"]
+        }
       ],
-      "acceptedTargets": [
-        "CMHandle1"
-      ],
-      "pendingTargets": [
-        "CMHandle4",
-        "CMHandle5"
+      "pending": [
+        {
+          "details": "EMS or node connectivity issues, retrying",
+          "targets": ["CMHandle5", "CMHandle6", "CMHandle7"]
+        }
       ]
     }
   }
diff --git a/cps-ncmp-service/src/test/resources/avcSubscriptionOutcomeEvent2.json b/cps-ncmp-service/src/test/resources/avcSubscriptionOutcomeEvent2.json
new file mode 100644 (file)
index 0000000..35ff024
--- /dev/null
@@ -0,0 +1,20 @@
+{
+  "data": {
+    "statusCode": 104,
+    "statusMessage": "partially applied subscription",
+    "additionalInfo": {
+      "rejected": [
+        {
+          "details": "Cm handle does not exist",
+          "targets": ["CMHandle1"]
+        }
+      ],
+      "pending": [
+        {
+          "details": "Subscription forwarded to dmi plugin",
+          "targets": ["CMHandle3"]
+        }
+      ]
+    }
+  }
+}
\ No newline at end of file
index f4afe3d..f904e8b 100644 (file)
@@ -120,7 +120,7 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
         newChildAsFragmentEntity.setParentId(parentFragmentEntity.getId());
         try {
             fragmentRepository.save(newChildAsFragmentEntity);
-        } catch (final DataIntegrityViolationException e) {
+        } catch (final DataIntegrityViolationException dataIntegrityViolationException) {
             throw AlreadyDefinedException.forDataNodes(Collections.singletonList(newChild.getXpath()),
                     anchorEntity.getName());
         }
@@ -138,9 +138,9 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
                 fragmentEntities.add(newChildAsFragmentEntity);
             }
             fragmentRepository.saveAll(fragmentEntities);
-        } catch (final DataIntegrityViolationException e) {
+        } catch (final DataIntegrityViolationException dataIntegrityViolationException) {
             log.warn("Exception occurred : {} , While saving : {} children, retrying using individual save operations",
-                    e, fragmentEntities.size());
+                    dataIntegrityViolationException, fragmentEntities.size());
             retrySavingEachChildIndividually(anchorEntity, parentNodeXpath, newChildren);
         }
     }
@@ -151,7 +151,7 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
         for (final DataNode newChild : newChildren) {
             try {
                 addNewChildDataNode(anchorEntity, parentNodeXpath, newChild);
-            } catch (final AlreadyDefinedException e) {
+            } catch (final AlreadyDefinedException alreadyDefinedException) {
                 failedXpaths.add(newChild.getXpath());
             }
         }
@@ -184,7 +184,7 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
             try {
                 final FragmentEntity fragmentEntity = convertToFragmentWithAllDescendants(anchorEntity, dataNode);
                 fragmentRepository.save(fragmentEntity);
-            } catch (final DataIntegrityViolationException e) {
+            } catch (final DataIntegrityViolationException dataIntegrityViolationException) {
                 failedXpaths.add(dataNode.getXpath());
             }
         }
@@ -251,22 +251,28 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
 
     private Collection<FragmentEntity> getFragmentEntities(final AnchorEntity anchorEntity,
                                                            final Collection<String> xpaths) {
-        final Collection<String> nonRootXpaths = new HashSet<>(xpaths);
-        final boolean haveRootXpath = nonRootXpaths.removeIf(CpsDataPersistenceServiceImpl::isRootXpath);
+        final Collection<String> normalizedXpaths = getNormalizedXpaths(xpaths);
 
-        final Collection<String> normalizedXpaths = new HashSet<>(nonRootXpaths.size());
-        for (final String xpath : nonRootXpaths) {
-            try {
-                normalizedXpaths.add(CpsPathUtil.getNormalizedXpath(xpath));
-            } catch (final PathParsingException e) {
-                log.warn("Error parsing xpath \"{}\": {}", xpath, e.getMessage());
+        final boolean haveRootXpath = normalizedXpaths.removeIf(CpsDataPersistenceServiceImpl::isRootXpath);
+
+        final List<FragmentEntity> fragmentEntities = fragmentRepository.findByAnchorAndXpathIn(anchorEntity,
+                normalizedXpaths);
+
+        for (final FragmentEntity fragmentEntity : fragmentEntities) {
+            normalizedXpaths.remove(fragmentEntity.getXpath());
+        }
+
+        for (final String xpath : normalizedXpaths) {
+            if (!CpsPathUtil.isPathToListElement(xpath)) {
+                fragmentEntities.addAll(fragmentRepository.findListByAnchorAndXpath(anchorEntity, xpath));
             }
         }
+
         if (haveRootXpath) {
-            normalizedXpaths.addAll(fragmentRepository.findAllXpathByAnchorAndParentIdIsNull(anchorEntity));
+            fragmentEntities.addAll(fragmentRepository.findRootsByAnchorId(anchorEntity.getId()));
         }
 
-        return fragmentRepository.findByAnchorAndXpathIn(anchorEntity, normalizedXpaths);
+        return fragmentEntities;
     }
 
     private FragmentEntity getFragmentEntity(final AnchorEntity anchorEntity, final String xpath) {
@@ -293,8 +299,8 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
         final CpsPathQuery cpsPathQuery;
         try {
             cpsPathQuery = CpsPathUtil.getCpsPathQuery(cpsPath);
-        } catch (final PathParsingException e) {
-            throw new CpsPathException(e.getMessage());
+        } catch (final PathParsingException pathParsingException) {
+            throw new CpsPathException(pathParsingException.getMessage());
         }
 
         Collection<FragmentEntity> fragmentEntities;
@@ -337,11 +343,23 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
         }
         try {
             return CpsPathUtil.getNormalizedXpath(xpathSource);
-        } catch (final PathParsingException e) {
-            throw new CpsPathException(e.getMessage());
+        } catch (final PathParsingException pathParsingException) {
+            throw new CpsPathException(pathParsingException.getMessage());
         }
     }
 
+    private static Collection<String> getNormalizedXpaths(final Collection<String> xpaths) {
+        final Collection<String> normalizedXpaths = new HashSet<>(xpaths.size());
+        for (final String xpath : xpaths) {
+            try {
+                normalizedXpaths.add(getNormalizedXpath(xpath));
+            } catch (final CpsPathException cpsPathException) {
+                log.warn("Error parsing xpath \"{}\": {}", xpath, cpsPathException.getMessage());
+            }
+        }
+        return normalizedXpaths;
+    }
+
     @Override
     public String startSession() {
         return sessionManager.startSession();
@@ -450,7 +468,7 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
         for (final FragmentEntity dataNodeFragment : fragmentEntities) {
             try {
                 fragmentRepository.save(dataNodeFragment);
-            } catch (final StaleStateException e) {
+            } catch (final StaleStateException staleStateException) {
                 failedXpaths.add(dataNodeFragment.getXpath());
             }
         }
@@ -542,15 +560,7 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
 
         final AnchorEntity anchorEntity = getAnchorEntity(dataspaceName, anchorName);
 
-        final Collection<String> deleteChecklist = new HashSet<>(xpathsToDelete.size());
-        for (final String xpath : xpathsToDelete) {
-            try {
-                deleteChecklist.add(CpsPathUtil.getNormalizedXpath(xpath));
-            } catch (final PathParsingException e) {
-                log.warn("Error parsing xpath \"{}\": {}", xpath, e.getMessage());
-            }
-        }
-
+        final Collection<String> deleteChecklist = getNormalizedXpaths(xpathsToDelete);
         final Collection<String> xpathsToExistingContainers =
             fragmentRepository.findAllXpathByAnchorAndXpathIn(anchorEntity, deleteChecklist);
         if (onlySupportListDeletion) {
index 303af5b..7d5be13 100755 (executable)
@@ -58,6 +58,17 @@ public interface FragmentRepository extends JpaRepository<FragmentEntity, Long>,
         return findByAnchorIdAndXpathIn(anchorEntity.getId(), xpaths.toArray(new String[0]));\r
     }\r
 \r
+    @Query(value = "SELECT * FROM fragment WHERE anchor_id = :anchorId \n"\r
+            + "AND xpath LIKE :escapedXpath||'[@%]' AND xpath NOT LIKE :escapedXpath||'[@%]/%[@%]'",\r
+            nativeQuery = true)\r
+    List<FragmentEntity> findListByAnchorIdAndEscapedXpath(@Param("anchorId") long anchorId,\r
+                                                           @Param("escapedXpath") String escapedXpath);\r
+\r
+    default List<FragmentEntity> findListByAnchorAndXpath(final AnchorEntity anchorEntity, final String xpath) {\r
+        final String escapedXpath = EscapeUtils.escapeForSqlLike(xpath);\r
+        return findListByAnchorIdAndEscapedXpath(anchorEntity.getId(), escapedXpath);\r
+    }\r
+\r
     @Query(value = "SELECT fragment.* FROM fragment JOIN anchor ON anchor.id = fragment.anchor_id "\r
         + "WHERE dataspace_id = :dataspaceId AND xpath = ANY (:xpaths)", nativeQuery = true)\r
     List<FragmentEntity> findByDataspaceIdAndXpathIn(@Param("dataspaceId") int dataspaceId,\r
@@ -110,7 +121,7 @@ public interface FragmentRepository extends JpaRepository<FragmentEntity, Long>,
 \r
     boolean existsByAnchorAndXpathStartsWith(AnchorEntity anchorEntity, String xpath);\r
 \r
-    @Query("SELECT xpath FROM FragmentEntity WHERE anchor = :anchor AND parentId IS NULL")\r
-    List<String> findAllXpathByAnchorAndParentIdIsNull(@Param("anchor") AnchorEntity anchorEntity);\r
+    @Query(value = "SELECT * FROM fragment WHERE anchor_id = :anchorId AND parent_id IS NULL", nativeQuery = true)\r
+    List<FragmentEntity> findRootsByAnchorId(@Param("anchorId") long anchorId);\r
 \r
 }\r
index cb554fa..c72c304 100644 (file)
@@ -56,6 +56,7 @@ class CpsDataPersistenceServiceSpec extends Specification {
     def setup() {
         mockAnchorRepository.getByDataspaceAndName(_, _) >> anchorEntity
         mockFragmentRepository.prefetchDescendantsOfFragmentEntities(_, _) >> { fetchDescendantsOption, fragmentEntities -> fragmentEntities }
+        mockFragmentRepository.findListByAnchorAndXpath(_, [] as Set) >> []
     }
 
     def 'Storing data nodes individually when batch operation fails'(){
diff --git a/cps-service/src/main/java/org/onap/cps/spi/exceptions/CloudEventConstructionException.java b/cps-service/src/main/java/org/onap/cps/spi/exceptions/CloudEventConstructionException.java
new file mode 100644 (file)
index 0000000..1d520e7
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2020 Pantheon.tech
+ *  Modifications Copyright (C) 2020 Bell Canada
+ *  Modifications Copyright (C) 2020-2023 Nordix Foundation
+ *  ================================================================================
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ *  SPDX-License-Identifier: Apache-2.0
+ *  ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.spi.exceptions;
+
+public class CloudEventConstructionException extends CpsException {
+
+    private static final long serialVersionUID = 7747941311132087621L;
+
+    /**
+     * Constructor.
+     *
+     * @param message the error message
+     * @param details the error details
+     */
+    public CloudEventConstructionException(final String message, final String details) {
+        super(message, details);
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param message the error message
+     * @param details the error details
+     * @param cause   the error cause
+     */
+    public CloudEventConstructionException(final String message, final String details, final Throwable cause) {
+        super(message, details, cause);
+    }
+}
diff --git a/cps-service/src/main/java/org/onap/cps/spi/exceptions/SubscriptionOutcomeTypeNotFoundException.java b/cps-service/src/main/java/org/onap/cps/spi/exceptions/SubscriptionOutcomeTypeNotFoundException.java
new file mode 100644 (file)
index 0000000..6b898e8
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2020 Pantheon.tech
+ *  Modifications Copyright (C) 2020 Bell Canada
+ *  Modifications Copyright (C) 2020-2023 Nordix Foundation
+ *  ================================================================================
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ *  SPDX-License-Identifier: Apache-2.0
+ *  ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.spi.exceptions;
+
+public class SubscriptionOutcomeTypeNotFoundException extends CpsException {
+
+    private static final long serialVersionUID = 7747941311132087621L;
+
+    /**
+     * Constructor.
+     *
+     * @param message the error message
+     * @param details the error details
+     */
+    public SubscriptionOutcomeTypeNotFoundException(final String message, final String details) {
+        super(message, details);
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param message the error message
+     * @param details the error details
+     * @param cause   the error cause
+     */
+    public SubscriptionOutcomeTypeNotFoundException(final String message, final String details, final Throwable cause) {
+        super(message, details, cause);
+    }
+}
diff --git a/docs/cm-handle-lcm-events.rst b/docs/cm-handle-lcm-events.rst
new file mode 100644 (file)
index 0000000..8446834
--- /dev/null
@@ -0,0 +1,117 @@
+.. This work is licensed under a Creative Commons Attribution 4.0 International License.
+.. http://creativecommons.org/licenses/by/4.0
+.. Copyright (C) 2023 Nordix Foundation
+
+.. DO NOT CHANGE THIS LABEL FOR RELEASE NOTES - EVEN THOUGH IT GIVES A WARNING
+.. _cmHandleLcmEvents:
+
+
+CM Handle Lifecycle Management (LCM) Events
+###########################################
+
+.. toctree::
+   :maxdepth: 1
+
+Introduction
+============
+
+LCM events for CM Handles are published when a CM Handle is created, deleted or another change in the cm handle state occurs.
+
+  **3 possible event types:**
+
+  * Create
+  * Update
+  * Delete
+
+CM Handle  LCM Event Schema
+---------------------------
+The current published LCM event is based on the following schema:
+
+:download:`Life cycle management event schema <schemas/lcm-event-schema-v1.json>`
+
+CM Handle LCM Event structure
+-----------------------------
+
+Events header
+^^^^^^^^^^^^^
+*Event header prototype for all event types*
+
+.. code-block::
+
+  {
+      "eventId"                : "00001",
+      "eventCorrelationId      : "cmhandle-001",
+      "eventTime"              : "2021-11-16T16:42:25-04:00",
+      "eventSource"            : "org.onap.ncmp",
+      "eventType"              : "org.onap.ncmp.cmhandle-lcm-event.create",
+      "eventSchema"            : "org.onap.ncmp:cmhandle-lcm-event",
+      "eventSchemaVersion"     : "1.0",
+      "event"                  : ...
+  }
+
+Events payload
+^^^^^^^^^^^^^^
+Event payload varies based on the type of event.
+
+**CREATE**
+
+Event payload for this event contains the properties of the new cm handle created.
+
+*Create event payload prototype*
+
+.. code-block:: json
+
+  "event": {
+         "cmHandleId" : "cmhandle-001",
+         "newValues" : {
+             "cmHandleState"  : "ADVISED",
+             "dataSyncEnabled" : "TRUE",
+             "cmhandleProperties" : [
+                          "prop1" : "val1",
+                          "prop2" : "val2"
+                ]
+            }
+       }
+   }
+
+
+**UPDATE**
+
+Event payload for this event contains the difference in state and properties of the cm handle.
+
+*Update event payload prototype*
+
+.. code-block:: json
+
+  "event": {
+         "cmHandleId" : "cmhandle-001",
+         "oldValues" : {
+                 "cmHandleState"  : "ADVISED",
+                 "dataSyncEnabled" : "FALSE",
+                 "cmhandleProperties" : [
+                          "prop1" : "val1",
+                          "prop2" : "val2",
+              }
+          "newValues" : {
+             "cmHandleState"  : "READY",
+             "dataSyncEnabled" : "TRUE",
+             "cmhandleProperties" : [
+                          "prop1" : "updatedval1",
+                          "prop2" : "updatedval2"
+                   ]
+            }
+       }
+   }
+
+
+**DELETE**
+
+Event payload for this event contains the identifier of the deleted cm handle.
+
+*Delete event payload prototype*
+
+.. code-block:: json
+
+  "event": {
+         "cmHandleId" : "cmhandle-001",
+   }
\ No newline at end of file
index d487018..25a253b 100644 (file)
@@ -1,6 +1,6 @@
 .. This work is licensed under a Creative Commons Attribution 4.0 International License.
 .. http://creativecommons.org/licenses/by/4.0
-.. Copyright (C) 2022 Nordix Foundation
+.. Copyright (C) 2022-2023 Nordix Foundation
 
 .. DO NOT CHANGE THIS LABEL FOR RELEASE NOTES - EVEN THOUGH IT GIVES A WARNING
 .. _cpsEvents:
 CPS Events
 ##########
 
-CPS-NCMP
-********
+.. toctree::
+   :maxdepth: 1
 
-Async events are triggered when a valid topic has been detected in a passthrough operation.
+   cm-handle-lcm-events.rst
+   data-operation-events.rst
 
-:download:`NCMP request response event schema <schemas/ncmp-async-request-response-event-schema-v1.json>`
-
-Event header
-^^^^^^^^^^^^^
-
-.. code-block:: json
-
-    {
-        "eventId"               : "001",
-        "eventCorrelationId"    : "cps-001",
-        "eventTime"             : "2022-09-28T12:24:21.003+0000",
-        "eventTarget"           : "test-topic",
-        "eventType"             : "org.onap.cps.ncmp.event.model.DmiAsyncRequestResponseEvent",
-        "eventSchema"           : "urn:cps:org.onap.cps.ncmp.event.model.DmiAsyncRequestResponseEvent:v1",
-        "forwarded-Event"       : { }
-    }
-
-Forwarded-Event Payload
-^^^^^^^^^^^^^^^^^^^^^^^
-
-.. code-block:: json
-
-    "Forwarded-Event": {
-        "eventId"               : "002",
-        "eventCorrelationId"    : "cps-001",
-        "eventTime"             : "2022-09-28T12:24:18.340+0000",
-        "eventTarget"           : "test-topic",
-        "eventType"             : "org.onap.cps.ncmp.event.model.DmiAsyncRequestResponseEvent",
-        "eventSchema"           : "urn:cps:org.onap.cps.ncmp.event.model.DmiAsyncRequestResponseEvent:v1",
-        "eventSource"           : "org.onap.cps.ncmp.dmi",
-        "response-data-schema"  : "urn:cps:org.onap.cps.ncmp.event.model.DmiAsyncRequestResponseEvent:v1",
-        "response-status"       : "OK",
-        "response-code"         : "200",
-        "response-data"         : { }
-    }
-
-
-Lifecycle Management (LCM) Event
-================================
-
-
-Overview
---------
-LCM events for CM Handles are published when a CM Handle is created, deleted or another change in the cm handle state occurs.
-
-  **3 possible event types:**
-
-  * Create
-  * Update
-  * Delete
-
-LCM Event Schema
-----------------
-The current published LCM event is based on the following schema:
-
-:download:`Life cycle management event schema <schemas/lcm-event-schema-v1.json>`
-
-LCM Event structure
--------------------
-
-Events header
-^^^^^^^^^^^^^
-*Event header prototype for all event types*
-
-.. code-block::
-
-  {
-      "eventId"                : "00001",
-      "eventCorrelationId      : "cmhandle-001",
-      "eventTime"              : "2021-11-16T16:42:25-04:00",
-      "eventSource"            : "org.onap.ncmp",
-      "eventType"              : "org.onap.ncmp.cmhandle-lcm-event.create",
-      "eventSchema"            : "org.onap.ncmp:cmhandle-lcm-event",
-      "eventSchemaVersion"     : "1.0",
-      "event"                  : ...
-  }
-
-Events payload
-^^^^^^^^^^^^^^
-Event payload varies based on the type of event.
-
-**CREATE**
-
-Event payload for this event contains the properties of the new cm handle created.
-
-*Create event payload prototype*
-
-.. code-block:: json
-
-  "event": {
-         "cmHandleId" : "cmhandle-001",
-         "newValues" : {
-             "cmHandleState"  : "ADVISED",
-             "dataSyncEnabled" : "TRUE",
-             "cmhandleProperties" : [
-                          "prop1" : "val1",
-                          "prop2" : "val2"
-                ]
-            }
-       }
-   }
-
-
-**UPDATE**
-
-Event payload for this event contains the difference in state and properties of the cm handle.
-
-*Update event payload prototype*
-
-.. code-block:: json
-
-  "event": {
-         "cmHandleId" : "cmhandle-001",
-         "oldValues" : {
-                 "cmHandleState"  : "ADVISED",
-                 "dataSyncEnabled" : "FALSE",
-                 "cmhandleProperties" : [
-                          "prop1" : "val1",
-                          "prop2" : "val2",
-              }
-          "newValues" : {
-             "cmHandleState"  : "READY",
-             "dataSyncEnabled" : "TRUE",
-             "cmhandleProperties" : [
-                          "prop1" : "updatedval1",
-                          "prop2" : "updatedval2"
-                   ]
-            }
-       }
-   }
-
-
-**DELETE**
-
-Event payload for this event contains the identifier of the deleted cm handle.
-
-*Delete event payload prototype*
-
-.. code-block:: json
-
-  "event": {
-         "cmHandleId" : "cmhandle-001",
-   }
+.. note::
+    Legacy async response on a client supplied topic for single cm handle data request are no longer supported. Click link below for the legacy specification.
 
+      .. toctree::
+         :maxdepth: 0
 
+         ncmp-async-events.rst
\ No newline at end of file
diff --git a/docs/cps-ncmp-message-status-codes.rst b/docs/cps-ncmp-message-status-codes.rst
new file mode 100644 (file)
index 0000000..99d802f
--- /dev/null
@@ -0,0 +1,41 @@
+.. This work is licensed under a Creative Commons Attribution 4.0 International License.
+.. http://creativecommons.org/licenses/by/4.0
+.. Copyright (C) 2023 Nordix Foundation
+
+.. DO NOT CHANGE THIS LABEL FOR RELEASE NOTES - EVEN THOUGH IT GIVES A WARNING
+.. _dataOperationMessageStatusCodes:
+
+
+CPS-NCMP Message Status Codes
+#############################
+
+    +-----------------+------------------------------------------------------+-----------------------------------+
+    | Status Code     | Status Message                                       | Feature                           |
+    +=================+======================================================+===================================+
+    | 0               | Successfully applied changes                         | Data Operation                    |
+    +-----------------+------------------------------------------------------+-----------------------------------+
+    | 1               | successfully applied subscription                    | CM Data Notification Subscription |
+    +-----------------+------------------------------------------------------+-----------------------------------+
+    | 100             | cm handle id(s) is(are) not found                    | Data Operation                    |
+    +-----------------+------------------------------------------------------+-----------------------------------+
+    | 101             | cm handle id(s) is(are) in non ready state           | Data Operation                    |
+    +-----------------+------------------------------------------------------+-----------------------------------+
+    | 102             | dmi plugin service is not responding                 | Data Operation                    |
+    +-----------------+------------------------------------------------------+-----------------------------------+
+    | 103             | dmi plugin service is not able to read resource data | Data Operation                    |
+    +-----------------+------------------------------------------------------+-----------------------------------+
+    | 104             | partially applied subscription                       | CM Data Notification Subscription |
+    +-----------------+------------------------------------------------------+-----------------------------------+
+    | 105             | subscription not applicable for all cm handles       | CM Data Notification Subscription |
+    +-----------------+------------------------------------------------------+-----------------------------------+
+    | 106             | subscription pending for all cm handles              | CM Data Notification Subscription |
+    +-----------------+------------------------------------------------------+-----------------------------------+
+
+.. note::
+
+    - Single response format for all scenarios both positive and error, just using optional fields instead.
+    - status-code 0-99 is reserved for any success response.
+    - status-code from 100 to 199 is reserved for any failed response.
+
+
+
diff --git a/docs/data-operation-events.rst b/docs/data-operation-events.rst
new file mode 100644 (file)
index 0000000..51ec125
--- /dev/null
@@ -0,0 +1,64 @@
+.. This work is licensed under a Creative Commons Attribution 4.0 International License.
+.. http://creativecommons.org/licenses/by/4.0
+.. Copyright (C) 2023 Nordix Foundation
+
+.. DO NOT CHANGE THIS LABEL FOR RELEASE NOTES - EVEN THOUGH IT GIVES A WARNING
+.. _dataOperationEvents:
+
+CPS-NCMP Data Operations Events
+###############################
+
+These events are based on the cloud events standard which is a specification for describing event data in common formats to provide interoperability across services, platforms and systems.
+
+Please refer to the `cloud events <https://cloudevents.io/>`_ for more details.
+
+Data operation response events
+******************************
+
+:download:`Data operation event schema <schemas/data-operation-event-schema-1.0.0.json>`
+
+Event headers example
+^^^^^^^^^^^^^^^^^^^^^
+
+.. code-block:: json
+
+    {
+        "specversion":      "1.0",
+        "id":               "77b8f114-4562-4069-8234-6d059ff742ac",
+        "type":             "org.onap.cps.ncmp.events.async1_0_0.DataOperationEvent",
+        "source":           "DMI",
+        "dataschema":       "urn:cps:org.onap.cps.ncmp.events.async1_0_0.DataOperationEvent:1.0.0",
+        "time":             "2020-12-01T00:00:00.000+0000",
+        "content-type":     "application/json",
+        "data":             "{some-key:some-value}",
+        "correlationid":    "6ea5cb30ecfd4a938de36fdc07a5008f",
+        "destination":      "client-topic"
+    }
+
+Data operation event headers
+============================
+
+    +----------------+-----------------+------------------------------------------------------------------------+
+    | Field name     | Mandatory       |  Description                                                           |
+    +================+=================+========================================================================+
+    | specversion    | Yes             | default : 1.0                                                          |
+    +----------------+-----------------+------------------------------------------------------------------------+
+    | id             | Yes             | UUID                                                                   |
+    +----------------+-----------------+------------------------------------------------------------------------+
+    | type           | Yes             | org.onap.cps.ncmp.events.async1_0_0.DataOperationEvent                 |
+    +----------------+-----------------+------------------------------------------------------------------------+
+    | source         | Yes             | NCMP / DMI                                                             |
+    +----------------+-----------------+------------------------------------------------------------------------+
+    | dataschema     | No              | `urn:cps:org.onap.cps.ncmp.events.async1_0_0.DataOperationEvent:1.0.0` |
+    +----------------+-----------------+------------------------------------------------------------------------+
+    | time           | No              | ISO_TIMESTAMP_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"                   |
+    +----------------+-----------------+------------------------------------------------------------------------+
+    | content-type   | No              | default : application/json                                             |
+    +----------------+-----------------+------------------------------------------------------------------------+
+    | data           | Yes             | actual event/payload now would be under "data" field.                  |
+    +----------------+-----------------+------------------------------------------------------------------------+
+    | correlationid  | Yes             | request id                                                             |
+    +----------------+-----------------+------------------------------------------------------------------------+
+    | destination    | Yes             | client topic                                                           |
+    +----------------+-----------------+------------------------------------------------------------------------+
+
index 6d31f83..ceaaefd 100644 (file)
@@ -1,7 +1,7 @@
 .. This work is licensed under a Creative Commons Attribution 4.0 International License.
 .. http://creativecommons.org/licenses/by/4.0
 .. Copyright (C) 2021 Pantheon.tech
-.. Modifications Copyright (C) 2021-2022 Nordix Foundation
+.. Modifications Copyright (C) 2021-2023 Nordix Foundation
 .. _modeling:
 
 .. toctree::
@@ -121,13 +121,20 @@ Basic Concepts
     | Passthrough-running            | config-true                         | read-write              |
     +--------------------------------+-------------------------------------+-------------------------+
 
-Querying CM Handles
-
-- **CM Handle Searches Endpoints** are used to query CM Handles.
+Additional information on CPS-NCMP interfaces
+---------------------------------------------
 
 .. toctree::
    :maxdepth: 1
 
    ncmp-cmhandle-querying.rst
    ncmp-inventory-querying.rst
+   ncmp-data-operation.rst
+
+CPS-NCMP Scheduled Processes
+----------------------------
+
+.. toctree::
+   :maxdepth: 1
+
    cps-scheduled-processes.rst
diff --git a/docs/ncmp-async-events.rst b/docs/ncmp-async-events.rst
new file mode 100644 (file)
index 0000000..49bf570
--- /dev/null
@@ -0,0 +1,54 @@
+.. This work is licensed under a Creative Commons Attribution 4.0 International License.
+.. http://creativecommons.org/licenses/by/4.0
+.. Copyright (C) 2023 Nordix Foundation
+
+.. DO NOT CHANGE THIS LABEL FOR RELEASE NOTES - EVEN THOUGH IT GIVES A WARNING
+.. _asyncEvents:
+
+
+CPS Async Events
+################
+
+.. toctree::
+   :maxdepth: 1
+
+Introduction
+============
+
+Async events are triggered when a valid topic has been detected in a passthrough operation.
+
+:download:`NCMP request response event schema <schemas/ncmp-async-request-response-event-schema-v1.json>`
+
+Event header
+^^^^^^^^^^^^
+
+.. code-block:: json
+
+    {
+        "eventId"               : "001",
+        "eventCorrelationId"    : "cps-001",
+        "eventTime"             : "2022-09-28T12:24:21.003+0000",
+        "eventTarget"           : "test-topic",
+        "eventType"             : "org.onap.cps.ncmp.event.model.DmiAsyncRequestResponseEvent",
+        "eventSchema"           : "urn:cps:org.onap.cps.ncmp.event.model.DmiAsyncRequestResponseEvent:v1",
+        "forwarded-Event"       : { }
+    }
+
+Forwarded-Event Payload
+^^^^^^^^^^^^^^^^^^^^^^^
+
+.. code-block:: json
+
+    "Forwarded-Event": {
+        "eventId"               : "002",
+        "eventCorrelationId"    : "cps-001",
+        "eventTime"             : "2022-09-28T12:24:18.340+0000",
+        "eventTarget"           : "test-topic",
+        "eventType"             : "org.onap.cps.ncmp.event.model.DmiAsyncRequestResponseEvent",
+        "eventSchema"           : "urn:cps:org.onap.cps.ncmp.event.model.DmiAsyncRequestResponseEvent:v1",
+        "eventSource"           : "org.onap.cps.ncmp.dmi",
+        "response-data-schema"  : "urn:cps:org.onap.cps.ncmp.event.model.DmiAsyncRequestResponseEvent:v1",
+        "response-status"       : "OK",
+        "response-code"         : "200",
+        "response-data"         : { }
+    }
\ No newline at end of file
diff --git a/docs/ncmp-data-operation.rst b/docs/ncmp-data-operation.rst
new file mode 100644 (file)
index 0000000..617b3ed
--- /dev/null
@@ -0,0 +1,148 @@
+.. This work is licensed under a Creative Commons Attribution 4.0 International License.
+.. http://creativecommons.org/licenses/by/4.0
+.. Copyright (C) 2023 Nordix Foundation
+
+.. DO NOT CHANGE THIS LABEL FOR RELEASE NOTES - EVEN THOUGH IT GIVES A WARNING
+.. _cmHandleDataOperation:
+
+
+CM Handles Data Operation Endpoints
+###################################
+
+.. toctree::
+   :maxdepth: 1
+
+Introduction
+============
+
+For data operation CM Handles we have a Post endpoints:
+
+- /ncmp/v1/data?topic={client-topic-name} forward request to it's dmi plugin service.
+
+- Returns request id (UUID) with http status 202.
+
+Request Body
+============
+
+This endpoint executes data operation for given array of operations:
+
+    +--------------------------+-------------+-------------------------------------------------------------------------+
+    | Operation attributes     | Mandatory   |  Description                                                            |
+    +==========================+=============+=========================================================================+
+    | operation                | Yes         | Only read operation is allowed.                                         |
+    +--------------------------+-------------+-------------------------------------------------------------------------+
+    | operationId              | Yes         | Unique operation id for each operation.                                 |
+    +--------------------------+-------------+-------------------------------------------------------------------------+
+    | datastore                | Yes         | Supports only ncmp-datastore:passthrough-operational and                |
+    |                          |             | ncmp-datastore:passthrough-running.                                     |
+    +--------------------------+-------------+-------------------------------------------------------------------------+
+    | options                  | No          | It is mandatory to wrap key(s)=value(s) in parenthesis'()'. The format  |
+    |                          |             | of options parameter depend on the associated DMI Plugin implementation.|
+    +--------------------------+-------------+-------------------------------------------------------------------------+
+    | resourceIdentifier       | No          | The format of resource identifier depend on the associated DMI Plugin   |
+    |                          |             | implementation. For ONAP DMI Plugin it will be RESTConf paths but it can|
+    |                          |             | really be anything.                                                     |
+    +--------------------------+-------------+-------------------------------------------------------------------------+
+    | targetIds                | Yes         | List of cm handle ids.                                                  |
+    +--------------------------+-------------+-------------------------------------------------------------------------+
+
+The status codes used in the events resulting from these operations are defined here:
+
+.. toctree::
+   :maxdepth: 1
+
+   cps-ncmp-message-status-codes.rst
+
+Request Body example from client app to NCMP endpoint:
+
+.. code-block:: bash
+
+    curl --location 'http: //{ncmp-host-name}:{ncmp-port}/ncmp/v1/data?topic=my-topic-name' \
+    --header 'Content-Type: application/json' \
+    --header 'Authorization: Basic Y3BzdXNlcjpjcHNyMGNrcyE=' \
+    --data '{
+    "operations": [
+        {
+            "operation": "read",
+            "operationId": "operational-12",
+            "datastore": "ncmp-datastore:passthrough-operational",
+            "options": "some option",
+            "resourceIdentifier": "parent/child",
+            "targetIds": [
+                "836bb62201f34a7aa056a47bd95a81ed",
+                "202acb75b4a54e43bb1ff8c0c17a8e08"
+            ]
+        },
+        {
+            "operation": "read",
+            "operationId": "running-14",
+            "datastore": "ncmp-datastore:passthrough-running",
+            "targetIds": [
+                "ec2e9495679a43c58659c07d87025e72",
+                "0df4d39af6514d99b816758148389cfd"
+            ]
+        }
+    ]
+    }'
+
+
+DMI service batch endpoint
+--------------------------
+
+DMI Service 1 (POST): `http://{dmi-host-name}:{dmi-port}/dmi/v1/data?topic=my-topic-name&requestId=4753fc1f-7de2-449a-b306-a6204b5370b3`
+
+.. code-block:: json
+
+    [
+    {
+        "operationType": "read",
+        "operationId": "running-14",
+        "datastore": "ncmp-datastore:passthrough-running",
+        "cmHandles": [
+            {
+                "id": "ec2e9495679a43c58659c07d87025e72",
+                "cmHandleProperties": {
+                    "neType": "RadioNode"
+                }
+            },
+            {
+                "id": "0df4d39af6514d99b816758148389cfd",
+                "cmHandleProperties": {
+                    "neType": "RadioNode"
+                }
+            }
+        ]
+    }
+    ]
+
+DMI Service 2 (POST) : `http://{dmi-host-name}:{dmi-port}/dmi/v1/data?topic=my-topic-name&requestId=4753fc1f-7de2-449a-b306-a6204b5370b3`
+
+.. code-block:: json
+
+    [
+    {
+        "operationType": "read",
+        "operationId": "operational-12",
+        "datastore": "ncmp-datastore:passthrough-operational",
+        "options": "some option",
+        "resourceIdentifier": "parent/child",
+        "cmHandles": [
+            {
+                "id": "836bb62201f34a7aa056a47bd95a81ed",
+                "cmHandleProperties": {
+                    "neType": "RadioNode"
+                }
+            },
+            {
+                "id": "202acb75b4a54e43bb1ff8c0c17a8e08",
+                "cmHandleProperties": {
+                    "neType": "RadioNode"
+                }
+            }
+        ]
+    }
+    ]
+
+Above examples are for illustration purpose only please refer link below for latest schema.
+
+:download:`Data operation event schema <schemas/data-operation-event-schema-1.0.0.json>`
\ No newline at end of file
index 6b35461..66dde1c 100755 (executable)
@@ -42,7 +42,7 @@ Bug Fixes
 
 Features
 --------
-3.3.6
+    - `CPS-1696 <https://jira.onap.org/browse/CPS-1696>`_ Get Data Node to return entire List data node.
 
 
 Version: 3.3.5
diff --git a/docs/schemas/data-operation-event-schema-1.0.0.json b/docs/schemas/data-operation-event-schema-1.0.0.json
new file mode 100644 (file)
index 0000000..f82e481
--- /dev/null
@@ -0,0 +1,69 @@
+{
+  "$schema": "https://json-schema.org/draft/2019-09/schema",
+  "$id": "urn:cps:org.onap.cps.ncmp.events.async:data-operation-event-schema:1.0.0",
+  "$ref": "#/definitions/DataOperationEvent",
+  "definitions": {
+    "DataOperationEvent": {
+      "description": "The payload of data operation event.",
+      "type": "object",
+      "javaType" : "org.onap.cps.ncmp.events.async1_0_0.DataOperationEvent",
+      "properties": {
+        "data": {
+          "description": "The payload content of the requested data.",
+          "type": "object",
+          "properties": {
+            "responses": {
+              "description": "An array of batch responses which contains both success and failure",
+              "type": "array",
+              "items": {
+                "type": "object",
+                "properties": {
+                  "operationId": {
+                    "description": "Used to distinguish multiple operations using same handle ids",
+                    "type": "string"
+                  },
+                  "ids": {
+                    "description": "Id's of the cmhandles",
+                    "type": "array",
+                    "items": {
+                      "type": "string"
+                    }
+                  },
+                  "statusCode": {
+                    "description": "which says success or failure (0-99) are for success and (100-199) are for failure",
+                    "type": "string"
+                  },
+                  "statusMessage": {
+                    "description": "Human readable message, Which says what the response has",
+                    "type": "string"
+                  },
+                  "result": {
+                    "description": "Contains the requested data response.",
+                    "type": "object",
+                    "existingJavaType": "java.lang.Object",
+                    "additionalProperties": false
+                  }
+                },
+                "required": [
+                  "operationId",
+                  "ids",
+                  "statusCode",
+                  "statusMessage"
+                ],
+                "additionalProperties": false
+              }
+            }
+          },
+          "required": [
+            "responses"
+          ],
+          "additionalProperties": false
+        }
+      },
+      "required": [
+        "data"
+      ],
+      "additionalProperties": false
+    }
+  }
+}
\ No newline at end of file
index a3f1439..6b556d3 100644 (file)
@@ -113,23 +113,49 @@ class CpsDataServiceIntegrationSpec extends FunctionalSpecBase {
             restoreBookstoreDataAnchor(1)
     }
 
+    def 'Get whole list data' () {
+            def xpathForWholeList = "/bookstore/categories"
+        when: 'get data nodes for bookstore container'
+            def dataNodes = objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, xpathForWholeList, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS)
+        then: 'the tree consist ouf of #expectNumberOfDataNodes data nodes'
+            assert dataNodes.size() == 5
+        and: 'each datanode contains the list node xpath partially in its xpath'
+            dataNodes.each {dataNode ->
+                assert dataNode.xpath.contains(xpathForWholeList)
+            }
+    }
+
+    def 'Read (multiple) data nodes with #scenario' () {
+        when: 'attempt to get data nodes using multiple valid xpaths'
+            def dataNodes = objectUnderTest.getDataNodesForMultipleXpaths(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, xpath, OMIT_DESCENDANTS)
+        then: 'expected numer of data nodes are returned'
+            dataNodes.size() == expectedNumberOfDataNodes
+        where: 'the following data was used'
+                    scenario                    |                       xpath                                       |   expectedNumberOfDataNodes
+            'container-node xpath'              | ['/bookstore']                                                    |               1
+            'list-item'                         | ['/bookstore/categories[@code=1]']                                |               1
+            'parent-list xpath'                 | ['/bookstore/categories']                                         |               5
+            'child-list xpath'                  | ['/bookstore/categories[@code=1]/books']                          |               2
+            'both parent and child list xpath'  | ['/bookstore/categories', '/bookstore/categories[@code=1]/books'] |               7
+    }
+
     def 'Add and Delete a (container) data node using #scenario.'() {
-        when: 'the new datanode is saved'
-            objectUnderTest.saveData(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1 , parentXpath, json, now)
-        then: 'it can be retrieved by its normalized xpath'
-            def result = objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, normalizedXpathToNode, DIRECT_CHILDREN_ONLY)
-            assert result.size() == 1
-            assert result[0].xpath == normalizedXpathToNode
-        and: 'there is now one extra datanode'
-            assert originalCountBookstoreChildNodes + 1 == countDataNodesInBookstore()
-        when: 'the new datanode is deleted'
-            objectUnderTest.deleteDataNode(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, normalizedXpathToNode, now)
-        then: 'the original number of data nodes is restored'
-            assert originalCountBookstoreChildNodes == countDataNodesInBookstore()
-        where:
-            scenario                      | parentXpath                         | json                                                                                        || normalizedXpathToNode
-            'normalized parent xpath'     | '/bookstore'                        | '{"webinfo": {"domain-name":"ourbookstore.com", "contact-email":"info@ourbookstore.com" }}' || "/bookstore/webinfo"
-            'non-normalized parent xpath' | '/bookstore/categories[ @code="1"]' | '{"books": {"title":"new" }}'                                                               || "/bookstore/categories[@code='1']/books[@title='new']"
+            when: 'the new datanode is saved'
+                objectUnderTest.saveData(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1 , parentXpath, json, now)
+            then: 'it can be retrieved by its normalized xpath'
+                def result = objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, normalizedXpathToNode, DIRECT_CHILDREN_ONLY)
+                assert result.size() == 1
+                assert result[0].xpath == normalizedXpathToNode
+            and: 'there is now one extra datanode'
+                assert originalCountBookstoreChildNodes + 1 == countDataNodesInBookstore()
+            when: 'the new datanode is deleted'
+                objectUnderTest.deleteDataNode(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, normalizedXpathToNode, now)
+            then: 'the original number of data nodes is restored'
+                assert originalCountBookstoreChildNodes == countDataNodesInBookstore()
+            where:
+                scenario                      | parentXpath                         | json                                                                                        || normalizedXpathToNode
+                'normalized parent xpath'     | '/bookstore'                        | '{"webinfo": {"domain-name":"ourbookstore.com", "contact-email":"info@ourbookstore.com" }}' || "/bookstore/webinfo"
+                'non-normalized parent xpath' | '/bookstore/categories[ @code="1"]' | '{"books": {"title":"new" }}'                                                               || "/bookstore/categories[@code='1']/books[@title='new']"
     }
 
     def 'Attempt to create a top level data node using root.'() {
index db36b88..e80a87d 100644 (file)
@@ -20,6 +20,8 @@
 
 package org.onap.cps.integration.performance.cps
 
+import org.onap.cps.spi.exceptions.DataNodeNotFoundException
+
 import java.time.OffsetDateTime
 import org.onap.cps.api.CpsDataService
 import org.onap.cps.integration.performance.base.CpsPerfTestBase
@@ -34,7 +36,7 @@ class DeletePerfTest extends CpsPerfTestBase {
         when: 'multiple anchors with a node with a large number of descendants is created'
             stopWatch.start()
             def data = generateOpenRoadData(50)
-            addAnchorsWithData(9, CPS_PERFORMANCE_TEST_DATASPACE, LARGE_SCHEMA_SET, 'delete', data)
+            addAnchorsWithData(10, CPS_PERFORMANCE_TEST_DATASPACE, LARGE_SCHEMA_SET, 'delete', data)
             stopWatch.stop()
             def setupDurationInMillis = stopWatch.getTotalTimeMillis()
         then: 'setup duration is under 40 seconds'
@@ -155,9 +157,23 @@ class DeletePerfTest extends CpsPerfTestBase {
             recordAndAssertPerformance('Delete data nodes for anchor', 300, deleteDurationInMillis)
     }
 
+    def 'Batch delete 100 non-existing nodes'() {
+        given: 'a list of xpaths to delete'
+            def xpathsToDelete = (1..100).collect { "/path/to/non-existing/node[@id='" + it + "']" }
+        when: 'child nodes are deleted'
+            stopWatch.start()
+            try {
+                objectUnderTest.deleteDataNodes(CPS_PERFORMANCE_TEST_DATASPACE, 'delete10', xpathsToDelete, OffsetDateTime.now())
+            } catch (DataNodeNotFoundException ignored) {}
+            stopWatch.stop()
+            def deleteDurationInMillis = stopWatch.getTotalTimeMillis()
+        then: 'delete duration is under 300 milliseconds'
+            recordAndAssertPerformance('Batch delete 100 non-existing', 300, deleteDurationInMillis)
+    }
+
     def 'Clean up test data'() {
         given: 'a list of anchors to delete'
-            def anchorNames = (1..9).collect {'delete' + it}
+            def anchorNames = (1..10).collect {'delete' + it}
         when: 'data nodes are deleted'
             stopWatch.start()
             cpsAdminService.deleteAnchors(CPS_PERFORMANCE_TEST_DATASPACE, anchorNames)
index eee87dd..f6ca5fc 100644 (file)
@@ -44,7 +44,7 @@ class GetPerfTest extends CpsPerfTestBase {
             recordAndAssertPerformance("Read datatrees with ${scenario}", durationLimit, durationInMillis)
         where: 'the following parameters are used'
             scenario             | fetchDescendantsOption  | anchor       || durationLimit | expectedNumberOfDataNodes
-            'no descendants'     | OMIT_DESCENDANTS        | 'openroadm1' || 50            | 1
+            'no descendants'     | OMIT_DESCENDANTS        | 'openroadm1' || 20            | 1
             'direct descendants' | DIRECT_CHILDREN_ONLY    | 'openroadm2' || 100           | 1 + 50
             'all descendants'    | INCLUDE_ALL_DESCENDANTS | 'openroadm3' || 200           | 1 + 50 * 86
     }
@@ -56,12 +56,27 @@ class GetPerfTest extends CpsPerfTestBase {
             stopWatch.start()
             def result = objectUnderTest.getDataNodesForMultipleXpaths(CPS_PERFORMANCE_TEST_DATASPACE, 'openroadm4', xpaths, INCLUDE_ALL_DESCENDANTS)
             stopWatch.stop()
-            assert countDataNodesInTree(result) == 50 * 86
             def durationInMillis = stopWatch.getTotalTimeMillis()
-        then: 'all data is read within 500 ms'
+        then: 'requested nodes and their descendants are returned'
+            assert countDataNodesInTree(result) == 50 * 86
+        and: 'all data is read within 200 ms'
             recordAndAssertPerformance("Read datatrees for multiple xpaths", 200, durationInMillis)
     }
 
+    def 'Read for multiple xpaths to non-existing datanodes'() {
+        given: 'a collection of xpaths to get'
+            def xpaths = (1..50).collect { "/path/to/non-existing/node[@id='" + it + "']" }
+        when: 'get data nodes from 1 anchor'
+            stopWatch.start()
+            def result = objectUnderTest.getDataNodesForMultipleXpaths(CPS_PERFORMANCE_TEST_DATASPACE, 'openroadm4', xpaths, INCLUDE_ALL_DESCENDANTS)
+            stopWatch.stop()
+            def durationInMillis = stopWatch.getTotalTimeMillis()
+        then: 'no data is returned'
+            assert result.isEmpty()
+        and: 'the operation completes within within 20 ms'
+            recordAndAssertPerformance("Read non-existing xpaths", 20, durationInMillis)
+    }
+
     def 'Read complete data trees using #scenario.'() {
         when: 'get data nodes for 5 anchors'
             stopWatch.start()
@@ -74,11 +89,12 @@ class GetPerfTest extends CpsPerfTestBase {
         then: 'all data is read within #durationLimit ms'
             recordAndAssertPerformance("Read datatrees using ${scenario}", durationLimit, durationInMillis)
         where: 'the following xpaths are used'
-            scenario                | anchorPrefix | xpath                || durationLimit | expectedNumberOfDataNodes
-            'bookstore root'        | 'bookstore'  | '/'                  || 200           | 78
-            'bookstore top element' | 'bookstore'  | '/bookstore'         || 200           | 78
-            'openroadm root'        | 'openroadm'  | '/'                  || 600           | 1 + 50 * 86
-            'openroadm top element' | 'openroadm'  | '/openroadm-devices' || 600           | 1 + 50 * 86
+            scenario                | anchorPrefix | xpath                                  || durationLimit | expectedNumberOfDataNodes
+            'bookstore root'        | 'bookstore'  | '/'                                    || 200           | 78
+            'bookstore top element' | 'bookstore'  | '/bookstore'                           || 200           | 78
+            'openroadm root'        | 'openroadm'  | '/'                                    || 600           | 1 + 50 * 86
+            'openroadm top element' | 'openroadm'  | '/openroadm-devices'                   || 600           | 1 + 50 * 86
+            'openroadm whole list'  | 'openroadm'  | '/openroadm-devices/openroadm-device'  || 600           | 50 * 86
     }
 
 }
index bad3f8a..78e0d01 100644 (file)
@@ -49,6 +49,7 @@ class QueryPerfTest extends CpsPerfTestBase {
             'leaf condition'             | 'openroadm2' | '//openroadm-device[@ne-state="inservice"]'                         || 200           | 50 * 86
             'ancestors'                  | 'openroadm3' | '//openroadm-device/ancestor::openroadm-devices'                    || 120           | 50 * 86 + 1
             'leaf condition + ancestors' | 'openroadm4' | '//openroadm-device[@status="success"]/ancestor::openroadm-devices' || 120           | 50 * 86 + 1
+            'non-existing data'          | 'openroadm1' | '/path/to/non-existing/node[@id="1"]'                               || 10            | 0
     }
 
     def 'Query complete data trees across all anchors with #scenario.'() {