summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--cps-application/src/main/resources/application.yml4
-rw-r--r--cps-ncmp-events/src/main/resources/schemas/avc-subscription-event-outcome-v1.json81
-rw-r--r--cps-ncmp-events/src/main/resources/schemas/avc-subscription-event-v1.json116
-rw-r--r--cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/NcmpEventResponseCode.java6
-rw-r--r--cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandleQueryServiceImpl.java1
-rw-r--r--cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avcsubscription/ResponseTimeoutTask.java9
-rw-r--r--cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventConsumer.java8
-rw-r--r--cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventForwarder.java42
-rw-r--r--cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventMapper.java4
-rw-r--r--cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventResponseConsumer.java35
-rw-r--r--cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventResponseMapper.java29
-rw-r--r--cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventResponseOutcome.java132
-rw-r--r--cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionOutcomeMapper.java109
-rw-r--r--cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/subscriptions/SubscriptionPersistenceImpl.java92
-rw-r--r--cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/subscriptions/SubscriptionStatus.java40
-rw-r--r--cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/CloudEventConstructionException.java41
-rw-r--r--cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/DataNodeHelper.java61
-rw-r--r--cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/SubscriptionEventCloudMapper.java21
-rw-r--r--cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/SubscriptionEventResponseCloudMapper.java57
-rw-r--r--cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/SubscriptionOutcomeCloudMapper.java62
-rw-r--r--cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/yangmodels/YangModelSubscriptionEvent.java9
-rw-r--r--cps-ncmp-service/src/main/resources/model/subscription.yang4
-rw-r--r--cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandleQueryServiceSpec.groovy1
-rw-r--r--cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventConsumerSpec.groovy27
-rw-r--r--cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventForwarderSpec.groovy40
-rw-r--r--cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventResponseConsumerSpec.groovy120
-rw-r--r--cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventResponseMapperSpec.groovy14
-rw-r--r--cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventResponseOutcomeSpec.groovy116
-rw-r--r--cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionOutcomeMapperSpec.groovy50
-rw-r--r--cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/subscriptions/SubscriptionPersistenceSpec.groovy9
-rw-r--r--cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/DataNodeBaseSpec.groovy6
-rw-r--r--cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/DataNodeHelperSpec.groovy39
-rw-r--r--cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/SubscriptionEventCloudMapperSpec.groovy25
-rw-r--r--cps-ncmp-service/src/test/resources/application.yml2
-rw-r--r--cps-ncmp-service/src/test/resources/avcSubscriptionEventResponse.json49
-rw-r--r--cps-ncmp-service/src/test/resources/avcSubscriptionOutcomeEvent.json33
-rw-r--r--cps-ncmp-service/src/test/resources/avcSubscriptionOutcomeEvent2.json20
-rw-r--r--cps-rest/docs/openapi/components.yml2
-rw-r--r--cps-rest/src/test/groovy/org/onap/cps/rest/controller/QueryRestControllerSpec.groovy10
-rw-r--r--cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java68
-rwxr-xr-xcps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepository.java15
-rw-r--r--cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy1
-rw-r--r--cps-service/pom.xml4
-rw-r--r--cps-service/src/main/java/org/onap/cps/notification/CpsDataUpdatedEventFactory.java31
-rw-r--r--cps-service/src/main/java/org/onap/cps/spi/FetchDescendantsOption.java4
-rw-r--r--cps-service/src/main/java/org/onap/cps/spi/model/DataNodeBuilder.java6
-rw-r--r--cps-service/src/main/java/org/onap/cps/utils/XmlFileUtils.java35
-rw-r--r--cps-service/src/main/java/org/onap/cps/yang/YangTextSchemaSourceSetBuilder.java20
-rwxr-xr-xcps-service/src/test/groovy/org/onap/cps/api/impl/CpsAdminServiceImplSpec.groovy16
-rw-r--r--cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy88
-rw-r--r--cps-service/src/test/groovy/org/onap/cps/api/impl/CpsModuleServiceImplSpec.groovy43
-rw-r--r--cps-service/src/test/groovy/org/onap/cps/cache/HazelcastCacheConfigSpec.groovy54
-rw-r--r--cps-service/src/test/groovy/org/onap/cps/config/CacheConfigSpec.groovy (renamed from cps-service/src/main/java/org/onap/cps/spi/exceptions/OperationNotYetSupportedException.java)22
-rw-r--r--cps-service/src/test/groovy/org/onap/cps/notification/CpsDataUpdatedEventFactorySpec.groovy (renamed from cps-service/src/test/groovy/org/onap/cps/notification/CpsDataUpdateEventFactorySpec.groovy)22
-rw-r--r--cps-service/src/test/groovy/org/onap/cps/notification/NotificationErrorHandlerSpec.groovy22
-rw-r--r--cps-service/src/test/groovy/org/onap/cps/notification/NotificationServiceSpec.groovy13
-rw-r--r--cps-service/src/test/groovy/org/onap/cps/spi/FetchDescendantsOptionSpec.groovy22
-rw-r--r--cps-service/src/test/groovy/org/onap/cps/spi/model/ConditionPropertiesSpec.groovy38
-rw-r--r--cps-service/src/test/groovy/org/onap/cps/spi/model/DataNodeBuilderSpec.groovy87
-rw-r--r--cps-service/src/test/groovy/org/onap/cps/utils/JsonObjectMapperSpec.groovy38
-rw-r--r--cps-service/src/test/groovy/org/onap/cps/utils/XmlFileUtilsSpec.groovy22
-rw-r--r--cps-service/src/test/groovy/org/onap/cps/utils/YangUtilsSpec.groovy11
-rw-r--r--cps-service/src/test/groovy/org/onap/cps/yang/YangTextSchemaSourceSetBuilderSpec.groovy16
-rw-r--r--docs/api/swagger/cps/openapi.yaml16
-rw-r--r--docs/cm-handle-lcm-events.rst117
-rw-r--r--docs/cps-events.rst157
-rw-r--r--docs/cps-ncmp-message-status-codes.rst41
-rw-r--r--docs/data-operation-events.rst64
-rw-r--r--docs/modeling.rst15
-rw-r--r--docs/ncmp-async-events.rst54
-rw-r--r--docs/ncmp-data-operation.rst148
-rwxr-xr-xdocs/release-notes.rst2
-rw-r--r--docs/schemas/data-operation-event-schema-1.0.0.json69
-rw-r--r--integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsDataServiceIntegrationSpec.groovy58
-rw-r--r--integration-test/src/test/groovy/org/onap/cps/integration/performance/base/CpsPerfTestBase.groovy9
-rw-r--r--integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/CpsDataServiceLimitsPerfTest.groovy76
-rw-r--r--integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/DeletePerfTest.groovy20
-rw-r--r--integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/GetPerfTest.groovy36
-rw-r--r--integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/QueryPerfTest.groovy27
-rw-r--r--integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/WritePerfTest.groovy83
-rw-r--r--pom.xml2
81 files changed, 2091 insertions, 1127 deletions
diff --git a/cps-application/src/main/resources/application.yml b/cps-application/src/main/resources/application.yml
index ed71339f9..47592b41f 100644
--- a/cps-application/src/main/resources/application.yml
+++ b/cps-application/src/main/resources/application.yml
@@ -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
index 34970ac1c..000000000
--- a/cps-ncmp-events/src/main/resources/schemas/avc-subscription-event-outcome-v1.json
+++ /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
index feff48c36..000000000
--- a/cps-ncmp-events/src/main/resources/schemas/avc-subscription-event-v1.json
+++ /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
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/NcmpEventResponseCode.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/NcmpEventResponseCode.java
index d250c36a8..3b1124983 100644
--- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/NcmpEventResponseCode.java
+++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/NcmpEventResponseCode.java
@@ -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;
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandleQueryServiceImpl.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandleQueryServiceImpl.java
index 54d89ba00..1d390f8d1 100644
--- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandleQueryServiceImpl.java
+++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandleQueryServiceImpl.java
@@ -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.
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avcsubscription/ResponseTimeoutTask.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avcsubscription/ResponseTimeoutTask.java
index c178700ee..176e644ba 100644
--- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avcsubscription/ResponseTimeoutTask.java
+++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avcsubscription/ResponseTimeoutTask.java
@@ -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);
}
}
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventConsumer.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventConsumer.java
index f511965c7..c80b07cb7 100644
--- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventConsumer.java
+++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventConsumer.java
@@ -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 {
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventForwarder.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventForwarder.java
index 1fe963a27..0eda914f2 100644
--- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventForwarder.java
+++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventForwarder.java
@@ -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());
}
}
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventMapper.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventMapper.java
index bf9ceb1c3..35d94cc7a 100644
--- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventMapper.java
+++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventMapper.java
@@ -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());
}
}
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventResponseConsumer.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventResponseConsumer.java
index 20df706c0..ddb9fd6fc 100644
--- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventResponseConsumer.java
+++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventResponseConsumer.java
@@ -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) {
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventResponseMapper.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventResponseMapper.java
index 44181c57c..dc122ee5d 100644
--- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventResponseMapper.java
+++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventResponseMapper.java
@@ -21,36 +21,35 @@
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());
}
}
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventResponseOutcome.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventResponseOutcome.java
index 8fdff1794..9ed686529 100644
--- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventResponseOutcome.java
+++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventResponseOutcome.java
@@ -20,21 +20,20 @@
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;
}
}
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionOutcomeMapper.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionOutcomeMapper.java
index cecde5f81..7803b982f 100644
--- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionOutcomeMapper.java
+++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionOutcomeMapper.java
@@ -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());
}
}
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/subscriptions/SubscriptionPersistenceImpl.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/subscriptions/SubscriptionPersistenceImpl.java
index d2b1237a4..83a375b1b 100644
--- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/subscriptions/SubscriptionPersistenceImpl.java
+++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/subscriptions/SubscriptionPersistenceImpl.java
@@ -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());
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/subscriptions/SubscriptionStatus.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/subscriptions/SubscriptionStatus.java
index ce3b88ba0..63ab102d1 100644
--- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/subscriptions/SubscriptionStatus.java
+++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/subscriptions/SubscriptionStatus.java
@@ -20,36 +20,30 @@
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
index 000000000..d0be344f2
--- /dev/null
+++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/CloudEventConstructionException.java
@@ -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);
+ }
+}
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/DataNodeHelper.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/DataNodeHelper.java
index f42a378fc..c032d1e8a 100644
--- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/DataNodeHelper.java
+++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/DataNodeHelper.java
@@ -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));
}
}
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/SubscriptionEventCloudMapper.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/SubscriptionEventCloudMapper.java
index a7de47904..d0d70cf02 100644
--- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/SubscriptionEventCloudMapper.java
+++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/SubscriptionEventCloudMapper.java
@@ -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
index 000000000..17aba65cf
--- /dev/null
+++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/SubscriptionEventResponseCloudMapper.java
@@ -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
index 000000000..b6cb039a9
--- /dev/null
+++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/utils/SubscriptionOutcomeCloudMapper.java
@@ -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);
+ }
+ }
+}
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/yangmodels/YangModelSubscriptionEvent.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/yangmodels/YangModelSubscriptionEvent.java
index 4dcc5797c..866bfd4e7 100644
--- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/yangmodels/YangModelSubscriptionEvent.java
+++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/yangmodels/YangModelSubscriptionEvent.java
@@ -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";
}
}
}
diff --git a/cps-ncmp-service/src/main/resources/model/subscription.yang b/cps-ncmp-service/src/main/resources/model/subscription.yang
index e332a2898..7096c18ab 100644
--- a/cps-ncmp-service/src/main/resources/model/subscription.yang
+++ b/cps-ncmp-service/src/main/resources/model/subscription.yang
@@ -41,6 +41,10 @@ module subscription {
leaf status {
type string;
}
+
+ leaf details {
+ type string;
+ }
}
leaf datastore {
diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandleQueryServiceSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandleQueryServiceSpec.groovy
index bff822218..93af7f4bf 100644
--- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandleQueryServiceSpec.groovy
+++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandleQueryServiceSpec.groovy
@@ -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.
diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventConsumerSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventConsumerSpec.groovy
index d4ab1e88a..7fa8155a2 100644
--- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventConsumerSpec.groovy
+++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventConsumerSpec.groovy
@@ -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)
}
}
diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventForwarderSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventForwarderSpec.groovy
index 2af32c20e..4193f7554 100644
--- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventForwarderSpec.groovy
+++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventForwarderSpec.groovy
@@ -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) {
diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventResponseConsumerSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventResponseConsumerSpec.groovy
index 5355dd8b9..7cc40cc90 100644
--- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventResponseConsumerSpec.groovy
+++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventResponseConsumerSpec.groovy
@@ -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()
}
}
diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventResponseMapperSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventResponseMapperSpec.groovy
index 00412aa93..4c60281f8 100644
--- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventResponseMapperSpec.groovy
+++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventResponseMapperSpec.groovy
@@ -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
diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventResponseOutcomeSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventResponseOutcomeSpec.groovy
index bb0e7b73a..c1c428b13 100644
--- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventResponseOutcomeSpec.groovy
+++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionEventResponseOutcomeSpec.groovy
@@ -21,13 +21,16 @@
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
- }
}
diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionOutcomeMapperSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionOutcomeMapperSpec.groovy
index 7f1a62829..f5fbdfcb5 100644
--- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionOutcomeMapperSpec.groovy
+++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/events/avcsubscription/SubscriptionOutcomeMapperSpec.groovy
@@ -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
diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/subscriptions/SubscriptionPersistenceSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/subscriptions/SubscriptionPersistenceSpec.groovy
index ec54e8917..7116a1786 100644
--- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/subscriptions/SubscriptionPersistenceSpec.groovy
+++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/subscriptions/SubscriptionPersistenceSpec.groovy
@@ -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)
}
diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/DataNodeBaseSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/DataNodeBaseSpec.groovy
index 7474166ff..e28a10261 100644
--- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/DataNodeBaseSpec.groovy
+++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/DataNodeBaseSpec.groovy
@@ -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
diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/DataNodeHelperSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/DataNodeHelperSpec.groovy
index 819f1fa08..28db7babf 100644
--- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/DataNodeHelperSpec.groovy
+++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/DataNodeHelperSpec.groovy
@@ -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
}
}
diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/SubscriptionEventCloudMapperSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/SubscriptionEventCloudMapperSpec.groovy
index 61eb31910..402344129 100644
--- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/SubscriptionEventCloudMapperSpec.groovy
+++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/utils/SubscriptionEventCloudMapperSpec.groovy
@@ -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'
}
}
diff --git a/cps-ncmp-service/src/test/resources/application.yml b/cps-ncmp-service/src/test/resources/application.yml
index edbd7022f..744267092 100644
--- a/cps-ncmp-service/src/test/resources/application.yml
+++ b/cps-ncmp-service/src/test/resources/application.yml
@@ -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-}
diff --git a/cps-ncmp-service/src/test/resources/avcSubscriptionEventResponse.json b/cps-ncmp-service/src/test/resources/avcSubscriptionEventResponse.json
index 3244f05a0..52ca1df62 100644
--- a/cps-ncmp-service/src/test/resources/avcSubscriptionEventResponse.json
+++ b/cps-ncmp-service/src/test/resources/avcSubscriptionEventResponse.json
@@ -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
diff --git a/cps-ncmp-service/src/test/resources/avcSubscriptionOutcomeEvent.json b/cps-ncmp-service/src/test/resources/avcSubscriptionOutcomeEvent.json
index 6bfa36bf7..2d83bdddc 100644
--- a/cps-ncmp-service/src/test/resources/avcSubscriptionOutcomeEvent.json
+++ b/cps-ncmp-service/src/test/resources/avcSubscriptionOutcomeEvent.json
@@ -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
index 000000000..35ff0241d
--- /dev/null
+++ b/cps-ncmp-service/src/test/resources/avcSubscriptionOutcomeEvent2.json
@@ -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
diff --git a/cps-rest/docs/openapi/components.yml b/cps-rest/docs/openapi/components.yml
index a7c13002b..a72130562 100644
--- a/cps-rest/docs/openapi/components.yml
+++ b/cps-rest/docs/openapi/components.yml
@@ -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
diff --git a/cps-rest/src/test/groovy/org/onap/cps/rest/controller/QueryRestControllerSpec.groovy b/cps-rest/src/test/groovy/org/onap/cps/rest/controller/QueryRestControllerSpec.groovy
index c4bb23ce4..2bf29fcb1 100644
--- a/cps-rest/src/test/groovy/org/onap/cps/rest/controller/QueryRestControllerSpec.groovy
+++ b/cps-rest/src/test/groovy/org/onap/cps/rest/controller/QueryRestControllerSpec.groovy
@@ -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
}
}
diff --git a/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java b/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java
index f4afe3d73..f904e8bdd 100644
--- a/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java
+++ b/cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java
@@ -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) {
diff --git a/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepository.java b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepository.java
index 303af5bc4..7d5be13a5 100755
--- a/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepository.java
+++ b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepository.java
@@ -58,6 +58,17 @@ public interface FragmentRepository extends JpaRepository<FragmentEntity, Long>,
return findByAnchorIdAndXpathIn(anchorEntity.getId(), xpaths.toArray(new String[0]));
}
+ @Query(value = "SELECT * FROM fragment WHERE anchor_id = :anchorId \n"
+ + "AND xpath LIKE :escapedXpath||'[@%]' AND xpath NOT LIKE :escapedXpath||'[@%]/%[@%]'",
+ nativeQuery = true)
+ List<FragmentEntity> findListByAnchorIdAndEscapedXpath(@Param("anchorId") long anchorId,
+ @Param("escapedXpath") String escapedXpath);
+
+ default List<FragmentEntity> findListByAnchorAndXpath(final AnchorEntity anchorEntity, final String xpath) {
+ final String escapedXpath = EscapeUtils.escapeForSqlLike(xpath);
+ return findListByAnchorIdAndEscapedXpath(anchorEntity.getId(), escapedXpath);
+ }
+
@Query(value = "SELECT fragment.* FROM fragment JOIN anchor ON anchor.id = fragment.anchor_id "
+ "WHERE dataspace_id = :dataspaceId AND xpath = ANY (:xpaths)", nativeQuery = true)
List<FragmentEntity> findByDataspaceIdAndXpathIn(@Param("dataspaceId") int dataspaceId,
@@ -110,7 +121,7 @@ public interface FragmentRepository extends JpaRepository<FragmentEntity, Long>,
boolean existsByAnchorAndXpathStartsWith(AnchorEntity anchorEntity, String xpath);
- @Query("SELECT xpath FROM FragmentEntity WHERE anchor = :anchor AND parentId IS NULL")
- List<String> findAllXpathByAnchorAndParentIdIsNull(@Param("anchor") AnchorEntity anchorEntity);
+ @Query(value = "SELECT * FROM fragment WHERE anchor_id = :anchorId AND parent_id IS NULL", nativeQuery = true)
+ List<FragmentEntity> findRootsByAnchorId(@Param("anchorId") long anchorId);
}
diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy
index cb554faee..c72c3046e 100644
--- a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy
+++ b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy
@@ -56,6 +56,7 @@ class CpsDataPersistenceServiceSpec extends Specification {
def setup() {
mockAnchorRepository.getByDataspaceAndName(_, _) >> anchorEntity
mockFragmentRepository.prefetchDescendantsOfFragmentEntities(_, _) >> { fetchDescendantsOption, fragmentEntities -> fragmentEntities }
+ mockFragmentRepository.findListByAnchorAndXpath(_, [] as Set) >> []
}
def 'Storing data nodes individually when batch operation fails'(){
diff --git a/cps-service/pom.xml b/cps-service/pom.xml
index c97623f2a..8bc39b1d4 100644
--- a/cps-service/pom.xml
+++ b/cps-service/pom.xml
@@ -35,10 +35,6 @@
<artifactId>cps-service</artifactId>
- <properties>
- <minimum-coverage>0.95</minimum-coverage>
- </properties>
-
<dependencies>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
diff --git a/cps-service/src/main/java/org/onap/cps/notification/CpsDataUpdatedEventFactory.java b/cps-service/src/main/java/org/onap/cps/notification/CpsDataUpdatedEventFactory.java
index 38f898827..696fd60f8 100644
--- a/cps-service/src/main/java/org/onap/cps/notification/CpsDataUpdatedEventFactory.java
+++ b/cps-service/src/main/java/org/onap/cps/notification/CpsDataUpdatedEventFactory.java
@@ -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;
}
diff --git a/cps-service/src/main/java/org/onap/cps/spi/FetchDescendantsOption.java b/cps-service/src/main/java/org/onap/cps/spi/FetchDescendantsOption.java
index 02574995d..3b90b06cb 100644
--- a/cps-service/src/main/java/org/onap/cps/spi/FetchDescendantsOption.java
+++ b/cps-service/src/main/java/org/onap/cps/spi/FetchDescendantsOption.java
@@ -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);
diff --git a/cps-service/src/main/java/org/onap/cps/spi/model/DataNodeBuilder.java b/cps-service/src/main/java/org/onap/cps/spi/model/DataNodeBuilder.java
index e21293338..b040af5bb 100644
--- a/cps-service/src/main/java/org/onap/cps/spi/model/DataNodeBuilder.java
+++ b/cps-service/src/main/java/org/onap/cps/spi/model/DataNodeBuilder.java
@@ -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 {
}
}
-
}
diff --git a/cps-service/src/main/java/org/onap/cps/utils/XmlFileUtils.java b/cps-service/src/main/java/org/onap/cps/utils/XmlFileUtils.java
index 09f2e16c6..98c7947e1 100644
--- a/cps-service/src/main/java/org/onap/cps/utils/XmlFileUtils.java
+++ b/cps-service/src/main/java/org/onap/cps/utils/XmlFileUtils.java
@@ -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() {
diff --git a/cps-service/src/main/java/org/onap/cps/yang/YangTextSchemaSourceSetBuilder.java b/cps-service/src/main/java/org/onap/cps/yang/YangTextSchemaSourceSetBuilder.java
index deb5b0575..ca907148d 100644
--- a/cps-service/src/main/java/org/onap/cps/yang/YangTextSchemaSourceSetBuilder.java
+++ b/cps-service/src/main/java/org/onap/cps/yang/YangTextSchemaSourceSetBuilder.java
@@ -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);
}
}
diff --git a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsAdminServiceImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsAdminServiceImplSpec.groovy
index 4e0349d2b..eb41e2085 100755
--- a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsAdminServiceImplSpec.groovy
+++ b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsAdminServiceImplSpec.groovy
@@ -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')
diff --git a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy
index 9d241f1bd..b4ac7a68f 100644
--- a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy
+++ b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy
@@ -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
}
+
}
diff --git a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsModuleServiceImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsModuleServiceImplSpec.groovy
index 3884eda66..a794c58fc 100644
--- a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsModuleServiceImplSpec.groovy
+++ b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsModuleServiceImplSpec.groovy
@@ -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
index 000000000..8efd48547
--- /dev/null
+++ b/cps-service/src/test/groovy/org/onap/cps/cache/HazelcastCacheConfigSpec.groovy
@@ -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
+ }
+
+}
diff --git a/cps-service/src/main/java/org/onap/cps/spi/exceptions/OperationNotYetSupportedException.java b/cps-service/src/test/groovy/org/onap/cps/config/CacheConfigSpec.groovy
index 6a4e2a098..b1880d50f 100644
--- a/cps-service/src/main/java/org/onap/cps/spi/exceptions/OperationNotYetSupportedException.java
+++ b/cps-service/src/test/groovy/org/onap/cps/config/CacheConfigSpec.groovy
@@ -18,23 +18,15 @@
* ============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
}
+
}
diff --git a/cps-service/src/test/groovy/org/onap/cps/notification/CpsDataUpdateEventFactorySpec.groovy b/cps-service/src/test/groovy/org/onap/cps/notification/CpsDataUpdatedEventFactorySpec.groovy
index 5dbc2bb04..49f4bf385 100644
--- a/cps-service/src/test/groovy/org/onap/cps/notification/CpsDataUpdateEventFactorySpec.groovy
+++ b/cps-service/src/test/groovy/org/onap/cps/notification/CpsDataUpdatedEventFactorySpec.groovy
@@ -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)
diff --git a/cps-service/src/test/groovy/org/onap/cps/notification/NotificationErrorHandlerSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/notification/NotificationErrorHandlerSpec.groovy
index d0cd47383..89e305aed 100644
--- a/cps-service/src/test/groovy/org/onap/cps/notification/NotificationErrorHandlerSpec.groovy
+++ b/cps-service/src/test/groovy/org/onap/cps/notification/NotificationErrorHandlerSpec.groovy
@@ -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'
}
}
-
diff --git a/cps-service/src/test/groovy/org/onap/cps/notification/NotificationServiceSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/notification/NotificationServiceSpec.groovy
index 2ef468bb5..f07f89b39 100644
--- a/cps-service/src/test/groovy/org/onap/cps/notification/NotificationServiceSpec.groovy
+++ b/cps-service/src/test/groovy/org/onap/cps/notification/NotificationServiceSpec.groovy
@@ -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
+ }
}
diff --git a/cps-service/src/test/groovy/org/onap/cps/spi/FetchDescendantsOptionSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/spi/FetchDescendantsOptionSpec.groovy
index 24f3487d1..c1958472e 100644
--- a/cps-service/src/test/groovy/org/onap/cps/spi/FetchDescendantsOptionSpec.groovy
+++ b/cps-service/src/test/groovy/org/onap/cps/spi/FetchDescendantsOptionSpec.groovy
@@ -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
index 000000000..c8446902d
--- /dev/null
+++ b/cps-service/src/test/groovy/org/onap/cps/spi/model/ConditionPropertiesSpec.groovy
@@ -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"}]}'
+ }
+
+}
diff --git a/cps-service/src/test/groovy/org/onap/cps/spi/model/DataNodeBuilderSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/spi/model/DataNodeBuilderSpec.groovy
index 1559783e9..fcbae628e 100644
--- a/cps-service/src/test/groovy/org/onap/cps/spi/model/DataNodeBuilderSpec.groovy
+++ b/cps-service/src/test/groovy/org/onap/cps/spi/model/DataNodeBuilderSpec.groovy
@@ -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");
@@ -22,15 +22,19 @@
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.'() {
diff --git a/cps-service/src/test/groovy/org/onap/cps/utils/JsonObjectMapperSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/utils/JsonObjectMapperSpec.groovy
index 2332282e2..8cbd49355 100644
--- a/cps-service/src/test/groovy/org/onap/cps/utils/JsonObjectMapperSpec.groovy
+++ b/cps-service/src/test/groovy/org/onap/cps/utils/JsonObjectMapperSpec.groovy
@@ -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)
}
diff --git a/cps-service/src/test/groovy/org/onap/cps/utils/XmlFileUtilsSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/utils/XmlFileUtilsSpec.groovy
index b044e2e72..3864a5253 100644
--- a/cps-service/src/test/groovy/org/onap/cps/utils/XmlFileUtilsSpec.groovy
+++ b/cps-service/src/test/groovy/org/onap/cps/utils/XmlFileUtilsSpec.groovy
@@ -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>'
-
-
}
}
diff --git a/cps-service/src/test/groovy/org/onap/cps/utils/YangUtilsSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/utils/YangUtilsSpec.groovy
index 50b630643..e6344d303 100644
--- a/cps-service/src/test/groovy/org/onap/cps/utils/YangUtilsSpec.groovy
+++ b/cps-service/src/test/groovy/org/onap/cps/utils/YangUtilsSpec.groovy
@@ -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) == ''
+ }
}
diff --git a/cps-service/src/test/groovy/org/onap/cps/yang/YangTextSchemaSourceSetBuilderSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/yang/YangTextSchemaSourceSetBuilderSpec.groovy
index 3b4d57d3a..2739281bc 100644
--- a/cps-service/src/test/groovy/org/onap/cps/yang/YangTextSchemaSourceSetBuilderSpec.groovy
+++ b/cps-service/src/test/groovy/org/onap/cps/yang/YangTextSchemaSourceSetBuilderSpec.groovy
@@ -23,13 +23,13 @@
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()
+ }
}
diff --git a/docs/api/swagger/cps/openapi.yaml b/docs/api/swagger/cps/openapi.yaml
index ace45f844..eb6c4240c 100644
--- a/docs/api/swagger/cps/openapi.yaml
+++ b/docs/api/swagger/cps/openapi.yaml
@@ -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
index 000000000..8446834c3
--- /dev/null
+++ b/docs/cm-handle-lcm-events.rst
@@ -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
diff --git a/docs/cps-events.rst b/docs/cps-events.rst
index d487018e4..25a253bad 100644
--- a/docs/cps-events.rst
+++ b/docs/cps-events.rst
@@ -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:
@@ -8,153 +8,16 @@
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
index 000000000..99d802f76
--- /dev/null
+++ b/docs/cps-ncmp-message-status-codes.rst
@@ -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
index 000000000..51ec1254a
--- /dev/null
+++ b/docs/data-operation-events.rst
@@ -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 |
+ +----------------+-----------------+------------------------------------------------------------------------+
+
diff --git a/docs/modeling.rst b/docs/modeling.rst
index 6d31f83f4..ceaaefda5 100644
--- a/docs/modeling.rst
+++ b/docs/modeling.rst
@@ -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
index 000000000..49bf57085
--- /dev/null
+++ b/docs/ncmp-async-events.rst
@@ -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
index 000000000..617b3ed30
--- /dev/null
+++ b/docs/ncmp-data-operation.rst
@@ -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
diff --git a/docs/release-notes.rst b/docs/release-notes.rst
index 6b3546142..66dde1cfb 100755
--- a/docs/release-notes.rst
+++ b/docs/release-notes.rst
@@ -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
index 000000000..f82e48141
--- /dev/null
+++ b/docs/schemas/data-operation-event-schema-1.0.0.json
@@ -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
diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsDataServiceIntegrationSpec.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsDataServiceIntegrationSpec.groovy
index 7c257adda..678aa6446 100644
--- a/integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsDataServiceIntegrationSpec.groovy
+++ b/integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsDataServiceIntegrationSpec.groovy
@@ -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.'() {
diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/performance/base/CpsPerfTestBase.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/performance/base/CpsPerfTestBase.groovy
index 74070b1d8..8a3bd6d23 100644
--- a/integration-test/src/test/groovy/org/onap/cps/integration/performance/base/CpsPerfTestBase.groovy
+++ b/integration-test/src/test/groovy/org/onap/cps/integration/performance/base/CpsPerfTestBase.groovy
@@ -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)
diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/CpsDataServiceLimitsPerfTest.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/CpsDataServiceLimitsPerfTest.groovy
index e0df2fee7..9cb65ab8f 100644
--- a/integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/CpsDataServiceLimitsPerfTest.groovy
+++ b/integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/CpsDataServiceLimitsPerfTest.groovy
@@ -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: 'a 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()
+ }
+
}
diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/DeletePerfTest.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/DeletePerfTest.groovy
index db36b8809..e80a87d50 100644
--- a/integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/DeletePerfTest.groovy
+++ b/integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/DeletePerfTest.groovy
@@ -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)
diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/GetPerfTest.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/GetPerfTest.groovy
index eee87dd7c..a11dc3568 100644
--- a/integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/GetPerfTest.groovy
+++ b/integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/GetPerfTest.groovy
@@ -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
}
}
diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/QueryPerfTest.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/QueryPerfTest.groovy
index bad3f8afd..afcc2eae2 100644
--- a/integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/QueryPerfTest.groovy
+++ b/integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/QueryPerfTest.groovy
@@ -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
index 000000000..419ec6096
--- /dev/null
+++ b/integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/WritePerfTest.groovy
@@ -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 119b14b78..6e8f4acb2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -32,7 +32,7 @@
<groupId>org.onap.cps</groupId>
<artifactId>cps-aggregator</artifactId>
- <version>3.3.5-SNAPSHOT</version>
+ <version>3.3.6-SNAPSHOT</version>
<packaging>pom</packaging>
<name>cps</name>