Merge "Fix: Make bookstore data consistent"
authorToine Siebelink <toine.siebelink@est.tech>
Tue, 1 Aug 2023 12:20:51 +0000 (12:20 +0000)
committerGerrit Code Review <gerrit@onap.org>
Tue, 1 Aug 2023 12:20:51 +0000 (12:20 +0000)
81 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/NetworkCmProxyCmHandleQueryServiceImpl.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/CloudEventConstructionException.java [new file with mode: 0644]
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/NetworkCmProxyCmHandleQueryServiceSpec.groovy
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-rest/docs/openapi/components.yml
cps-rest/src/test/groovy/org/onap/cps/rest/controller/QueryRestControllerSpec.groovy
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/pom.xml
cps-service/src/main/java/org/onap/cps/notification/CpsDataUpdatedEventFactory.java
cps-service/src/main/java/org/onap/cps/spi/FetchDescendantsOption.java
cps-service/src/main/java/org/onap/cps/spi/model/DataNodeBuilder.java
cps-service/src/main/java/org/onap/cps/utils/XmlFileUtils.java
cps-service/src/main/java/org/onap/cps/yang/YangTextSchemaSourceSetBuilder.java
cps-service/src/test/groovy/org/onap/cps/api/impl/CpsAdminServiceImplSpec.groovy
cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy
cps-service/src/test/groovy/org/onap/cps/api/impl/CpsModuleServiceImplSpec.groovy
cps-service/src/test/groovy/org/onap/cps/cache/HazelcastCacheConfigSpec.groovy [new file with mode: 0644]
cps-service/src/test/groovy/org/onap/cps/config/CacheConfigSpec.groovy [moved from cps-service/src/main/java/org/onap/cps/spi/exceptions/OperationNotYetSupportedException.java with 62% similarity]
cps-service/src/test/groovy/org/onap/cps/notification/CpsDataUpdatedEventFactorySpec.groovy [moved from cps-service/src/test/groovy/org/onap/cps/notification/CpsDataUpdateEventFactorySpec.groovy with 84% similarity]
cps-service/src/test/groovy/org/onap/cps/notification/NotificationErrorHandlerSpec.groovy
cps-service/src/test/groovy/org/onap/cps/notification/NotificationServiceSpec.groovy
cps-service/src/test/groovy/org/onap/cps/spi/FetchDescendantsOptionSpec.groovy
cps-service/src/test/groovy/org/onap/cps/spi/model/ConditionPropertiesSpec.groovy [new file with mode: 0644]
cps-service/src/test/groovy/org/onap/cps/spi/model/DataNodeBuilderSpec.groovy
cps-service/src/test/groovy/org/onap/cps/utils/JsonObjectMapperSpec.groovy
cps-service/src/test/groovy/org/onap/cps/utils/XmlFileUtilsSpec.groovy
cps-service/src/test/groovy/org/onap/cps/utils/YangUtilsSpec.groovy
cps-service/src/test/groovy/org/onap/cps/yang/YangTextSchemaSourceSetBuilderSpec.groovy
docs/api/swagger/cps/openapi.yaml
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/base/CpsPerfTestBase.groovy
integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/CpsDataServiceLimitsPerfTest.groovy
integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/DeletePerfTest.groovy
integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/GetPerfTest.groovy
integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/QueryPerfTest.groovy
integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/WritePerfTest.groovy [new file with mode: 0644]
pom.xml

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 54d89ba..1d390f8 100644 (file)
@@ -1,7 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
  *  Copyright (C) 2022-2023 Nordix Foundation
- *  Modifications Copyright (C) 2023 TechMahindra Ltd.
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
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..c80b07c 100644 (file)
@@ -28,7 +28,6 @@ import org.onap.cps.ncmp.api.impl.subscriptions.SubscriptionPersistence;
 import org.onap.cps.ncmp.api.impl.utils.SubscriptionEventCloudMapper;
 import org.onap.cps.ncmp.api.impl.yangmodels.YangModelSubscriptionEvent;
 import org.onap.cps.ncmp.events.avcsubscription1_0_0.client_to_ncmp.SubscriptionEvent;
-import org.onap.cps.spi.exceptions.OperationNotYetSupportedException;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.kafka.annotation.KafkaListener;
 import org.springframework.stereotype.Component;
