diff options
88 files changed, 2170 insertions, 1226 deletions
diff --git a/cps-application/src/main/resources/application.yml b/cps-application/src/main/resources/application.yml index ed71339f93..47592b41f0 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 34970ac1c3..0000000000 --- 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 feff48c36a..0000000000 --- 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 d250c36a80..3b11249838 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 54d89ba00e..1d390f8d16 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 c178700eed..176e644bae 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 f511965c77..c80b07cb70 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 1fe963a279..0eda914f23 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 bf9ceb1c3d..35d94cc7a2 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 20df706c07..ddb9fd6fcf 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 44181c57c9..dc122ee5d1 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 8fdff17944..9ed686529d 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 cecde5f816..7803b982f3 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 d2b1237a4d..83a375b1b8 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 ce3b88ba03..63ab102d14 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 0000000000..d0be344f2e --- /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 f42a378fcb..c032d1e8a4 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 a7de479046..d0d70cf028 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 0000000000..17aba65cf7 --- /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 0000000000..b6cb039a9c --- /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 4dcc5797ca..866bfd4e7b 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 e332a2898a..7096c18abc 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 bff8222181..93af7f4bf4 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 d4ab1e88ad..7fa8155a2b 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 2af32c20e9..4193f75545 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 5355dd8b9a..7cc40cc90e 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 00412aa933..4c60281f85 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 bb0e7b73a0..c1c428b13f 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 7f1a628291..f5fbdfcb56 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 ec54e8917a..7116a17862 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 7474166ffe..e28a10261e 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 819f1fa08e..28db7babf9 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 61eb319101..4023441293 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 edbd7022f2..7442670920 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 3244f05a03..52ca1df62b 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 6bfa36bf79..2d83bdddcb 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 0000000000..35ff0241df --- /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 85e19aa883..900f663bfc 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 8ee01c089c..fd669b75c3 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 @@ -26,6 +26,7 @@ package org.onap.cps.rest.controller import org.onap.cps.spi.PaginationOption 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 @@ -98,17 +99,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.'() { @@ -147,6 +152,7 @@ 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 } def 'Query data node by cps path for the given dataspace across all anchors with pagination #scenario.'() { 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 19302d67aa..56fbe8cceb 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) { @@ -292,8 +298,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; @@ -368,9 +374,21 @@ 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 @@ -494,7 +512,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()); } } @@ -586,15 +604,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 11b2b07733..e38fc2f47e 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,
@@ -115,7 +126,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 cb554faee8..c72c3046e8 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 c97623f2a1..8bc39b1d48 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 38f8988279..696fd60f8c 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 02574995dc..3b90b06cb0 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 e212933388..b040af5bb4 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 09f2e16c6a..98c7947e1c 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 deb5b05752..ca907148dd 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 4e0349d2b8..eb41e2085f 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 ba438496fd..b4ac7a68f3 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 @@ -114,7 +118,7 @@ class CpsDataServiceImplSpec extends Specification { given: 'schema set for given anchor and dataspace references bookstore model' setupSchemaSetMocks('bookstore.yang') when: 'save data method is invoked with list element json data' - def jsonData = '{"multiple-data-tree:invoice": [{"ProductID": "2","ProductName": "Banana","price": "100","stock": True}]}' + def jsonData = '{"bookstore-address":[{"bookstore-name":"Easons","address":"Dublin,Ireland","postal-code":"D02HA21"}]}' objectUnderTest.saveListElements(dataspaceName, anchorName, '/', jsonData, observedTimestamp) then: 'the persistence service method is invoked with correct parameters' 1 * mockCpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName, @@ -122,7 +126,7 @@ class CpsDataServiceImplSpec extends Specification { { assert dataNodeCollection.size() == 1 assert dataNodeCollection.collect { it.getXpath() } - .containsAll(['/invoice[@ProductID=\'2\']']) + .containsAll(['/bookstore-address[@bookstore-name=\'Easons\']']) } } ) @@ -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 3884eda661..a794c58fc6 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 0000000000..8efd48547e --- /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 6a4e2a098f..b1880d50fb 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 5dbc2bb04b..49f4bf3850 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 d0cd47383f..89e305aedb 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 2ef468bb53..f07f89b391 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 24f3487d17..c1958472e9 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 0000000000..c8446902d5 --- /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 1559783e97..fcbae628e6 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 2332282e2b..8cbd493550 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 b044e2e727..3864a5253a 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 50b6306439..e6344d3035 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 3b4d57d3a6..2739281bc7 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/cps-service/src/test/resources/bookstore.json b/cps-service/src/test/resources/bookstore.json index 4b8ed3dab1..c5fd0fffdc 100644 --- a/cps-service/src/test/resources/bookstore.json +++ b/cps-service/src/test/resources/bookstore.json @@ -1,10 +1,9 @@ { - "multiple-data-tree:invoice": [ + "bookstore-address": [ { - "ProductID": "1", - "ProductName": "Apple", - "price": "100", - "stock": false + "bookstore-name": "Easons", + "address": "Dublin,Ireland", + "postal-code": "D02HA21" } ], "test:bookstore":{ diff --git a/cps-service/src/test/resources/bookstore.yang b/cps-service/src/test/resources/bookstore.yang index b7a52e2c8c..2033fc7031 100644 --- a/cps-service/src/test/resources/bookstore.yang +++ b/cps-service/src/test/resources/bookstore.yang @@ -15,31 +15,22 @@ module stores { } } - list invoice { - key "ProductID"; - leaf ProductID { - type uint64; - mandatory "true"; - description - "Unique product ID. Example: 001"; - } - leaf ProductName { + list bookstore-address { + key "bookstore-name"; + leaf bookstore-name { type string; - mandatory "true"; description - "Name of the Product"; + "Name of bookstore. Example: My Bookstore"; } - leaf price { - type uint64; - mandatory "true"; + leaf address { + type string; description - "Price of book"; + "Address of store"; } - leaf stock { - type boolean; - default "false"; + leaf postal-code { + type string; description - "Book in stock or not. Example value: true"; + "Postal code of store"; } } diff --git a/docs/api/swagger/cps/openapi.yaml b/docs/api/swagger/cps/openapi.yaml index 12b438a3e6..0e2191b675 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 @@ -2551,8 +2551,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 0000000000..8446834c31 --- /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 d487018e4f..25a253bada 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 0000000000..99d802f766 --- /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 0000000000..51ec1254af --- /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 6d31f83f4b..ceaaefda5a 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 0000000000..49bf57085b --- /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 0000000000..617b3ed309 --- /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 6b35461422..66dde1cfb7 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 0000000000..f82e481415 --- /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 ebaf9093cb..475d3d2fdb 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.'() { @@ -183,15 +209,15 @@ class CpsDataServiceIntegrationSpec extends FunctionalSpecBase { def 'Add and Delete top-level list (element) data nodes with root node.'() { given: 'a new (multiple-data-tree:invoice) datanodes' - def json = '{"multiple-data-tree:invoice": [{"ProductID": "2","ProductName": "Mango","price": "150","stock": true}]}' + def json = '{"bookstore-address":[{"bookstore-name":"Scholastic","address":"Bangalore,India","postal-code":"560043"}]}' when: 'the new list elements are saved' objectUnderTest.saveListElements(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1 , '/', json, now) then: 'they can be retrieved by their xpaths' - objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1 , '/invoice[@ProductID ="2"]', INCLUDE_ALL_DESCENDANTS) + objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1 , '/bookstore-address[@bookstore-name="Easons"]', INCLUDE_ALL_DESCENDANTS) and: 'there is one extra datanode' assert originalCountBookstoreTopLevelListNodes + 1 == countTopLevelListDataNodesInBookstore() when: 'the new elements are deleted' - objectUnderTest.deleteDataNode(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1 , '/invoice[@ProductID ="2"]', now) + objectUnderTest.deleteDataNode(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1 , '/bookstore-address[@bookstore-name="Easons"]', now) then: 'the original number of datanodes is restored' assert originalCountBookstoreTopLevelListNodes == countTopLevelListDataNodesInBookstore() } 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 74070b1d83..8a3bd6d23c 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/CpsAdminServiceLimits.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/CpsAdminServiceLimitsPerfTest.groovy index 0034af453b..9ea7a7b53a 100644 --- a/integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/CpsAdminServiceLimits.groovy +++ b/integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/CpsAdminServiceLimitsPerfTest.groovy @@ -23,7 +23,7 @@ package org.onap.cps.integration.performance.cps import org.onap.cps.api.CpsAdminService import org.onap.cps.integration.performance.base.CpsPerfTestBase -class CpsAdminServiceLimits extends CpsPerfTestBase { +class CpsAdminServiceLimitsPerfTest extends CpsPerfTestBase { CpsAdminService objectUnderTest diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/CpsDataServiceLimits.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/CpsDataServiceLimits.groovy deleted file mode 100644 index 1579470eab..0000000000 --- a/integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/CpsDataServiceLimits.groovy +++ /dev/null @@ -1,63 +0,0 @@ -/* - * ============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.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 - -class CpsDataServiceLimits extends CpsPerfTestBase { - - CpsDataService objectUnderTest - - 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 '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 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' - objectUnderTest.deleteDataNodes(CPS_PERFORMANCE_TEST_DATASPACE, anchorNames, OffsetDateTime.now()) - then: 'a database exception is not thrown' - noExceptionThrown() - } - -} 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 new file mode 100644 index 0000000000..9cb65ab8fd --- /dev/null +++ b/integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/CpsDataServiceLimitsPerfTest.groovy @@ -0,0 +1,99 @@ +/* + * ============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.api.CpsDataService +import org.onap.cps.integration.performance.base.CpsPerfTestBase + +import static org.onap.cps.spi.FetchDescendantsOption.DIRECT_CHILDREN_ONLY +import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS + +class CpsDataServiceLimitsPerfTest extends CpsPerfTestBase { + + CpsDataService objectUnderTest + + def setup() { objectUnderTest = cpsDataService } + + 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 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 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 db36b8809b..e80a87d509 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 eee87dd7c0..a11dc35682 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 bad3f8afd2..afcc2eae27 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 0000000000..419ec6096b --- /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/integration-test/src/test/resources/data/bookstore/bookstore.yang b/integration-test/src/test/resources/data/bookstore/bookstore.yang index ab384de1c4..6f60f1981c 100644 --- a/integration-test/src/test/resources/data/bookstore/bookstore.yang +++ b/integration-test/src/test/resources/data/bookstore/bookstore.yang @@ -15,31 +15,22 @@ module stores { } } - list invoice { - key "ProductID"; - leaf ProductID { - type uint64; - mandatory "true"; - description - "Unique product ID. Example: 001"; - } - leaf ProductName { + list bookstore-address { + key "bookstore-name"; + leaf bookstore-name { type string; - mandatory "true"; description - "Name of the Product"; + "Name of bookstore. Example: My Bookstore"; } - leaf price { - type uint64; - mandatory "true"; + leaf address { + type string; description - "Price of book"; + "Address of store"; } - leaf stock { - type boolean; - default "false"; + leaf postal-code { + type string; description - "Book in stock or not. Example value: true"; + "Postal code of store"; } } diff --git a/integration-test/src/test/resources/data/bookstore/bookstoreData.json b/integration-test/src/test/resources/data/bookstore/bookstoreData.json index 5f66a1d002..418acf8ef8 100644 --- a/integration-test/src/test/resources/data/bookstore/bookstoreData.json +++ b/integration-test/src/test/resources/data/bookstore/bookstoreData.json @@ -1,10 +1,9 @@ { - "multiple-data-tree:invoice": [ + "bookstore-address": [ { - "ProductID": "1", - "ProductName": "Apple", - "price": "100", - "stock": false + "bookstore-name": "Easons", + "address": "Dublin,Ireland", + "postal-code": "D02HA21" } ], "bookstore": { @@ -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>
diff --git a/test-tools/test-deregistration.sh b/test-tools/test-deregistration.sh index 6608b02bce..571644d7d5 100755 --- a/test-tools/test-deregistration.sh +++ b/test-tools/test-deregistration.sh @@ -77,8 +77,8 @@ remove_handles_and_record_time() { create_request_bodies() { local CREATE_SIZE=$1 local REMOVE_SIZE=$2 - echo -n '{"dmiPlugin": "http://ncmp-dmi-plugin-stub:8080","createdCmHandles":[' > $CREATE_REQUEST - echo -n '{"dmiPlugin": "http://ncmp-dmi-plugin-stub:8080","removedCmHandles":[' > $REMOVE_REQUEST + echo -n '{"dmiPlugin": "http://ncmp-dmi-plugin-demo-and-csit-stub:8092","createdCmHandles":[' > $CREATE_REQUEST + echo -n '{"dmiPlugin": "http://ncmp-dmi-plugin-demo-and-csit-stub:8092","removedCmHandles":[' > $REMOVE_REQUEST for i in $(seq 1 "$CREATE_SIZE"); do local CMHANDLE CMHANDLE=$(uuidgen | tr -d '-') |