@@ -58,22 +57,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"))) {
-            throw new OperationNotYetSupportedException(
+            throw new UnsupportedOperationException(
                 "passthrough 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..0eda914 100644 (file)
@@ -44,8 +44,9 @@ 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;
 import org.springframework.stereotype.Component;
 
@@ -74,24 +75,30 @@ 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("*"))) {
-            throw new OperationNotYetSupportedException(
+            throw new UnsupportedOperationException(
                     "CMHandle targets are required. \"Wildcard\" operations are not yet supported");
         }
         final Collection<YangModelCmHandle> yangModelCmHandles =
                 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 +111,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 +142,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 +157,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 +189,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;
     }
 }
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/CloudEventConstructionException.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/CloudEventConstructionException.java
new file mode 100644 (file)
index 0000000..d0be344
--- /dev/null
@@ -0,0 +1,41 @@
+/*
+ *  ============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.ncmp.api.impl.utils;
+
+import org.onap.cps.spi.exceptions.CpsException;
+
+public class CloudEventConstructionException extends CpsException {
+
+    private static final long serialVersionUID = 7747941311132087621L;
+
+    /**
+     * 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);
+    }
+}
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..d0d70cf 100644 (file)
@@ -27,6 +27,7 @@ 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;
@@ -38,6 +39,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 +65,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..b6cb039
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ *  ============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;
+
+@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 bff8222..93af7f4 100644 (file)
@@ -1,7 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
  *  Copyright (C) 2022-2023 Nordix Foundation
- *  Modifications Copyright (C) 2023 TechMahindra Ltd.
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
index d4ab1e8..7fa8155 100644 (file)
@@ -29,7 +29,6 @@ import org.onap.cps.ncmp.api.impl.yangmodels.YangModelSubscriptionEvent
 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.utils.TestUtils
-import org.onap.cps.spi.exceptions.OperationNotYetSupportedException
 import org.onap.cps.utils.JsonObjectMapper
 import org.springframework.beans.factory.annotation.Autowired
 import org.springframework.boot.test.context.SpringBootTest
@@ -58,7 +57,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 +73,34 @@ 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)
+        then: 'an operation not supported exception is thrown'
+            thrown(UnsupportedOperationException)
     }
 
 }
index 2af32c2..4193f75 100644 (file)
@@ -35,9 +35,10 @@ 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
 import org.onap.cps.utils.JsonObjectMapper
 import org.spockframework.spring.SpringBean
 import org.springframework.beans.factory.annotation.Autowired
@@ -75,13 +76,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 +86,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 +100,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,9 +114,9 @@ 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)
-        then: 'an operation not yet supported exception is thrown'
-            thrown(OperationNotYetSupportedException)
+            objectUnderTest.forwardCreateSubscriptionEvent(testEventSent, 'some-event-type')
+        then: 'an operation not supported exception is thrown'
+            thrown(UnsupportedOperationException)
         where:
             scenario   | invalidTargets
             'null'     | null
@@ -136,13 +128,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 +146,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 +171,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..4023441 100644 (file)
@@ -45,7 +45,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 +59,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 +75,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 a7c1300..a721305 100644 (file)
@@ -263,7 +263,7 @@ components:
     descendantsInQuery:
       name: descendants
       in: query
-      description: Number of descendants to query. Allowed values are 'none', 'all', -1 (for all), 0 (for none) and any positive number.
+      description: Number of descendants to query. Allowed values are 'none', 'all', 'direct', 1 (for direct), -1 (for all), 0 (for none) and any positive number.
       required: false
       schema:
         type: string
index c4bb23c..2bf29fc 100644 (file)
@@ -25,6 +25,7 @@ package org.onap.cps.rest.controller
 
 import org.onap.cps.utils.PrefixResolver
 
+import static org.onap.cps.spi.FetchDescendantsOption.DIRECT_CHILDREN_ONLY
 import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
 import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
@@ -97,17 +98,21 @@ class QueryRestControllerSpec extends Specification {
             def dataNode1 = new DataNodeBuilder().withXpath('/xpath')
                 .withLeaves([leaf: 'value', leafList: ['leaveListElement1', 'leaveListElement2']]).build()
             mockCpsQueryService.queryDataNodes(dataspaceName, anchorName, cpsPath, { descendantsOption -> {
-                    assert descendantsOption.depth == 2}}) >> [dataNode1, dataNode1]
+                assert descendantsOption.depth == expectedDepth}}) >> [dataNode1, dataNode1]
         when: 'query data nodes API is invoked'
             def response =
                 mvc.perform(
                         get(dataNodeEndpointV2)
                                 .param('cps-path', cpsPath)
-                                .param('descendants', '2'))
+                                .param('descendants', includeDescendantsOptionString))
                         .andReturn().response
         then: 'the response contains the the datanode in json format'
             assert response.status == HttpStatus.OK.value()
             assert response.getContentAsString().contains('{"xpath":{"leaf":"value","leafList":["leaveListElement1","leaveListElement2"]}}')
+       where: 'the following options for include descendants are provided in the request'
+           scenario          | includeDescendantsOptionString || expectedDepth
+           'direct children' | 'direct'                       || 1
+           'descendants'     | '2'                            || 2
     }
 
     def 'Query data node by cps path for the given dataspace across all anchors with #scenario.'() {
@@ -139,5 +144,6 @@ class QueryRestControllerSpec extends Specification {
             'no descendants by default' | ''                             || OMIT_DESCENDANTS
             'no descendant explicitly'  | 'none'                         || OMIT_DESCENDANTS
             'descendants'               | 'all'                          || INCLUDE_ALL_DESCENDANTS
+            'direct children'           | 'direct'                       || DIRECT_CHILDREN_ONLY
     }
 }
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'(){
index c97623f..8bc39b1 100644 (file)
 
   <artifactId>cps-service</artifactId>
 
-  <properties>
-    <minimum-coverage>0.95</minimum-coverage>
-  </properties>
-
   <dependencies>
     <dependency>
       <groupId>com.github.ben-manes.caffeine</groupId>
index 38f8988..696fd60 100644 (file)
@@ -1,7 +1,7 @@
 /*
  * ============LICENSE_START=======================================================
  * Copyright (c) 2021-2022 Bell Canada.
- * Modifications Copyright (c) 2022 Nordix Foundation
+ * Modifications Copyright (c) 2022-2023 Nordix Foundation
  * Modifications Copyright (C) 2023 TechMahindra Ltd.
  * ================================================================================
  * Licensed under the Apache License, Version 2.0 (the "License");
@@ -28,6 +28,7 @@ import java.time.OffsetDateTime;
 import java.time.format.DateTimeFormatter;
 import java.util.UUID;
 import lombok.AllArgsConstructor;
+import lombok.SneakyThrows;
 import org.onap.cps.api.CpsDataService;
 import org.onap.cps.event.model.Content;
 import org.onap.cps.event.model.CpsDataUpdatedEvent;
@@ -44,22 +45,9 @@ import org.springframework.stereotype.Component;
 @AllArgsConstructor(onConstructor = @__(@Lazy))
 public class CpsDataUpdatedEventFactory {
 
-    private static final URI EVENT_SCHEMA;
-    private static final URI EVENT_SOURCE;
-    private static final String EVENT_TYPE = "org.onap.cps.data-updated-event";
     private static final DateTimeFormatter DATE_TIME_FORMATTER =
         DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
 
-    static {
-        try {
-            EVENT_SCHEMA = new URI("urn:cps:org.onap.cps:data-updated-event-schema:v1");
-            EVENT_SOURCE = new URI("urn:cps:org.onap.cps");
-        } catch (final URISyntaxException e) {
-            // As it is fixed string, I don't expect to see this error
-            throw new IllegalArgumentException(e);
-        }
-    }
-
     @Lazy
     private final CpsDataService cpsDataService;
 
@@ -82,14 +70,17 @@ public class CpsDataUpdatedEventFactory {
         return toCpsDataUpdatedEvent(anchor, dataNode, observedTimestamp, operation);
     }
 
-    private CpsDataUpdatedEvent toCpsDataUpdatedEvent(final Anchor anchor, final DataNode dataNode,
-        final OffsetDateTime observedTimestamp, final Operation operation) {
-        final var cpsDataUpdatedEvent = new CpsDataUpdatedEvent();
+    @SneakyThrows(URISyntaxException.class)
+    private CpsDataUpdatedEvent toCpsDataUpdatedEvent(final Anchor anchor,
+                                                      final DataNode dataNode,
+                                                      final OffsetDateTime observedTimestamp,
+                                                      final Operation operation) {
+        final CpsDataUpdatedEvent cpsDataUpdatedEvent = new CpsDataUpdatedEvent();
         cpsDataUpdatedEvent.withContent(createContent(anchor, dataNode, observedTimestamp, operation));
         cpsDataUpdatedEvent.withId(UUID.randomUUID().toString());
-        cpsDataUpdatedEvent.withSchema(EVENT_SCHEMA);
-        cpsDataUpdatedEvent.withSource(EVENT_SOURCE);
-        cpsDataUpdatedEvent.withType(EVENT_TYPE);
+        cpsDataUpdatedEvent.withSchema(new URI("urn:cps:org.onap.cps:data-updated-event-schema:v1"));
+        cpsDataUpdatedEvent.withSource(new URI("urn:cps:org.onap.cps"));
+        cpsDataUpdatedEvent.withType("org.onap.cps.data-updated-event");
         return cpsDataUpdatedEvent;
     }
 
index 0257499..3b90b06 100644 (file)
@@ -42,7 +42,7 @@ public class FetchDescendantsOption {
     }
 
     private static final Pattern FETCH_DESCENDANTS_OPTION_PATTERN =
-        Pattern.compile("^$|^all$|^none$|^[0-9]+$|^-1$");
+        Pattern.compile("^$|^all$|^none$|^direct$|^[0-9]+$|^-1$|^1$");
 
     private final int depth;
 
@@ -96,6 +96,8 @@ public class FetchDescendantsOption {
             return FetchDescendantsOption.OMIT_DESCENDANTS;
         } else if ("-1".equals(fetchDescendantsOptionAsString) || "all".equals(fetchDescendantsOptionAsString)) {
             return FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS;
+        } else if ("1".equals(fetchDescendantsOptionAsString) || "direct".equals(fetchDescendantsOptionAsString)) {
+            return FetchDescendantsOption.DIRECT_CHILDREN_ONLY;
         } else {
             final Integer depth = Integer.valueOf(fetchDescendantsOptionAsString);
             return new FetchDescendantsOption(depth);
index e212933..b040af5 100644 (file)
@@ -184,9 +184,8 @@ public class DataNodeBuilder {
 
     private DataNode buildFromContainerNode() {
         final Collection<DataNode> dataNodeCollection = buildCollectionFromContainerNode();
-        if (!dataNodeCollection.iterator().hasNext()) {
-            throw new DataValidationException(
-                "Unsupported xpath: ", "Unsupported xpath as it is referring to one element");
+        if (dataNodeCollection.isEmpty()) {
+            throw new DataValidationException("Unsupported Normalized Node", "No valid node found");
         }
         return dataNodeCollection.iterator().next();
     }
@@ -278,5 +277,4 @@ public class DataNodeBuilder {
         }
     }
 
-
 }
index 09f2e16..98c7947 100644 (file)
@@ -1,6 +1,7 @@
 /*
  *  ============LICENSE_START=======================================================
  *  Copyright (C) 2022 Deutsche Telekom AG
+ *  Modifications 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.
@@ -39,7 +40,6 @@ import javax.xml.transform.dom.DOMSource;
 import javax.xml.transform.stream.StreamResult;
 import lombok.AccessLevel;
 import lombok.NoArgsConstructor;
-import org.onap.cps.spi.exceptions.DataValidationException;
 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
 import org.w3c.dom.Document;
@@ -102,10 +102,8 @@ public class XmlFileUtils {
                                                  final Map<String, String> rootNodeProperty)
         throws IOException, SAXException, ParserConfigurationException, TransformerException {
         final DocumentBuilder documentBuilder = getDocumentBuilderFactory().newDocumentBuilder();
-        final StringBuilder xmlStringBuilder = new StringBuilder();
-        xmlStringBuilder.append(xmlContent);
-        final Document document = documentBuilder.parse(
-                new ByteArrayInputStream(xmlStringBuilder.toString().getBytes(StandardCharsets.UTF_8)));
+        final Document document =
+            documentBuilder.parse(new ByteArrayInputStream(xmlContent.getBytes(StandardCharsets.UTF_8)));
         final Element root = document.getDocumentElement();
         if (!root.getTagName().equals(rootNodeTagName)
             && !root.getTagName().equals(YangUtils.DATA_ROOT_NODE_TAG_NAME)) {
@@ -143,22 +141,19 @@ public class XmlFileUtils {
     static Document addDataRootNode(final Element node,
                                     final String tagName,
                                     final String namespace,
-                                    final Map<String, String> rootNodeProperty) {
-        try {
-            final DocumentBuilder documentBuilder = getDocumentBuilderFactory().newDocumentBuilder();
-            final Document document = documentBuilder.newDocument();
-            final Element rootElement = document.createElementNS(namespace, tagName);
-            for (final Map.Entry<String, String> entry : rootNodeProperty.entrySet()) {
-                final Element propertyElement = document.createElement(entry.getKey());
-                propertyElement.setTextContent(entry.getValue());
-                rootElement.appendChild(propertyElement);
-            }
-            rootElement.appendChild(document.adoptNode(node));
-            document.appendChild(rootElement);
-            return document;
-        } catch (final ParserConfigurationException exception) {
-            throw new DataValidationException("Can't parse XML", "XML can't be parsed", exception);
+                                    final Map<String, String> rootNodeProperty)
+        throws ParserConfigurationException {
+        final DocumentBuilder documentBuilder = getDocumentBuilderFactory().newDocumentBuilder();
+        final Document document = documentBuilder.newDocument();
+        final Element rootElement = document.createElementNS(namespace, tagName);
+        for (final Map.Entry<String, String> entry : rootNodeProperty.entrySet()) {
+            final Element propertyElement = document.createElement(entry.getKey());
+            propertyElement.setTextContent(entry.getValue());
+            rootElement.appendChild(propertyElement);
         }
+        rootElement.appendChild(document.adoptNode(node));
+        document.appendChild(rootElement);
+        return document;
     }
 
     private static DocumentBuilderFactory getDocumentBuilderFactory() {
index deb5b05..ca90714 100644 (file)
@@ -27,7 +27,6 @@ import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableMap;
 import io.micrometer.core.annotation.Timed;
 import java.io.ByteArrayInputStream;
-import java.io.IOException;
 import java.io.InputStream;
 import java.nio.charset.StandardCharsets;
 import java.util.Collections;
@@ -37,7 +36,6 @@ import java.util.Optional;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 import lombok.NoArgsConstructor;
-import org.onap.cps.spi.exceptions.CpsException;
 import org.onap.cps.spi.exceptions.ModelValidationException;
 import org.onap.cps.spi.model.ModuleReference;
 import org.opendaylight.yangtools.yang.common.Revision;
@@ -45,7 +43,6 @@ import org.opendaylight.yangtools.yang.model.api.Module;
 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
 import org.opendaylight.yangtools.yang.model.repo.api.RevisionSourceIdentifier;
 import org.opendaylight.yangtools.yang.model.repo.api.YangTextSchemaSource;
-import org.opendaylight.yangtools.yang.parser.api.YangSyntaxErrorException;
 import org.opendaylight.yangtools.yang.parser.rfc7950.reactor.RFC7950Reactors;
 import org.opendaylight.yangtools.yang.parser.rfc7950.repo.YangStatementStreamSource;
 import org.opendaylight.yangtools.yang.parser.spi.meta.ReactorException;
@@ -144,23 +141,20 @@ public final class YangTextSchemaSourceSetBuilder {
             final String resourceName = yangTextSchemaSource.getIdentifier().getName();
             try {
                 reactor.addSource(YangStatementStreamSource.create(yangTextSchemaSource));
-            } catch (final IOException e) {
-                throw new CpsException("Failed to read yang resource.",
-                    String.format("Exception occurred on reading resource %s.", resourceName), e);
-            } catch (final YangSyntaxErrorException e) {
-                throw new ModelValidationException("Yang resource is invalid.",
-                    String.format(
-                            "Yang syntax validation failed for resource %s:%n%s", resourceName, e.getMessage()), e);
+            } catch (final Exception exception) {
+                throw new ModelValidationException("Yang resource processing exception.",
+                    String.format("Could not process resource %s:%n%s", resourceName, exception.getMessage()),
+                    exception);
             }
         }
         try {
             return reactor.buildEffective();
-        } catch (final ReactorException e) {
+        } catch (final ReactorException reactorException) {
             final List<String> resourceNames = yangResourceNameToContent.keySet().stream().collect(Collectors.toList());
             Collections.sort(resourceNames);
             throw new ModelValidationException("Invalid schema set.",
-                String.format("Effective schema context build failed for resources %s.", resourceNames.toString()),
-                e);
+                String.format("Effective schema context build failed for resources %s.", resourceNames),
+                reactorException);
         }
     }
 
index 4e0349d..eb41e20 100755 (executable)
@@ -25,6 +25,7 @@ package org.onap.cps.api.impl
 
 import org.onap.cps.api.CpsDataService
 import org.onap.cps.spi.CpsAdminPersistenceService
+import org.onap.cps.spi.exceptions.ModuleNamesNotFoundException
 import org.onap.cps.spi.model.Anchor
 import org.onap.cps.spi.model.Dataspace
 import org.onap.cps.spi.utils.CpsValidator
@@ -154,6 +155,21 @@ class CpsAdminServiceImplSpec extends Specification {
             1 * mockCpsValidator.validateNameCharacters('some-dataspace-name')
     }
 
+    def 'Query all anchors with Module Names Not Found Exception in persistence layer.'() {
+        given: 'the persistence layer throws a Module Names Not Found Exception'
+            def originalException = new ModuleNamesNotFoundException('exception-ds', [ 'm1', 'm2'])
+            mockCpsAdminPersistenceService.queryAnchors(*_) >> { throw originalException}
+        when: 'attempt query anchors'
+            objectUnderTest.queryAnchorNames('some-dataspace-name', [])
+        then: 'the same exception is thrown (up)'
+            def thrownUp = thrown(ModuleNamesNotFoundException)
+            assert thrownUp == originalException
+        and: 'the exception details contains the relevant data'
+            assert thrownUp.details.contains('exception-ds')
+            assert thrownUp.details.contains('m1')
+            assert thrownUp.details.contains('m2')
+    }
+
     def 'Delete dataspace.'() {
         when: 'delete dataspace is invoked'
             objectUnderTest.deleteDataspace('someDataspace')
index 9d241f1..b4ac7a6 100644 (file)
@@ -29,7 +29,11 @@ import org.onap.cps.notification.NotificationService
 import org.onap.cps.notification.Operation
 import org.onap.cps.spi.CpsDataPersistenceService
 import org.onap.cps.spi.FetchDescendantsOption
+import org.onap.cps.spi.exceptions.ConcurrencyException
+import org.onap.cps.spi.exceptions.DataNodeNotFoundExceptionBatch
 import org.onap.cps.spi.exceptions.DataValidationException
+import org.onap.cps.spi.exceptions.SessionManagerException
+import org.onap.cps.spi.exceptions.SessionTimeoutException
 import org.onap.cps.spi.model.Anchor
 import org.onap.cps.spi.model.DataNode
 import org.onap.cps.spi.model.DataNodeBuilder
@@ -333,6 +337,18 @@ class CpsDataServiceImplSpec extends Specification {
             'level 2 node'   | ['/test-tree' : '{"branch": [{"name":"Name"}]}', '/test-tree/branch[@name=\'Name\']':'{"nest":{"name":"nestName"}}'] || ["/test-tree/branch[@name='Name']", "/test-tree/branch[@name='Name']/nest"]
     }
 
+    def 'Replace data node with concurrency exception in persistence layer.'() {
+        given: 'the persistence layer throws an concurrency exception'
+            def originalException = new ConcurrencyException('message', 'details')
+            mockCpsDataPersistenceService.updateDataNodesAndDescendants(*_) >> { throw originalException }
+            setupSchemaSetMocks('test-tree.yang')
+        when: 'attempt to replace data node'
+            objectUnderTest.updateDataNodesAndDescendants(dataspaceName, anchorName, ['/' : '{"test-tree": {}}'] , observedTimestamp)
+        then: 'the same exception is thrown up'
+            def thrownUp = thrown(ConcurrencyException)
+            assert thrownUp == originalException
+    }
+
     def 'Replace list content data fragment under parent node.'() {
         given: 'schema set for given anchor and dataspace references test-tree model'
             setupSchemaSetMocks('test-tree.yang')
@@ -366,8 +382,6 @@ class CpsDataServiceImplSpec extends Specification {
     }
 
     def 'Delete list element under existing node.'() {
-        given: 'schema set for given anchor and dataspace references test-tree model'
-            setupSchemaSetMocks('test-tree.yang')
         when: 'delete list data method is invoked with list element json data'
             objectUnderTest.deleteListOrListElement(dataspaceName, anchorName, '/test-tree/branch', observedTimestamp)
         then: 'the persistence service method is invoked with correct parameters'
@@ -379,8 +393,6 @@ class CpsDataServiceImplSpec extends Specification {
     }
 
     def 'Delete multiple list elements under existing node.'() {
-        given: 'schema set for given anchor and dataspace references test-tree model'
-            setupSchemaSetMocks('test-tree.yang')
         when: 'delete multiple list data method is invoked with list element json data'
             objectUnderTest.deleteDataNodes(dataspaceName, anchorName, ['/test-tree/branch[@name="A"]', '/test-tree/branch[@name="B"]'], observedTimestamp)
         then: 'the persistence service method is invoked with correct parameters'
@@ -392,8 +404,6 @@ class CpsDataServiceImplSpec extends Specification {
     }
 
     def 'Delete data node under anchor and dataspace.'() {
-        given: 'schema set for given anchor and dataspace references test tree model'
-            setupSchemaSetMocks('test-tree.yang')
         when: 'delete data node method is invoked with correct parameters'
             objectUnderTest.deleteDataNode(dataspaceName, anchorName, '/data-node', observedTimestamp)
         then: 'the persistence service method is invoked with the correct parameters'
@@ -405,9 +415,7 @@ class CpsDataServiceImplSpec extends Specification {
     }
 
     def 'Delete all data nodes for a given anchor and dataspace.'() {
-        given: 'schema set for given anchor and dataspace references test tree model'
-            setupSchemaSetMocks('test-tree.yang')
-        when: 'delete data node method is invoked with correct parameters'
+        when: 'delete data nodes method is invoked with correct parameters'
             objectUnderTest.deleteDataNodes(dataspaceName, anchorName, observedTimestamp)
         then: 'data updated event is sent to notification service before the delete'
             1 * mockNotificationService.processDataUpdatedEvent(anchor, '/', Operation.DELETE, observedTimestamp)
@@ -417,6 +425,20 @@ class CpsDataServiceImplSpec extends Specification {
             1 * mockCpsDataPersistenceService.deleteDataNodes(dataspaceName, anchorName)
     }
 
+    def 'Delete all data nodes for a given anchor and dataspace with batch exception in persistence layer.'() {
+        given: 'a batch exception in persistence layer'
+            def originalException = new DataNodeNotFoundExceptionBatch('ds1','a1',[])
+            mockCpsDataPersistenceService.deleteDataNodes(*_)  >> { throw originalException }
+        when: 'attempt to delete data nodes'
+            objectUnderTest.deleteDataNodes(dataspaceName, anchorName, observedTimestamp)
+        then: 'the original exception is thrown up'
+            def thrownUp = thrown(DataNodeNotFoundExceptionBatch)
+            assert thrownUp == originalException
+        and: 'the exception details contain the expected data'
+            assert thrownUp.details.contains('ds1')
+            assert thrownUp.details.contains('a1')
+    }
+
     def 'Delete all data nodes for given dataspace and multiple anchors.'() {
         given: 'schema set for given anchors and dataspace references test tree model'
             setupSchemaSetMocks('test-tree.yang')
@@ -433,22 +455,28 @@ class CpsDataServiceImplSpec extends Specification {
             1 * mockCpsDataPersistenceService.deleteDataNodes(dataspaceName, _ as Collection<String>)
     }
 
-    def setupSchemaSetMocks(String... yangResources) {
-        def mockYangTextSchemaSourceSet = Mock(YangTextSchemaSourceSet)
-        mockYangTextSchemaSourceSetCache.get(dataspaceName, schemaSetName) >> mockYangTextSchemaSourceSet
-        def yangResourceNameToContent = TestUtils.getYangResourcesAsMap(yangResources)
-        def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext()
-        mockYangTextSchemaSourceSet.getSchemaContext() >> schemaContext
-    }
-
-    def 'start session'() {
+    def 'Start session.'() {
         when: 'start session method is called'
             objectUnderTest.startSession()
         then: 'the persistence service method to start session is invoked'
             1 * mockCpsDataPersistenceService.startSession()
     }
 
-    def 'close session'(){
+    def 'Start session with Session Manager Exceptions.'() {
+        given: 'the persistence layer throws an Session Manager Exception'
+            mockCpsDataPersistenceService.startSession() >> { throw originalException }
+        when: 'attempt to start session'
+            objectUnderTest.startSession()
+        then: 'the original exception is thrown up'
+            def thrownUp = thrown(SessionManagerException)
+            assert thrownUp == originalException
+        where: 'variations of Session Manager Exception are used'
+            originalException << [ new SessionManagerException('message','details'),
+                                   new SessionManagerException('message','details', new Exception('cause')),
+                                   new SessionTimeoutException('message','details', new Exception('cause'))]
+    }
+
+    def 'Close session.'(){
         given: 'session Id from calling the start session method'
             def sessionId = objectUnderTest.startSession()
         when: 'close session method is called'
@@ -457,20 +485,26 @@ class CpsDataServiceImplSpec extends Specification {
             1 * mockCpsDataPersistenceService.closeSession(sessionId)
     }
 
-    def 'lock anchor with no timeout parameter'(){
+    def 'Lock anchor with no timeout parameter.'(){
         when: 'lock anchor method with no timeout parameter with details of anchor entity to lock'
             objectUnderTest.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName')
         then: 'the persistence service method to lock anchor is invoked with default timeout'
-            1 * mockCpsDataPersistenceService.lockAnchor('some-sessionId', 'some-dataspaceName',
-                    'some-anchorName', 300L)
+            1 * mockCpsDataPersistenceService.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName', 300L)
     }
 
-    def 'lock anchor with timeout parameter'(){
+    def 'Lock anchor with timeout parameter.'(){
         when: 'lock anchor method with timeout parameter is called with details of anchor entity to lock'
-            objectUnderTest.lockAnchor('some-sessionId', 'some-dataspaceName',
-                    'some-anchorName', 250L)
+            objectUnderTest.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName', 250L)
         then: 'the persistence service method to lock anchor is invoked with the given timeout'
-            1 * mockCpsDataPersistenceService.lockAnchor('some-sessionId', 'some-dataspaceName',
-                    'some-anchorName', 250L)
+            1 * mockCpsDataPersistenceService.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName', 250L)
+    }
+
+    def setupSchemaSetMocks(String... yangResources) {
+        def mockYangTextSchemaSourceSet = Mock(YangTextSchemaSourceSet)
+        mockYangTextSchemaSourceSetCache.get(dataspaceName, schemaSetName) >> mockYangTextSchemaSourceSet
+        def yangResourceNameToContent = TestUtils.getYangResourcesAsMap(yangResources)
+        def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext()
+        mockYangTextSchemaSourceSet.getSchemaContext() >> schemaContext
     }
+
 }
index 3884eda..a794c58 100644 (file)
@@ -26,8 +26,10 @@ package org.onap.cps.api.impl
 import org.onap.cps.TestUtils
 import org.onap.cps.api.CpsAdminService
 import org.onap.cps.spi.CpsModulePersistenceService
+import org.onap.cps.spi.exceptions.DuplicatedYangResourceException
 import org.onap.cps.spi.exceptions.ModelValidationException
 import org.onap.cps.spi.exceptions.SchemaSetInUseException
+import org.onap.cps.spi.model.ModuleDefinition
 import org.onap.cps.spi.utils.CpsValidator
 import org.onap.cps.spi.model.Anchor
 import org.onap.cps.spi.model.ModuleReference
@@ -50,24 +52,22 @@ class CpsModuleServiceImplSpec extends Specification {
     def objectUnderTest = new CpsModuleServiceImpl(mockCpsModulePersistenceService, mockYangTextSchemaSourceSetCache, mockCpsAdminService, mockCpsValidator,timedYangTextSchemaSourceSetBuilder)
 
     def 'Create schema set.'() {
-        given: 'Valid yang resource as name-to-content map'
-            def yangResourcesNameToContentMap = TestUtils.getYangResourcesAsMap('bookstore.yang')
         when: 'Create schema set method is invoked'
-            objectUnderTest.createSchemaSet('someDataspace', 'someSchemaSet', yangResourcesNameToContentMap)
+            objectUnderTest.createSchemaSet('someDataspace', 'someSchemaSet', [:])
         then: 'Parameters are validated and processing is delegated to persistence service'
-            1 * mockCpsModulePersistenceService.storeSchemaSet('someDataspace', 'someSchemaSet', yangResourcesNameToContentMap)
+            1 * mockCpsModulePersistenceService.storeSchemaSet('someDataspace', 'someSchemaSet', [:])
         and: 'the CpsValidator is called on the dataspaceName and schemaSetName'
             1 * mockCpsValidator.validateNameCharacters('someDataspace', 'someSchemaSet')
     }
 
     def 'Create schema set from new modules and existing modules.'() {
         given: 'a list of existing modules module reference'
-            def moduleReferenceForExistingModule = new ModuleReference("test",  "2021-10-12","test.org")
+            def moduleReferenceForExistingModule = new ModuleReference('test',  '2021-10-12','test.org')
             def listOfExistingModulesModuleReference = [moduleReferenceForExistingModule]
         when: 'create schema set from modules method is invoked'
-            objectUnderTest.createSchemaSetFromModules("someDataspaceName", "someSchemaSetName", [newModule: "newContent"], listOfExistingModulesModuleReference)
+            objectUnderTest.createSchemaSetFromModules('someDataspaceName', 'someSchemaSetName', [newModule: 'newContent'], listOfExistingModulesModuleReference)
         then: 'processing is delegated to persistence service'
-            1 * mockCpsModulePersistenceService.storeSchemaSetFromModules("someDataspaceName", "someSchemaSetName", [newModule: "newContent"], listOfExistingModulesModuleReference)
+            1 * mockCpsModulePersistenceService.storeSchemaSetFromModules('someDataspaceName', 'someSchemaSetName', [newModule: 'newContent'], listOfExistingModulesModuleReference)
         and: 'the CpsValidator is called on the dataspaceName and schemaSetName'
             1 * mockCpsValidator.validateNameCharacters('someDataspaceName', 'someSchemaSetName')
     }
@@ -78,7 +78,21 @@ class CpsModuleServiceImplSpec extends Specification {
         when: 'Create schema set method is invoked'
             objectUnderTest.createSchemaSet('someDataspace', 'someSchemaSet', yangResourcesNameToContentMap)
         then: 'Model validation exception is thrown'
-            thrown(ModelValidationException.class)
+            thrown(ModelValidationException)
+    }
+
+    def 'Create schema set with duplicate yang resource exception in persistence layer.'() {
+        given: 'the persistence layer throws an duplicated yang resource exception'
+            def originalException = new DuplicatedYangResourceException('name', '123', null)
+            mockCpsModulePersistenceService.storeSchemaSet(*_) >> { throw originalException }
+        when: 'attempt to create schema set'
+            objectUnderTest.createSchemaSet('someDataspace', 'someSchemaSet', [:])
+        then: 'the same duplicated yang resource exception is thrown (up)'
+            def thrownUp = thrown(DuplicatedYangResourceException)
+            assert thrownUp == originalException
+        and: 'the exception message contains the relevant data'
+            assert thrownUp.message.contains('name')
+            assert thrownUp.message.contains('123')
     }
 
     def 'Get schema set by name and dataspace.'() {
@@ -212,20 +226,23 @@ class CpsModuleServiceImplSpec extends Specification {
             1 * mockCpsValidator.validateNameCharacters('someDataspaceName', 'someAnchorName')
     }
 
-    def 'Identifying new module references'(){
+    def 'Identifying new module references.'(){
         given: 'module references from cm handle'
             def moduleReferencesToCheck = [new ModuleReference('some-module', 'some-revision')]
         when: 'identifyNewModuleReferences is called'
             objectUnderTest.identifyNewModuleReferences(moduleReferencesToCheck)
         then: 'cps module persistence service is called with module references to check'
-            1 * mockCpsModulePersistenceService.identifyNewModuleReferences(moduleReferencesToCheck);
+            1 * mockCpsModulePersistenceService.identifyNewModuleReferences(moduleReferencesToCheck)
     }
 
     def 'Getting module definitions.'() {
+        given: 'the module persistence service returns a collection of module definitions'
+            def moduleDefinitionsFromPersistenceService = [ new ModuleDefinition('name', 'revision', 'content' ) ]
+            mockCpsModulePersistenceService.getYangResourceDefinitions('some-dataspace-name', 'some-anchor-name')  >> moduleDefinitionsFromPersistenceService
         when: 'get module definitions method is called with a valid dataspace and anchor name'
-            objectUnderTest.getModuleDefinitionsByAnchorName('some-dataspace-name', 'some-anchor-name')
-        then: 'CPS module persistence service is invoked the correct number of times'
-            1 * mockCpsModulePersistenceService.getYangResourceDefinitions('some-dataspace-name', 'some-anchor-name')
+            def result = objectUnderTest.getModuleDefinitionsByAnchorName('some-dataspace-name', 'some-anchor-name')
+        then: 'the result is the same collection returned by the persistence service'
+            assert result == moduleDefinitionsFromPersistenceService
         and: 'the CpsValidator is called on the dataspaceName and schemaSetName'
             1 * mockCpsValidator.validateNameCharacters('some-dataspace-name', 'some-anchor-name')
     }
diff --git a/cps-service/src/test/groovy/org/onap/cps/cache/HazelcastCacheConfigSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/cache/HazelcastCacheConfigSpec.groovy
new file mode 100644 (file)
index 0000000..8efd485
--- /dev/null
@@ -0,0 +1,54 @@
+/*
+ *  ============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.cache
+
+import spock.lang.Specification
+
+class HazelcastCacheConfigSpec extends Specification {
+
+    def objectUnderTest = new HazelcastCacheConfig()
+
+    def 'Create Hazelcast instance with a #scenario'() {
+        given: 'a cluster name'
+            objectUnderTest.clusterName = 'my cluster'
+        when: 'an hazelcast instance is created (name has to be unique)'
+            def result = objectUnderTest.createHazelcastInstance(scenario, config)
+        then: 'the instance is created and has the correct name'
+            assert result.name == scenario
+        and: 'if applicable it has a map config with the expected name'
+            if (expectMapConfig) {
+                assert result.config.mapConfigs.values()[0].name == 'my map config'
+            } else {
+                assert result.config.mapConfigs.isEmpty()
+            }
+        and: 'if applicable it has a queue config with the expected name'
+            if (expectQueueConfig) {
+                assert result.config.queueConfigs.values()[0].name == 'my queue config'
+            } else {
+                assert result.config.queueConfigs.isEmpty()
+            }
+        where: 'the following configs are used'
+            scenario       | config                                                    || expectMapConfig | expectQueueConfig
+            'Map Config'   | HazelcastCacheConfig.createMapConfig('my map config')     || true            | false
+            'Queue Config' | HazelcastCacheConfig.createQueueConfig('my queue config') || false           | true
+    }
+
+}
  *  ============LICENSE_END=========================================================
  */
 
-package org.onap.cps.spi.exceptions;
+package org.onap.cps.config
 
-/**
- * Operation Not Yet Supported Exception.
- * Indicates the operation is not supported and has intention to be supported in the future.
- */
-
-public class OperationNotYetSupportedException extends CpsException {
+import spock.lang.Specification
 
-    private static final long serialVersionUID = 1517903069236383746L;
+class CacheConfigSpec extends Specification {
 
-    /**
-     * Constructor.
-     *
-     * @param details reason for the exception
-     */
-    public OperationNotYetSupportedException(final String details) {
-        super("Operation Not Yet Supported Exception", details);
+    def 'Create Cache Config. (easiest test ever)'() {
+        expect: 'can create a Cache Config'
+            new CacheConfig() != null
     }
+
 }
@@ -1,7 +1,7 @@
 /*
  *  ============LICENSE_START=======================================================
  *  Copyright (c) 2021-2022 Bell Canada.
- *  Modifications Copyright (c) 2022 Nordix Foundation
+ *  Modifications Copyright (c) 2022-2023 Nordix Foundation
  *  Modifications Copyright (C) 2023 TechMahindra Ltd.
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
@@ -22,6 +22,8 @@
 
 package org.onap.cps.notification
 
+import org.onap.cps.spi.model.DataNode
+
 import java.time.OffsetDateTime
 import java.time.format.DateTimeFormatter
 import org.onap.cps.utils.DateTimeUtility
@@ -35,7 +37,7 @@ import org.onap.cps.spi.model.DataNodeBuilder
 import org.springframework.util.StringUtils
 import spock.lang.Specification
 
-class CpsDataUpdateEventFactorySpec extends Specification {
+class CpsDataUpdatedEventFactorySpec extends Specification {
 
     def mockCpsDataService = Mock(CpsDataService)
 
@@ -112,6 +114,22 @@ class CpsDataUpdateEventFactorySpec extends Specification {
             }
     }
 
+    def 'Create CPS Data Event with URI Syntax Exception'() {
+        given: 'an anchor'
+            def anchor = new Anchor('my-anchorname', 'my-dataspace', 'my-schemaset-name')
+        and: 'a mocked data Node (collection)'
+            def mockDataNode = Mock(DataNode)
+            mockCpsDataService.getDataNodes(*_) >> [ mockDataNode ]
+        and: 'a URI syntax exception is thrown somewhere (using datanode as cannot manipulate hardcoded URIs'
+            def originalException = new URISyntaxException('input', 'reason', 0)
+            mockDataNode.getXpath() >> { throw originalException }
+        when: 'attempt to create data updated event'
+            objectUnderTest.createCpsDataUpdatedEvent(anchor, OffsetDateTime.now(), Operation.UPDATE)
+        then: 'the same exception is thrown up'
+            def thrownUp = thrown(URISyntaxException)
+            assert thrownUp == originalException
+    }
+
     def isExpectedDateTimeFormat(String observedTimestamp) {
         try {
             DateTimeFormatter.ofPattern(dateTimeFormat).parse(observedTimestamp)
index d0cd473..89e305a 100644 (file)
@@ -1,6 +1,6 @@
 /*
  * ============LICENSE_START=======================================================
- *  Copyright (C) 2022 Nordix Foundation
+ *  Copyright (C) 2022-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.
@@ -44,15 +44,17 @@ class NotificationErrorHandlerSpec extends Specification{
         ((Logger) LoggerFactory.getLogger(NotificationErrorHandler.class)).detachAndStopAllAppenders();
     }
 
-    def 'Logging exception via notification error handler'() {
-        when: 'some exception occurs'
-            objectUnderTest.onException(new Exception('sample exception'), 'some context')
+    def 'Logging exception via notification error handler #scenario'() {
+        when: 'exception #scenario occurs'
+            objectUnderTest.onException(exception, 'some context')
         then: 'log output results contains the correct error details'
-            def logMessage = logWatcher.list.get(0).getFormattedMessage()
-            logMessage.contains(
-                    "Failed to process \n" +
-                    " Error cause: sample exception \n" +
-                    " Error context: [some context]")
+            def logMessage = logWatcher.list[0].getFormattedMessage()
+            assert logMessage.contains('Failed to process')
+            assert logMessage.contains("Error cause: ${exptectedCauseString}")
+            assert logMessage.contains("Error context: [some context]")
+        where:
+            scenario        | exception                                               || exptectedCauseString
+            'with cause'    | new Exception('message')                                || 'message'
+            'without cause' | new Exception('message', new RuntimeException('cause')) || 'java.lang.RuntimeException: cause'
     }
 }
-
index 2ef468b..f07f89b 100644 (file)
@@ -42,14 +42,14 @@ import java.util.concurrent.TimeUnit
 @ContextConfiguration(classes = [NotificationProperties, NotificationService, NotificationErrorHandler, AsyncConfig])
 class NotificationServiceSpec extends Specification {
 
+    @SpringSpy
+    NotificationProperties spyNotificationProperties
     @SpringBean
     NotificationPublisher mockNotificationPublisher = Mock()
     @SpringBean
     CpsDataUpdatedEventFactory mockCpsDataUpdatedEventFactory = Mock()
     @SpringSpy
     NotificationErrorHandler spyNotificationErrorHandler
-    @SpringSpy
-    NotificationProperties spyNotificationProperties
     @SpringBean
     CpsAdminService mockCpsAdminService = Mock()
 
@@ -146,4 +146,13 @@ class NotificationServiceSpec extends Specification {
             notThrown Exception
             1 * spyNotificationErrorHandler.onException(_, _, _, '/', Operation.CREATE)
     }
+
+    def 'Disabled Notification services'() {
+        given: 'a notification service that is disabled'
+            spyNotificationProperties.enabled >> false
+            NotificationService notificationService = new NotificationService(spyNotificationProperties, mockNotificationPublisher, mockCpsDataUpdatedEventFactory, spyNotificationErrorHandler, mockCpsAdminService)
+            notificationService.init()
+        expect: 'it will not send notifications'
+            assert notificationService.shouldSendNotification('') == false
+    }
 }
index 24f3487..c195847 100644 (file)
@@ -21,6 +21,7 @@
 
 package org.onap.cps.spi
 
+import org.onap.cps.spi.exceptions.DataValidationException
 import spock.lang.Specification
 
 class FetchDescendantsOptionSpec extends Specification {
@@ -74,10 +75,10 @@ class FetchDescendantsOptionSpec extends Specification {
             thrown IllegalArgumentException
     }
 
-    def 'Create fetch descendant option with  descendant using #scenario.'() {
-        when: 'the next level of depth is not allowed'
-           def FetchDescendantsOption fetchDescendantsOption = FetchDescendantsOption.getFetchDescendantsOption(fetchDescendantsOptionAsString)
-        then: 'fetch descendant object created'
+    def 'Create fetch descendant option from string scenario: #scenario.'() {
+        when: 'create fetch descendant option from string'
+           def fetchDescendantsOption = FetchDescendantsOption.getFetchDescendantsOption(fetchDescendantsOptionAsString)
+        then: 'fetch descendant object created with correct depth'
             assert fetchDescendantsOption.depth == expectedDepth
         where: 'following parameters are used'
             scenario                            | fetchDescendantsOptionAsString || expectedDepth
@@ -85,10 +86,21 @@ class FetchDescendantsOptionSpec extends Specification {
             'all descendants using all'         | 'all'                          || -1
             'No descendants by default'         | ''                             || 0
             'No descendants using none'         | 'none'                         || 0
+            'No descendants using number'       | '0'                            || 0
+            'direct child using number'         | '1'                            || 1
+            'direct child using direct'         | 'direct'                       || 1
             'til 10th descendants using number' | '10'                           || 10
     }
 
-    def 'String values.'() {
+    def 'Create fetch descendant option from string with invalid string.'() {
+        when: 'attempt to create fetch descendant option from invalid string'
+            FetchDescendantsOption.getFetchDescendantsOption('invalid-string')
+        then: 'a validation exception is thrown with the invalid string in the details'
+            def thrown = thrown(DataValidationException)
+            thrown.details.contains('invalid-string')
+    }
+
+    def 'Convert to string.'() {
         expect: 'each fetch descendant option has the correct String value'
             assert fetchDescendantsOption.toString() == expectedStringValue
         where: 'the following option is used'
diff --git a/cps-service/src/test/groovy/org/onap/cps/spi/model/ConditionPropertiesSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/spi/model/ConditionPropertiesSpec.groovy
new file mode 100644 (file)
index 0000000..c844690
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ *  ============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.spi.model
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import org.onap.cps.utils.JsonObjectMapper
+import spock.lang.Specification
+
+class ConditionPropertiesSpec extends Specification {
+
+    ObjectMapper objectMapper = new ObjectMapper()
+
+    def 'Condition Properties JSON conversion.'() {
+        given: 'a condition properties'
+            def objectUnderTest = new ConditionProperties(conditionName: 'test', conditionParameters: [ [ key : 'value' ] ])
+        expect: 'the name is blank'
+            assert objectMapper.writeValueAsString(objectUnderTest) == '{"conditionName":"test","conditionParameters":[{"key":"value"}]}'
+    }
+
+}
index 1559783..fcbae62 100644 (file)
@@ -1,7 +1,7 @@
 /*
  *  ============LICENSE_START=======================================================
  *  Copyright (C) 2021 Pantheon.tech
- *  Modifications Copyright (C) 2021-2022 Nordix Foundation.
+ *  Modifications Copyright (C) 2021-2023 Nordix Foundation.
  *  Modifications Copyright (C) 2022 TechMahindra Ltd.
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
 package org.onap.cps.spi.model
 
 import org.onap.cps.TestUtils
+import org.onap.cps.spi.exceptions.DataValidationException
 import org.onap.cps.utils.DataMapUtils
 import org.onap.cps.utils.YangUtils
 import org.onap.cps.yang.YangTextSchemaSourceSetBuilder
 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode
+import org.opendaylight.yangtools.yang.data.api.schema.ForeignDataNode
 import spock.lang.Specification
 
 class DataNodeBuilderSpec extends Specification {
 
-    Map<String, Map<String, Serializable>> expectedLeavesByXpathMap = [
+    def objectUnderTest = new DataNodeBuilder()
+
+    def expectedLeavesByXpathMap = [
             '/test-tree'                                            : [],
             '/test-tree/branch[@name=\'Left\']'                     : [name: 'Left'],
             '/test-tree/branch[@name=\'Left\']/nest'                : [name: 'Small', birds: ['Sparrow', 'Robin', 'Finch']],
@@ -56,7 +60,7 @@ class DataNodeBuilderSpec extends Specification {
             def jsonData = TestUtils.getResourceFileContent('test-tree.json')
             def containerNode = YangUtils.parseJsonData(jsonData, schemaContext)
         when: 'the container node is converted to a data node'
-            def result = new DataNodeBuilder().withContainerNode(containerNode).build()
+            def result = objectUnderTest.withContainerNode(containerNode).build()
             def mappedResult = TestUtils.getFlattenMapByXpath(result)
         then: '6 DataNode objects with unique xpath were created in total'
             mappedResult.size() == 6
@@ -76,16 +80,12 @@ class DataNodeBuilderSpec extends Specification {
             def jsonData = '{ "branch": [{ "name": "Branch", "nest": { "name": "Nest", "birds": ["bird"] } }] }'
             def containerNode = YangUtils.parseJsonData(jsonData, schemaContext, "/test-tree")
         when: 'the container node is converted to a data node with parent node xpath defined'
-            def result = new DataNodeBuilder()
-                    .withContainerNode(containerNode)
-                    .withParentNodeXpath("/test-tree")
-                    .build()
+            def result = objectUnderTest.withContainerNode(containerNode).withParentNodeXpath('/test-tree').build()
             def mappedResult = TestUtils.getFlattenMapByXpath(result)
         then: '2 DataNode objects with unique xpath were created in total'
             mappedResult.size() == 2
         and: 'all expected xpaths were built'
-            mappedResult.keySet()
-                    .containsAll(['/test-tree/branch[@name=\'Branch\']', '/test-tree/branch[@name=\'Branch\']/nest'])
+            mappedResult.keySet().containsAll(['/test-tree/branch[@name=\'Branch\']', '/test-tree/branch[@name=\'Branch\']/nest'])
     }
 
     def 'Converting ContainerNode (tree) to a DataNode (tree) -- augmentation case.'() {
@@ -96,11 +96,10 @@ class DataNodeBuilderSpec extends Specification {
             def jsonData = TestUtils.getResourceFileContent('ietf/data/ietf-network-topology-sample-rfc8345.json')
             def containerNode = YangUtils.parseJsonData(jsonData, schemaContext)
         when: 'the container node is converted to a data node '
-            def result = new DataNodeBuilder().withContainerNode(containerNode).build()
+            def result = objectUnderTest.withContainerNode(containerNode).build()
             def mappedResult = TestUtils.getFlattenMapByXpath(result)
         then: 'all expected data nodes are populated'
             mappedResult.size() == 32
-            println(mappedResult.keySet().sort())
         and: 'xpaths for augmentation nodes (link and termination-point nodes) were built correctly'
             mappedResult.keySet().containsAll([
                     "/networks/network[@network-id='otn-hc']/link[@link-id='D1,1-2-1,D2,2-1-1']",
@@ -130,8 +129,7 @@ class DataNodeBuilderSpec extends Specification {
             def jsonData = '{"source": {"source-node": "D1", "source-tp": "1-2-1"}}'
             def containerNode = YangUtils.parseJsonData(jsonData, schemaContext, parentNodeXpath)
         when: 'the container node is converted to a data node with given parent node xpath'
-            def result = new DataNodeBuilder().withContainerNode(containerNode)
-                    .withParentNodeXpath(parentNodeXpath).build()
+            def result = objectUnderTest.withContainerNode(containerNode).withParentNodeXpath(parentNodeXpath).build()
         then: 'the resulting data node represents a child of augmentation node'
             assert result.xpath == "/networks/network[@network-id='otn-hc']/link[@link-id='D1,1-2-1,D2,2-1-1']/source"
             assert result.leaves['source-node'] == 'D1'
@@ -146,15 +144,13 @@ class DataNodeBuilderSpec extends Specification {
             def jsonData = TestUtils.getResourceFileContent('data-with-choice-node.json')
             def containerNode = YangUtils.parseJsonData(jsonData, schemaContext)
         when: 'the container node is converted to a data node'
-            def result = new DataNodeBuilder().withContainerNode(containerNode).build()
+            def result = objectUnderTest.withContainerNode(containerNode).build()
             def mappedResult = TestUtils.getFlattenMapByXpath(result)
         then: 'the resulting data node contains only one xpath with 3 leaves'
-            mappedResult.keySet().containsAll([
-                "/container-with-choice-leaves"
-            ])
-            assert result.leaves['leaf-1'] == "test"
-            assert result.leaves['choice-case1-leaf-a'] == "test"
-            assert result.leaves['choice-case1-leaf-b'] == "test"
+            mappedResult.keySet().containsAll([ '/container-with-choice-leaves' ])
+            assert result.leaves['leaf-1'] == 'test'
+            assert result.leaves['choice-case1-leaf-a'] == 'test'
+            assert result.leaves['choice-case1-leaf-b'] == 'test'
     }
 
     def 'Converting ContainerNode into DataNode collection: #scenario.'() {
@@ -162,12 +158,11 @@ class DataNodeBuilderSpec extends Specification {
             def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('test-tree.yang')
             def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent) getSchemaContext()
         and: 'parent node xpath referencing parent of list element'
-            def parentNodeXpath = "/test-tree"
+            def parentNodeXpath = '/test-tree'
         and: 'the json data fragment (list element) parsed into container node object'
             def containerNode = YangUtils.parseJsonData(jsonData, schemaContext, parentNodeXpath)
         when: 'the container node is converted to a data node collection'
-            def result = new DataNodeBuilder().withContainerNode(containerNode)
-                    .withParentNodeXpath(parentNodeXpath).buildCollection()
+            def result = objectUnderTest.withContainerNode(containerNode).withParentNodeXpath(parentNodeXpath).buildCollection()
             def resultXpaths = result.collect { it.getXpath() }
         then: 'the resulting collection contains data nodes for expected list elements'
             assert resultXpaths.size() == expectedSize
@@ -178,15 +173,43 @@ class DataNodeBuilderSpec extends Specification {
             'multiple entries' | '{"branch": [{"name": "One"}, {"name": "Two"}]}' | 2            | ['/test-tree/branch[@name=\'One\']', '/test-tree/branch[@name=\'Two\']']
     }
 
-    def 'Converting ContainerNode to a DataNode collection -- edge cases: #scenario.'() {
-        when: 'the container node is #node'
-            def result = new DataNodeBuilder().withContainerNode(containerNode).buildCollection()
-        then: 'the resulting collection contains data nodes for expected list elements'
-            assert result.isEmpty()
-        where: 'following parameters are used'
-            scenario                               | containerNode
-            'ContainerNode is null'                | null
-            'ContainerNode is an unsupported type' | Mock(ContainerNode)
+    def 'Converting ContainerNode to a Collection with #scenario.'() {
+        expect: 'converting null to a collection returns an empty collection'
+            assert objectUnderTest.withContainerNode(containerNode).buildCollection().isEmpty()
+        where: 'the following container node is used'
+            scenario              | containerNode
+            'null object'         | null
+            'object without body' | Mock(ContainerNode)
+    }
+
+    def 'Converting ContainerNode to a DataNode with unsupported Normalized Node.'() {
+        given: 'a container node of an unsupported type'
+            def mockContainerNode = Mock(ContainerNode)
+            mockContainerNode.body() >> [ Mock(ForeignDataNode) ]
+        when: 'attempt to convert it'
+            objectUnderTest.withContainerNode(mockContainerNode).build()
+        then: 'a data validation exception is thrown'
+            thrown(DataValidationException)
+    }
+
+    def 'Build datanode from attributes.'() {
+        when: 'data node is built'
+            def result = new DataNodeBuilder()
+                .withDataspace('my dataspace')
+                .withAnchor('my anchor')
+                .withModuleNamePrefix('my prefix')
+                .withXpath('some xpath')
+                .withLeaves([leaf1: 'value1'])
+                .withChildDataNodes([Mock(DataNode)])
+                .build()
+        then: 'the datanode has all the defined attributes'
+            assert result.dataspace == 'my dataspace'
+            assert result.anchorName == 'my anchor'
+            assert result.moduleNamePrefix == 'my prefix'
+            assert result.moduleNamePrefix == 'my prefix'
+            assert result.xpath == 'some xpath'
+            assert result.leaves == [leaf1: 'value1']
+            assert result.childDataNodes.size() == 1
     }
 
     def 'Use of adding the module name prefix attribute of data node.'() {
index 2332282..8cbd493 100644 (file)
@@ -46,13 +46,23 @@ class JsonObjectMapperSpec extends Specification {
             type << ['String', 'bytes']
     }
 
+    def 'Convert to bytes with processing exception.'() {
+        given: 'the object mapper throws an processing exception'
+            spiedObjectMapper.writeValueAsBytes(_) >> { throw new JsonProcessingException('message from cause')}
+        when: 'attempt to convert an object to bytes'
+            jsonObjectMapper.asJsonBytes('does not matter')
+        then: 'a data validation exception is thrown with the original exception message as details'
+            def thrown = thrown(DataValidationException)
+            assert thrown.details == 'message from cause'
+    }
+
     def 'Map a structured object to json String error.'() {
         given: 'some object'
             def object = new Object()
         and: 'the Object mapper throws an exception'
             spiedObjectMapper.writeValueAsString(object) >> { throw new JsonProcessingException('Sample problem'){} }
         when: 'attempting to convert the object to a string'
-            jsonObjectMapper.asJsonString(object);
+            jsonObjectMapper.asJsonString(object)
         then: 'a Data Validation Exception is thrown'
             def thrown = thrown(DataValidationException)
         and: 'the details containing the original error message'
@@ -63,21 +73,27 @@ class JsonObjectMapperSpec extends Specification {
         given: 'a map object model'
             def contentMap = new JsonSlurper().parseText(TestUtils.getResourceFileContent('bookstore.json'))
         when: 'converted into a Map'
-            def result = jsonObjectMapper.convertToValueType(contentMap, Map);
+            def result = jsonObjectMapper.convertToValueType(contentMap, Map)
         then: 'the result is a mapped into class of type Map'
             assert result instanceof Map
         and: 'the map contains the expected key'
             assert result.containsKey('test:bookstore')
             assert result.'test:bookstore'.categories[0].name == 'SciFi'
+    }
 
+    def 'Mapping a valid json string to class object of specific class type T.'() {
+        given: 'a json string representing a map'
+            def content = '{"key":"value"}'
+        expect: 'the string is converted correctly to a map'
+            jsonObjectMapper.convertJsonString(content, Map) == [ key: 'value' ]
     }
 
     def 'Mapping an unstructured json string to class object of specific class type T.'() {
         given: 'Unstructured json string'
-            def content = '{ "nest": { "birds": "bird"] } }'
+            def content = '{invalid json'
         when: 'mapping json string to given class type'
-            jsonObjectMapper.convertJsonString(content, Map);
-        then: 'an exception is thrown'
+            jsonObjectMapper.convertJsonString(content, Map)
+        then: 'a data validation exception is thrown'
             thrown(DataValidationException)
     }
 
@@ -87,7 +103,7 @@ class JsonObjectMapperSpec extends Specification {
         and: 'Object mapper throws an exception'
             spiedObjectMapper.convertValue(*_) >> { throw new IllegalArgumentException() }
         when: 'converted into specific class type'
-            jsonObjectMapper.convertToValueType(contentMap, Object);
+            jsonObjectMapper.convertToValueType(contentMap, Object)
         then: 'an exception is thrown'
             thrown(DataValidationException)
     }
@@ -96,9 +112,9 @@ class JsonObjectMapperSpec extends Specification {
         given: 'Unstructured object'
             def object = new Object()
         and: 'disable serialization failure on empty bean'
-            spiedObjectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
+            spiedObjectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
         when: 'the object is mapped to string'
-            jsonObjectMapper.asJsonString(object);
+            jsonObjectMapper.asJsonString(object)
         then: 'no exception is thrown'
             noExceptionThrown()
     }
@@ -107,16 +123,16 @@ class JsonObjectMapperSpec extends Specification {
         given: 'Unstructured object'
             def content = '{ "nest": { "birds": "bird" } }'
         when: 'the object is mapped to string'
-            def result = jsonObjectMapper.convertToJsonNode(content);
+            def result = jsonObjectMapper.convertToJsonNode(content)
         then: 'the result is a valid JsonNode'
-            result.fieldNames().next() == "nest"
+            result.fieldNames().next() == 'nest'
     }
 
     def 'Map a unstructured json String to JsonNode.'() {
         given: 'Unstructured object'
             def content = '{ "nest": { "birds": "bird" }] }'
         when: 'the object is mapped to string'
-            jsonObjectMapper.convertToJsonNode(content);
+            jsonObjectMapper.convertToJsonNode(content)
         then: 'a data validation exception is thrown'
             thrown(DataValidationException)
     }
index b044e2e..3864a52 100644 (file)
@@ -1,6 +1,7 @@
 /*
  * ============LICENSE_START=======================================================
  *  Copyright (C) 2022 Deutsche Telekom AG
+ *  Modifications 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.
@@ -21,16 +22,18 @@ package org.onap.cps.utils
 
 import org.onap.cps.TestUtils
 import org.onap.cps.yang.YangTextSchemaSourceSetBuilder
+import org.xml.sax.SAXParseException
 import spock.lang.Specification
 
 class XmlFileUtilsSpec extends Specification {
+
     def 'Parse a valid xml content #scenario'(){
         given: 'YANG model schema context'
             def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('bookstore.yang')
             def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext()
-        when: 'the XML data is parsed'
+        when: 'the xml data is parsed'
             def parsedXmlContent = XmlFileUtils.prepareXmlContent(xmlData, schemaContext)
-        then: 'the result XML is wrapped by root node defined in YANG schema'
+        then: 'the result xml is wrapped by root node defined in YANG schema'
             assert parsedXmlContent == expectedOutput
         where:
             scenario                        | xmlData                                                                   || expectedOutput
@@ -39,13 +42,22 @@ class XmlFileUtilsSpec extends Specification {
             'no xml header'                 | '<stores><class> </class></stores>'                                       || '<stores><class> </class></stores>'
     }
 
+    def 'Parse a invalid xml content'(){
+        given: 'YANG model schema context'
+            def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('bookstore.yang')
+            def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext()
+        when: 'attempt to parse invalid xml'
+            XmlFileUtils.prepareXmlContent('invalid-xml', schemaContext)
+        then: 'a Sax Parser exception is thrown'
+            thrown(SAXParseException)
+    }
+
     def 'Parse a xml content with XPath container #scenario'() {
         given: 'YANG model schema context'
             def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('test-tree.yang')
             def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext()
         and: 'Parent schema node by xPath'
-            def parentSchemaNode = YangUtils.getDataSchemaNodeAndIdentifiersByXpath(xPath, schemaContext)
-                    .get("dataSchemaNode")
+            def parentSchemaNode = YangUtils.getDataSchemaNodeAndIdentifiersByXpath(xPath, schemaContext).get("dataSchemaNode")
         when: 'the XML data is parsed'
             def parsedXmlContent = XmlFileUtils.prepareXmlContent(xmlData, parentSchemaNode, xPath)
         then: 'the result XML is wrapped by xPath defined parent root node'
@@ -54,8 +66,6 @@ class XmlFileUtilsSpec extends Specification {
             scenario                 | xmlData                                                                                                                                                                                    | xPath                                 || expectedOutput
             'XML element test tree'  | '<?xml version="1.0" encoding="UTF-8"?><test-tree xmlns="org:onap:cps:test:test-tree"><branch><name>Left</name><nest><name>Small</name><birds>Sparrow</birds></nest></branch></test-tree>' | '/test-tree'                          || '<?xml version="1.0" encoding="UTF-8"?><test-tree xmlns="org:onap:cps:test:test-tree"><branch><name>Left</name><nest><name>Small</name><birds>Sparrow</birds></nest></branch></test-tree>'
             'without root data node' | '<?xml version="1.0" encoding="UTF-8"?><nest xmlns="org:onap:cps:test:test-tree"><name>Small</name><birds>Sparrow</birds></nest>'                                                          | '/test-tree/branch[@name=\'Branch\']' || '<?xml version="1.0" encoding="UTF-8"?><branch xmlns="org:onap:cps:test:test-tree"><name>Branch</name><nest><name>Small</name><birds>Sparrow</birds></nest></branch>'
-
-
     }
 
 }
index 50b6306..e6344d3 100644 (file)
@@ -1,6 +1,6 @@
 /*
  * ============LICENSE_START=======================================================
- *  Copyright (C) 2020-2022 Nordix Foundation
+ *  Copyright (C) 2020-2023 Nordix Foundation
  *  Modifications Copyright (C) 2021 Pantheon.tech
  *  Modifications Copyright (C) 2022 TechMahindra Ltd.
  *  Modifications Copyright (C) 2022 Deutsche Telekom AG
@@ -27,6 +27,7 @@ import org.onap.cps.TestUtils
 import org.onap.cps.spi.exceptions.DataValidationException
 import org.onap.cps.yang.YangTextSchemaSourceSetBuilder
 import org.opendaylight.yangtools.yang.common.QName
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier
 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode
 import spock.lang.Specification
 
@@ -162,4 +163,12 @@ class YangUtilsSpec extends Specification {
             'xpath contains list attribute'                | '/test-tree/branch[@name=\'Branch\']'                               || ['test-tree','branch']
             'xpath contains list attributes with /'        | '/test-tree/branch[@name=\'/Branch\']/categories[@id=\'/broken\']'  || ['test-tree','branch','categories']
     }
+
+    def 'Get key attribute statement without key attributes'() {
+        given: 'a path argument without key attributes'
+            def mockPathArgument = Mock(YangInstanceIdentifier.NodeIdentifierWithPredicates)
+            mockPathArgument.entrySet() >> [ ]
+        expect: 'the result is an empty string'
+            YangUtils.getKeyAttributesStatement(mockPathArgument) == ''
+    }
 }
index 3b4d57d..2739281 100644 (file)
 
 package org.onap.cps.yang
 
-
 import org.onap.cps.TestUtils
 import org.onap.cps.spi.exceptions.ModelValidationException
-import org.onap.cps.yang.YangTextSchemaSourceSetBuilder
 import org.opendaylight.yangtools.yang.common.Revision
 import spock.lang.Specification
 
+import java.nio.charset.StandardCharsets
+
 class YangTextSchemaSourceSetBuilderSpec extends Specification {
 
     def 'Building a valid YangTextSchemaSourceSet using #filenameCase filename.'() {
@@ -62,4 +62,16 @@ class YangTextSchemaSourceSetBuilderSpec extends Specification {
             'invalid-empty.yang'          | 'no valid content'     || ModelValidationException
             'invalid-missing-import.yang' | 'no dependency module' || ModelValidationException
     }
+
+    def 'Convert yang source to a YangTextSchemaSource.'() {
+        given: 'a yang source text'
+            def yangSourceText = TestUtils.getResourceFileContent('bookstore.yang')
+        when: 'convert it to a YangTextSchemaSource'
+            def result = YangTextSchemaSourceSetBuilder.toYangTextSchemaSource('some name', yangSourceText)
+        then: 'the converted object has correct properties'
+            assert result.toString() == '{identifier=RevisionSourceIdentifier [name=some name]}'
+            assert new String(result.openStream().readAllBytes(), StandardCharsets.UTF_8) ==  yangSourceText
+        and: 'it has no symbolic name'
+            assert result.getSymbolicName().isEmpty()
+    }
 }
index ace45f8..eb6c424 100644 (file)
@@ -1316,8 +1316,8 @@ paths:
           schema:
             default: /
             type: string
-        - description: "Number of descendants to query. Allowed values are 'none', 'all',\
-          \ -1 (for all), 0 (for none) and any positive number."
+        - description: "Number of descendants to query. Allowed values are 'none', 'all', 'direct',\
+          \ 1 (for direct), -1 (for all), 0 (for none) and any positive number."
           in: query
           name: descendants
           required: false
@@ -2261,8 +2261,8 @@ paths:
           schema:
             default: /
             type: string
-        - description: "Number of descendants to query. Allowed values are 'none', 'all',\
-          \ -1 (for all), 0 (for none) and any positive number."
+        - description: "Number of descendants to query. Allowed values are 'none', 'all', 'direct',\
+          \ 1 (for direct), -1 (for all), 0 (for none) and any positive number."
           in: query
           name: descendants
           required: false
@@ -2350,8 +2350,8 @@ paths:
           schema:
             default: /
             type: string
-        - description: "Number of descendants to query. Allowed values are 'none', 'all',\
-          \ -1 (for all), 0 (for none) and any positive number."
+        - description: "Number of descendants to query. Allowed values are 'none', 'all', 'direct',\
+          \ 1 (for direct), -1 (for all), 0 (for none) and any positive number."
           in: query
           name: descendants
           required: false
@@ -2532,8 +2532,8 @@ components:
         example: false
         type: boolean
     descendantsInQuery:
-      description: "Number of descendants to query. Allowed values are 'none', 'all',\
-        \ -1 (for all), 0 (for none) and any positive number."
+      description: "Number of descendants to query. Allowed values are 'none', 'all', 'direct',\
+          \ 1 (for direct), -1 (for all), 0 (for none) and any positive number."
       in: query
       name: descendants
       required: false
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 7c257ad..678aa64 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 74070b1..8a3bd6d 100644 (file)
@@ -26,7 +26,10 @@ import org.springframework.web.multipart.MultipartFile
 
 class CpsPerfTestBase extends PerfTestBase {
 
-    static def CPS_PERFORMANCE_TEST_DATASPACE = 'cpsPerformanceDataspace'
+    static final def CPS_PERFORMANCE_TEST_DATASPACE = 'cpsPerformanceDataspace'
+    static final def OPENROADM_ANCHORS = 5
+    static final def OPENROADM_DEVICES_PER_ANCHOR = 50
+    static final def OPENROADM_DATANODES_PER_DEVICE = 86
 
     def printTitle() {
         println('##        C P S   P E R F O R M A N C E   T E S T   R E S U L T S          ##')
@@ -76,9 +79,9 @@ class CpsPerfTestBase extends PerfTestBase {
     }
 
     def addOpenRoadData() {
-        def data = generateOpenRoadData(50)
+        def data = generateOpenRoadData(OPENROADM_DEVICES_PER_ANCHOR)
         stopWatch.start()
-        addAnchorsWithData(5, CPS_PERFORMANCE_TEST_DATASPACE, LARGE_SCHEMA_SET, 'openroadm', data)
+        addAnchorsWithData(OPENROADM_ANCHORS, CPS_PERFORMANCE_TEST_DATASPACE, LARGE_SCHEMA_SET, 'openroadm', data)
         stopWatch.stop()
         def durationInMillis = stopWatch.getTotalTimeMillis()
         recordAndAssertPerformance('Creating openroadm anchors with large data tree', 20_000, durationInMillis)
index e0df2fe..9cb65ab 100644 (file)
@@ -23,9 +23,9 @@ package org.onap.cps.integration.performance.cps
 import java.time.OffsetDateTime
 import org.onap.cps.api.CpsDataService
 import org.onap.cps.integration.performance.base.CpsPerfTestBase
-import org.onap.cps.spi.exceptions.DataNodeNotFoundException
 
-import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
+import static org.onap.cps.spi.FetchDescendantsOption.DIRECT_CHILDREN_ONLY
+import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
 
 class CpsDataServiceLimitsPerfTest extends CpsPerfTestBase {
 
@@ -33,31 +33,67 @@ class CpsDataServiceLimitsPerfTest extends CpsPerfTestBase {
 
     def setup() { objectUnderTest = cpsDataService }
 
-    def 'Multiple get limit exceeded: 32,764 (~ 2^15) xpaths.'() {
-        given: 'more than 32,764 xpaths'
-            def xpaths = (0..40_000).collect { "/size/of/this/path/does/not/matter/for/limit[@id='" + it + "']" }
-        when: 'single operation is executed to get all datanodes with given xpaths'
-            objectUnderTest.getDataNodesForMultipleXpaths(CPS_PERFORMANCE_TEST_DATASPACE, 'bookstore1', xpaths, INCLUDE_ALL_DESCENDANTS)
-        then: 'a database exception is not thrown'
-            noExceptionThrown()
+    def 'Create 33,000 books (note further tests depend on this running first).'() {
+        given: 'an anchor containing a bookstore with one category'
+            cpsAdminService.createAnchor(CPS_PERFORMANCE_TEST_DATASPACE, BOOKSTORE_SCHEMA_SET, 'limitsAnchor')
+            def parentNodeData = '{"bookstore": { "categories": [{ "code": 1, "name": "Test", "books" : [] }] }}'
+            cpsDataService.saveData(CPS_PERFORMANCE_TEST_DATASPACE, 'limitsAnchor', parentNodeData, OffsetDateTime.now())
+        when: '33,000 books are added'
+            stopWatch.start()
+            for (int i = 1; i <= 33_000; i+=100) {
+                def booksData = '{"books":[' + (i..<i+100).collect {'{ "title": "' + it + '" }' }.join(',') + ']}'
+                cpsDataService.saveData(CPS_PERFORMANCE_TEST_DATASPACE, 'limitsAnchor', '/bookstore/categories[@code=1]', booksData, OffsetDateTime.now())
+            }
+            stopWatch.stop()
+            def durationInMillis = stopWatch.getTotalTimeMillis()
+        then: 'the operation completes within 10 seconds'
+            recordAndAssertPerformance("Creating 33,000 books", 10_000, durationInMillis)
+    }
+
+    def 'Get data nodes from multiple xpaths 32K (2^15) limit exceeded.'() {
+        given: '33,000 xpaths'
+            def xpaths = (1..33_000).collect { "/bookstore/categories[@code=1]/books[@title='${it}']".toString() }
+        when: 'a single operation is executed to get all datanodes with given xpaths'
+            def results = objectUnderTest.getDataNodesForMultipleXpaths(CPS_PERFORMANCE_TEST_DATASPACE, 'limitsAnchor', xpaths, OMIT_DESCENDANTS)
+        then: '33,000 data nodes are returned'
+            assert results.size() == 33_000
     }
 
-    def 'Delete multiple datanodes limit exceeded: 32,767 (~ 2^15) xpaths.'() {
-        given: 'more than 32,767 xpaths'
-            def xpaths = (0..40_000).collect { "/size/of/this/path/does/not/matter/for/limit[@id='" + it + "']" }
-        when: 'single operation is executed to delete all datanodes with given xpaths'
-            objectUnderTest.deleteDataNodes(CPS_PERFORMANCE_TEST_DATASPACE, 'bookstore1', xpaths, OffsetDateTime.now())
-        then: 'a database exception is not thrown (but a CPS DataNodeNotFoundException is thrown)'
-            thrown(DataNodeNotFoundException.class)
+    def 'Delete multiple data nodes 32K (2^15) limit exceeded.'() {
+        given: 'existing data nodes'
+            def countOfDataNodesBeforeDelete = countDataNodes()
+        and: 'a list of 33,000 xpaths'
+            def xpaths = (1..33_000).collect { "/bookstore/categories[@code=1]/books[@title='${it}']".toString() }
+        when: 'a single operation is executed to delete all datanodes with given xpaths'
+            objectUnderTest.deleteDataNodes(CPS_PERFORMANCE_TEST_DATASPACE, 'limitsAnchor', xpaths, OffsetDateTime.now())
+        then: '33,000 data nodes are deleted'
+            def countOfDataNodesAfterDelete = countDataNodes()
+            assert countOfDataNodesBeforeDelete - countOfDataNodesAfterDelete == 33_000
     }
 
-    def 'Delete datanodes from multiple anchors limit exceeded: 32,766 (~ 2^15) anchors.'() {
-        given: 'more than 32,766 anchor names'
-            def anchorNames = (0..40_000).collect { "size-of-this-name-does-not-matter-for-limit-" + it }
-        when: 'single operation is executed to delete all datanodes in given anchors'
+    def 'Delete data nodes from multiple anchors 32K (2^15) limit exceeded.'() {
+        given: '33,000 anchor names'
+            def anchorNames = (1..33_000).collect { "size-of-this-name-does-not-matter-for-limit-" + it }
+        when: 'single operation is executed to delete all datanodes in given anchors'
             objectUnderTest.deleteDataNodes(CPS_PERFORMANCE_TEST_DATASPACE, anchorNames, OffsetDateTime.now())
         then: 'a database exception is not thrown'
             noExceptionThrown()
     }
 
+    def 'Clean up test data.'() {
+        when:
+            stopWatch.start()
+            cpsDataService.deleteDataNodes(CPS_PERFORMANCE_TEST_DATASPACE, 'limitsAnchor', OffsetDateTime.now())
+            cpsAdminService.deleteAnchor(CPS_PERFORMANCE_TEST_DATASPACE, 'limitsAnchor')
+            stopWatch.stop()
+            def durationInMillis = stopWatch.getTotalTimeMillis()
+        then: 'test data is deleted in 10 seconds'
+            recordAndAssertPerformance("Deleting test data", 10_000, durationInMillis)
+    }
+
+    def countDataNodes() {
+        def results = objectUnderTest.getDataNodes(CPS_PERFORMANCE_TEST_DATASPACE, 'limitsAnchor', '/bookstore/categories[@code=1]', DIRECT_CHILDREN_ONLY)
+        return results[0].childDataNodes.size()
+    }
+
 }
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..a11dc35 100644 (file)
@@ -45,28 +45,43 @@ class GetPerfTest extends CpsPerfTestBase {
         where: 'the following parameters are used'
             scenario             | fetchDescendantsOption  | anchor       || durationLimit | expectedNumberOfDataNodes
             'no descendants'     | OMIT_DESCENDANTS        | 'openroadm1' || 50            | 1
-            'direct descendants' | DIRECT_CHILDREN_ONLY    | 'openroadm2' || 100           | 1 + 50
-            'all descendants'    | INCLUDE_ALL_DESCENDANTS | 'openroadm3' || 200           | 1 + 50 * 86
+            'direct descendants' | DIRECT_CHILDREN_ONLY    | 'openroadm2' || 100           | 1 + OPENROADM_DEVICES_PER_ANCHOR
+            'all descendants'    | INCLUDE_ALL_DESCENDANTS | 'openroadm3' || 200           | 1 + OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE
     }
 
     def 'Read data trees for multiple xpaths'() {
         given: 'a collection of xpaths to get'
-            def xpaths = (1..50).collect { "/openroadm-devices/openroadm-device[@device-id='C201-7-1A-" + it + "']" }
+            def xpaths = (1..OPENROADM_DEVICES_PER_ANCHOR).collect { "/openroadm-devices/openroadm-device[@device-id='C201-7-1A-" + 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()
-            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) == OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE
+        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()
             (1..5).each {
-                def result = objectUnderTest.getDataNodes(CPS_PERFORMANCE_TEST_DATASPACE, anchorPrefix + it, xpath, INCLUDE_ALL_DESCENDANTS)
+                def result = objectUnderTest.getDataNodes(CPS_PERFORMANCE_TEST_DATASPACE, 'openroadm' + it, xpath, INCLUDE_ALL_DESCENDANTS)
                 assert countDataNodesInTree(result) == expectedNumberOfDataNodes
             }
             stopWatch.stop()
@@ -74,11 +89,10 @@ 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                | xpath                                  || durationLimit | expectedNumberOfDataNodes
+            'openroadm root'        | '/'                                    || 600           | 1 + OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE
+            'openroadm top element' | '/openroadm-devices'                   || 600           | 1 + OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE
+            'openroadm whole list'  | '/openroadm-devices/openroadm-device'  || 600           | OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE
     }
 
 }
index bad3f8a..afcc2ea 100644 (file)
@@ -45,10 +45,11 @@ class QueryPerfTest extends CpsPerfTestBase {
             recordAndAssertPerformance("Query 1 anchor ${scenario}", durationLimit, durationInMillis)
         where: 'the following parameters are used'
             scenario                     | anchor       | cpsPath                                                             || durationLimit | expectedNumberOfDataNodes
-            'top element'                | 'openroadm1' | '/openroadm-devices'                                                || 120           | 50 * 86 + 1
-            '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
+            'top element'                | 'openroadm1' | '/openroadm-devices'                                                || 120           | OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE + 1
+            'leaf condition'             | 'openroadm2' | '//openroadm-device[@ne-state="inservice"]'                         || 200           | OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE
+            'ancestors'                  | 'openroadm3' | '//openroadm-device/ancestor::openroadm-devices'                    || 120           | OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE + 1
+            'leaf condition + ancestors' | 'openroadm4' | '//openroadm-device[@status="success"]/ancestor::openroadm-devices' || 120           | OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE + 1
+            'non-existing data'          | 'openroadm1' | '/path/to/non-existing/node[@id="1"]'                               || 10            | 0
     }
 
     def 'Query complete data trees across all anchors with #scenario.'() {
@@ -63,10 +64,10 @@ class QueryPerfTest extends CpsPerfTestBase {
             recordAndAssertPerformance("Query across anchors ${scenario}", durationLimit, durationInMillis)
         where: 'the following parameters are used'
             scenario                     | cpspath                                                             || durationLimit | expectedNumberOfDataNodes
-            'top element'                | '/openroadm-devices'                                                || 400           | 5 * (50 * 86 + 1)
-            'leaf condition'             | '//openroadm-device[@ne-state="inservice"]'                         || 700           | 5 * (50 * 86)
-            'ancestors'                  | '//openroadm-device/ancestor::openroadm-devices'                    || 400           | 5 * (50 * 86 + 1)
-            'leaf condition + ancestors' | '//openroadm-device[@status="success"]/ancestor::openroadm-devices' || 400           | 5 * (50 * 86 + 1)
+            'top element'                | '/openroadm-devices'                                                || 400           | OPENROADM_ANCHORS * (OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE + 1)
+            'leaf condition'             | '//openroadm-device[@ne-state="inservice"]'                         || 700           | OPENROADM_ANCHORS * (OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE)
+            'ancestors'                  | '//openroadm-device/ancestor::openroadm-devices'                    || 400           | OPENROADM_ANCHORS * (OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE + 1)
+            'leaf condition + ancestors' | '//openroadm-device[@status="success"]/ancestor::openroadm-devices' || 400           | OPENROADM_ANCHORS * (OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE + 1)
     }
 
     def 'Query with leaf condition and #scenario.'() {
@@ -81,9 +82,9 @@ class QueryPerfTest extends CpsPerfTestBase {
             recordAndAssertPerformance("Query with ${scenario}", durationLimit, durationInMillis)
         where: 'the following parameters are used'
             scenario             | fetchDescendantsOption  | anchor       || durationLimit | expectedNumberOfDataNodes
-            'no descendants'     | OMIT_DESCENDANTS        | 'openroadm1' || 15            | 50
-            'direct descendants' | DIRECT_CHILDREN_ONLY    | 'openroadm2' || 60            | 50 * 2
-            'all descendants'    | INCLUDE_ALL_DESCENDANTS | 'openroadm3' || 150           | 50 * 86
+            'no descendants'     | OMIT_DESCENDANTS        | 'openroadm1' || 15            | OPENROADM_DEVICES_PER_ANCHOR
+            'direct descendants' | DIRECT_CHILDREN_ONLY    | 'openroadm2' || 60            | OPENROADM_DEVICES_PER_ANCHOR * 2
+            'all descendants'    | INCLUDE_ALL_DESCENDANTS | 'openroadm3' || 150           | OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE
     }
 
     def 'Query ancestors with #scenario.'() {
@@ -99,8 +100,8 @@ class QueryPerfTest extends CpsPerfTestBase {
         where: 'the following parameters are used'
             scenario             | fetchDescendantsOption  | anchor       || durationLimit | expectedNumberOfDataNodes
             'no descendants'     | OMIT_DESCENDANTS        | 'openroadm1' || 15            | 1
-            'direct descendants' | DIRECT_CHILDREN_ONLY    | 'openroadm2' || 60            | 1 + 50
-            'all descendants'    | INCLUDE_ALL_DESCENDANTS | 'openroadm3' || 150           | 1 + 50 * 86
+            'direct descendants' | DIRECT_CHILDREN_ONLY    | 'openroadm2' || 60            | 1 + OPENROADM_DEVICES_PER_ANCHOR
+            'all descendants'    | INCLUDE_ALL_DESCENDANTS | 'openroadm3' || 150           | 1 + OPENROADM_DEVICES_PER_ANCHOR * OPENROADM_DATANODES_PER_DEVICE
     }
 
 }
diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/WritePerfTest.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/WritePerfTest.groovy
new file mode 100644 (file)
index 0000000..419ec60
--- /dev/null
@@ -0,0 +1,83 @@
+/*
+ *  ============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.integration.performance.cps
+
+import java.time.OffsetDateTime
+import org.onap.cps.integration.performance.base.CpsPerfTestBase
+
+class WritePerfTest extends CpsPerfTestBase {
+
+    def 'Writing openroadm data has linear time.'() {
+        given: 'an empty anchor exists for openroadm'
+            cpsAdminService.createAnchor(CPS_PERFORMANCE_TEST_DATASPACE, LARGE_SCHEMA_SET, 'writeAnchor')
+        and: 'a list of device nodes to add'
+            def jsonData = generateOpenRoadData(totalNodes)
+        when: 'device nodes are added'
+            stopWatch.start()
+            cpsDataService.saveData(CPS_PERFORMANCE_TEST_DATASPACE, 'writeAnchor', jsonData, OffsetDateTime.now())
+            stopWatch.stop()
+            def durationInMillis = stopWatch.getTotalTimeMillis()
+        then: 'the operation takes less than #expectedDuration'
+            recordAndAssertPerformance("Writing ${totalNodes} devices", expectedDuration, durationInMillis)
+        cleanup:
+            cpsDataService.deleteDataNodes(CPS_PERFORMANCE_TEST_DATASPACE, 'writeAnchor', OffsetDateTime.now())
+            cpsAdminService.deleteAnchor(CPS_PERFORMANCE_TEST_DATASPACE, 'writeAnchor')
+        where:
+            totalNodes || expectedDuration
+            50         ||   2_500
+            100        ||   4_000
+            200        ||   8_000
+            400        ||  16_000
+//          800        ||  32_000
+//          1600       ||  64_000
+//          3200       || 128_000
+    }
+
+    def 'Writing bookstore data has exponential time.'() {
+        given: 'an anchor containing a bookstore with a single category'
+            cpsAdminService.createAnchor(CPS_PERFORMANCE_TEST_DATASPACE, BOOKSTORE_SCHEMA_SET, 'writeAnchor')
+            def parentNodeData = '{"bookstore": { "categories": [{ "code": 1, "name": "Test", "books" : [] }] }}'
+            cpsDataService.saveData(CPS_PERFORMANCE_TEST_DATASPACE, 'writeAnchor', parentNodeData, OffsetDateTime.now())
+        and: 'a list of books to add'
+            def booksData = '{"books":[' + (1..totalBooks).collect {'{ "title": "' + it + '" }' }.join(',') + ']}'
+        when: 'books are added'
+            stopWatch.start()
+            cpsDataService.saveData(CPS_PERFORMANCE_TEST_DATASPACE, 'writeAnchor', '/bookstore/categories[@code=1]', booksData, OffsetDateTime.now())
+            stopWatch.stop()
+            def durationInMillis = stopWatch.getTotalTimeMillis()
+        then: 'the operation takes less than #expectedDuration'
+            recordAndAssertPerformance("Writing ${totalBooks} books", expectedDuration, durationInMillis)
+        cleanup:
+            cpsDataService.deleteDataNodes(CPS_PERFORMANCE_TEST_DATASPACE, 'writeAnchor', OffsetDateTime.now())
+            cpsAdminService.deleteAnchor(CPS_PERFORMANCE_TEST_DATASPACE, 'writeAnchor')
+        where:
+            totalBooks || expectedDuration
+            400        ||     200
+            800        ||     500
+            1600       ||   1_000
+            3200       ||   2_500
+            6400       ||  10_000
+//          12800      ||  30_000
+//          25600      || 120_000
+//          51200      || 600_000
+    }
+
+}
diff --git a/pom.xml b/pom.xml
index 119b14b..6e8f4ac 100644 (file)
--- a/pom.xml
+++ b/pom.xml
@@ -32,7 +32,7 @@
 \r
     <groupId>org.onap.cps</groupId>\r
     <artifactId>cps-aggregator</artifactId>\r
-    <version>3.3.5-SNAPSHOT</version>\r
+    <version>3.3.6-SNAPSHOT</version>\r
     <packaging>pom</packaging>\r
 \r
     <name>cps</name>\r