diff options
33 files changed, 636 insertions, 316 deletions
diff --git a/cps-ncmp-events/src/main/resources/schemas/avc-event-schema-v1.json b/cps-ncmp-events/src/main/resources/schemas/avc-event-schema-v1.json index 6db03f6eb..c8109ca17 100644 --- a/cps-ncmp-events/src/main/resources/schemas/avc-event-schema-v1.json +++ b/cps-ncmp-events/src/main/resources/schemas/avc-event-schema-v1.json @@ -47,11 +47,14 @@ "eventType", "eventSchema", "eventSchemaVersion" - ] + ], + "additionalProperties": false }, "Event": { "description": "The AVC event content.", - "type": "object" + "type": "object", + "existingJavaType": "java.lang.Object", + "additionalProperties": false } } }
\ 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 index 5ab446cbb..feff48c36 100644 --- 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 @@ -29,6 +29,7 @@ "event": { "description": "The event content.", "type": "object", + "javaType": "InnerSubscriptionEvent", "properties": { "subscription": { "description": "The subscription details.", @@ -88,12 +89,26 @@ ], "predicates": { "description": "Additional values to be added into the subscription", - "existingJavaType" : "java.util.Map<String,Object>", - "type" : "object" - } + "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": [ + } + }, + "required": [ "subscription", "dataType" ] diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/event/lcm/LcmEventsPublisher.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/event/EventsPublisher.java index eda881767..60d39db7a 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/event/lcm/LcmEventsPublisher.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/event/EventsPublisher.java @@ -18,11 +18,10 @@ * ============LICENSE_END========================================================= */ -package org.onap.cps.ncmp.api.impl.event.lcm; +package org.onap.cps.ncmp.api.impl.event; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.onap.ncmp.cmhandle.event.lcm.LcmEvent; import org.springframework.kafka.core.KafkaTemplate; import org.springframework.kafka.support.SendResult; import org.springframework.stereotype.Service; @@ -30,36 +29,36 @@ import org.springframework.util.concurrent.ListenableFuture; import org.springframework.util.concurrent.ListenableFutureCallback; /** - * LcmEventsPublisher to publish the LcmEvents on event of CREATE, UPDATE and DELETE. + * EventsPublisher to publish events. */ @Slf4j @Service @RequiredArgsConstructor -public class LcmEventsPublisher { +public class EventsPublisher<T> { - private final KafkaTemplate<String, LcmEvent> lcmEventKafkaTemplate; + private final KafkaTemplate<String, T> eventKafkaTemplate; /** * LCM Event publisher. * * @param topicName valid topic name * @param eventKey message key - * @param lcmEvent message payload + * @param event message payload */ - public void publishEvent(final String topicName, final String eventKey, final LcmEvent lcmEvent) { - final ListenableFuture<SendResult<String, LcmEvent>> lcmEventFuture = - lcmEventKafkaTemplate.send(topicName, eventKey, lcmEvent); + public void publishEvent(final String topicName, final String eventKey, final T event) { + final ListenableFuture<SendResult<String, T>> eventFuture = + eventKafkaTemplate.send(topicName, eventKey, event); - lcmEventFuture.addCallback(new ListenableFutureCallback<>() { + eventFuture.addCallback(new ListenableFutureCallback<>() { @Override public void onFailure(final Throwable throwable) { log.error("Unable to publish event to topic : {} due to {}", topicName, throwable.getMessage()); } @Override - public void onSuccess(final SendResult<String, LcmEvent> sendResult) { - log.debug("Successfully published event to topic : {} , LcmEvent : {}", + public void onSuccess(final SendResult<String, T> sendResult) { + log.debug("Successfully published event to topic : {} , Event : {}", sendResult.getRecordMetadata().topic(), sendResult.getProducerRecord().value()); } }); diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/event/avc/SubscriptionEventConsumer.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/event/avc/SubscriptionEventConsumer.java index 92949cbb7..d08baac5d 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/event/avc/SubscriptionEventConsumer.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/event/avc/SubscriptionEventConsumer.java @@ -22,7 +22,10 @@ package org.onap.cps.ncmp.api.impl.event.avc; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.onap.cps.ncmp.event.model.InnerSubscriptionEvent; import org.onap.cps.ncmp.event.model.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; @@ -32,6 +35,11 @@ import org.springframework.stereotype.Component; @RequiredArgsConstructor public class SubscriptionEventConsumer { + private final SubscriptionEventForwarder subscriptionEventForwarder; + + @Value("${notification.enabled:true}") + private boolean notificationFeatureEnabled; + /** * Consume the specified event. * @@ -40,12 +48,21 @@ public class SubscriptionEventConsumer { @KafkaListener(topics = "${app.ncmp.avc.subscription-topic}", properties = {"spring.json.value.default.type=org.onap.cps.ncmp.event.model.SubscriptionEvent"}) public void consumeSubscriptionEvent(final SubscriptionEvent subscriptionEvent) { - if ("CM".equals(subscriptionEvent.getEvent().getDataType().getDataCategory())) { + final InnerSubscriptionEvent event = subscriptionEvent.getEvent(); + final String eventDatastore = event.getPredicates().getDatastore(); + if (!(eventDatastore.equals("passthrough-running") || eventDatastore.equals("passthrough-operational"))) { + throw new OperationNotYetSupportedException( + "passthrough datastores are currently only supported for event subscriptions"); + } + if ("CM".equals(event.getDataType().getDataCategory())) { log.debug("Consuming event {} ...", subscriptionEvent); if ("CREATE".equals(subscriptionEvent.getEventType().value())) { - log.info("Subscription for ClientID {} with name{} ...", - subscriptionEvent.getEvent().getSubscription().getClientID(), - subscriptionEvent.getEvent().getSubscription().getName()); + log.info("Subscription for ClientID {} with name {} ...", + event.getSubscription().getClientID(), + event.getSubscription().getName()); + if (notificationFeatureEnabled) { + subscriptionEventForwarder.forwardCreateSubscriptionEvent(subscriptionEvent); + } } } else { log.trace("Non-CM subscription event ignored"); diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/event/avc/SubscriptionEventForwarder.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/event/avc/SubscriptionEventForwarder.java new file mode 100644 index 000000000..635059bfe --- /dev/null +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/event/avc/SubscriptionEventForwarder.java @@ -0,0 +1,106 @@ +/* + * ============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.event.avc; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.onap.cps.ncmp.api.impl.event.EventsPublisher; +import org.onap.cps.ncmp.api.impl.operations.RequiredDmiService; +import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle; +import org.onap.cps.ncmp.api.inventory.InventoryPersistence; +import org.onap.cps.ncmp.event.model.SubscriptionEvent; +import org.onap.cps.spi.exceptions.OperationNotYetSupportedException; +import org.springframework.stereotype.Component; + + +@Component +@Slf4j +@RequiredArgsConstructor +public class SubscriptionEventForwarder { + + private final InventoryPersistence inventoryPersistence; + private final EventsPublisher<SubscriptionEvent> eventsPublisher; + + private static final String DMI_AVC_SUBSCRIPTION_TOPIC_PREFIX = "ncmp-dmi-cm-avc-subscription-"; + + /** + * Forward subscription event. + * + * @param subscriptionEvent the event to be forwarded + */ + public void forwardCreateSubscriptionEvent(final SubscriptionEvent subscriptionEvent) { + final List<Object> cmHandleTargets = subscriptionEvent.getEvent().getPredicates().getTargets(); + if (cmHandleTargets == null || cmHandleTargets.isEmpty() + || cmHandleTargets.stream().anyMatch(id -> ((String) id).contains("*"))) { + throw new OperationNotYetSupportedException( + "CMHandle targets are required. \"Wildcard\" operations are not yet supported"); + } + final List<String> cmHandleTargetsAsStrings = cmHandleTargets.stream().map( + Objects::toString).collect(Collectors.toList()); + final Collection<YangModelCmHandle> yangModelCmHandles = + inventoryPersistence.getYangModelCmHandles(cmHandleTargetsAsStrings); + final Map<String, Map<String, Map<String, String>>> dmiNameCmHandleMap = + organizeByDmiName(yangModelCmHandles); + dmiNameCmHandleMap.forEach((dmiName, cmHandlePropertiesMap) -> { + subscriptionEvent.getEvent().getPredicates().setTargets(Collections.singletonList(cmHandlePropertiesMap)); + final String eventKey = createEventKey(subscriptionEvent, dmiName); + eventsPublisher.publishEvent(DMI_AVC_SUBSCRIPTION_TOPIC_PREFIX + dmiName, eventKey, subscriptionEvent); + }); + } + + private Map<String, Map<String, Map<String, String>>> organizeByDmiName( + final Collection<YangModelCmHandle> yangModelCmHandles) { + final Map<String, Map<String, Map<String, String>>> dmiNameCmHandlePropertiesMap = new HashMap<>(); + yangModelCmHandles.forEach(cmHandle -> { + final String dmiName = cmHandle.resolveDmiServiceName(RequiredDmiService.DATA); + if (!dmiNameCmHandlePropertiesMap.containsKey(dmiName)) { + final Map<String, Map<String, String>> cmHandleDmiPropertiesMap = new HashMap<>(); + cmHandleDmiPropertiesMap.put(cmHandle.getId(), dmiPropertiesAsMap(cmHandle)); + dmiNameCmHandlePropertiesMap.put(cmHandle.getDmiDataServiceName(), cmHandleDmiPropertiesMap); + } else { + dmiNameCmHandlePropertiesMap.get(cmHandle.getDmiDataServiceName()) + .put(cmHandle.getId(), dmiPropertiesAsMap(cmHandle)); + } + }); + return dmiNameCmHandlePropertiesMap; + } + + private String createEventKey(final SubscriptionEvent subscriptionEvent, final String dmiName) { + return subscriptionEvent.getEvent().getSubscription().getClientID() + + "-" + + subscriptionEvent.getEvent().getSubscription().getName() + + "-" + + dmiName; + } + + public Map<String, String> dmiPropertiesAsMap(final YangModelCmHandle yangModelCmHandle) { + return yangModelCmHandle.getDmiProperties().stream().collect( + Collectors.toMap(YangModelCmHandle.Property::getName, YangModelCmHandle.Property::getValue)); + } + +} diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/event/lcm/LcmEventsService.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/event/lcm/LcmEventsService.java index a94d664de..2eba83053 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/event/lcm/LcmEventsService.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/event/lcm/LcmEventsService.java @@ -23,6 +23,7 @@ package org.onap.cps.ncmp.api.impl.event.lcm; import io.micrometer.core.annotation.Timed; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.onap.cps.ncmp.api.impl.event.EventsPublisher; import org.onap.ncmp.cmhandle.event.lcm.LcmEvent; import org.springframework.beans.factory.annotation.Value; import org.springframework.kafka.KafkaException; @@ -37,7 +38,7 @@ import org.springframework.stereotype.Service; @RequiredArgsConstructor public class LcmEventsService { - private final LcmEventsPublisher lcmEventsPublisher; + private final EventsPublisher<LcmEvent> eventsPublisher; @Value("${app.lcm.events.topic:ncmp-events}") private String topicName; @@ -56,7 +57,7 @@ public class LcmEventsService { public void publishLcmEvent(final String cmHandleId, final LcmEvent lcmEvent) { if (notificationsEnabled) { try { - lcmEventsPublisher.publishEvent(topicName, cmHandleId, lcmEvent); + eventsPublisher.publishEvent(topicName, cmHandleId, lcmEvent); } catch (final KafkaException e) { log.error("Unable to publish message to topic : {} and cause : {}", topicName, e.getMessage()); } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/init/SubscriptionModelLoader.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/init/SubscriptionModelLoader.java index 0d82bb5c1..705c9d266 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/init/SubscriptionModelLoader.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/init/SubscriptionModelLoader.java @@ -22,11 +22,13 @@ package org.onap.cps.ncmp.init; import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.time.OffsetDateTime; import java.util.Map; import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.onap.cps.api.CpsAdminService; +import org.onap.cps.api.CpsDataService; import org.onap.cps.api.CpsModuleService; import org.onap.cps.ncmp.api.impl.exception.NcmpStartUpException; import org.onap.cps.spi.exceptions.AlreadyDefinedException; @@ -42,9 +44,11 @@ public class SubscriptionModelLoader implements ModelLoader { private final CpsAdminService cpsAdminService; private final CpsModuleService cpsModuleService; + private final CpsDataService cpsDataService; private static final String SUBSCRIPTION_DATASPACE_NAME = "NCMP-Admin"; private static final String SUBSCRIPTION_ANCHOR_NAME = "AVC-Subscriptions"; private static final String SUBSCRIPTION_SCHEMASET_NAME = "subscriptions"; + private static final String SUBSCRIPTION_REGISTRY_DATANODE_NAME = "subscription-registry"; @Value("${ncmp.model-loader.subscription:false}") private boolean subscriptionModelLoaderEnabled; @@ -76,6 +80,8 @@ public class SubscriptionModelLoader implements ModelLoader { if (!yangResourceContentMap.get("subscription.yang").isEmpty()) { createSchemaSet(SUBSCRIPTION_DATASPACE_NAME, SUBSCRIPTION_SCHEMASET_NAME, yangResourceContentMap); createAnchor(SUBSCRIPTION_DATASPACE_NAME, SUBSCRIPTION_SCHEMASET_NAME, SUBSCRIPTION_ANCHOR_NAME); + createTopLevelDataNode(SUBSCRIPTION_DATASPACE_NAME, SUBSCRIPTION_ANCHOR_NAME, + SUBSCRIPTION_REGISTRY_DATANODE_NAME); } } @@ -116,6 +122,20 @@ public class SubscriptionModelLoader implements ModelLoader { return true; } + private void createTopLevelDataNode(final String dataspaceName, + final String anchorName, + final String dataNodeName) { + final String nodeData = "{\"" + dataNodeName + "\":{}}"; + try { + cpsDataService.saveData(dataspaceName, anchorName, nodeData, OffsetDateTime.now()); + } catch (final AlreadyDefinedException exception) { + log.info("Creating new data node {} failed as data node already exists", dataNodeName); + } catch (final Exception exception) { + log.debug("Creating data node for subscription model failed: {}", exception.getMessage()); + throw new NcmpStartUpException("Creating data node failed", exception.getMessage()); + } + } + private String getFileContentAsString() { try (InputStream inputStream = getClass().getClassLoader() .getResourceAsStream("model/subscription.yang")) { diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/event/avc/SubscriptionEventConsumerSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/event/avc/SubscriptionEventConsumerSpec.groovy index 20d60e396..7a9dadebc 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/event/avc/SubscriptionEventConsumerSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/event/avc/SubscriptionEventConsumerSpec.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. @@ -24,29 +24,68 @@ import com.fasterxml.jackson.databind.ObjectMapper import org.onap.cps.ncmp.api.kafka.MessagingBaseSpec import org.onap.cps.ncmp.event.model.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 -@SpringBootTest(classes = [SubscriptionEventConsumer, ObjectMapper, JsonObjectMapper]) +@SpringBootTest(classes = [ObjectMapper, JsonObjectMapper]) class SubscriptionEventConsumerSpec extends MessagingBaseSpec { - def objectUnderTest = new SubscriptionEventConsumer() + def subscriptionEventForwarder = Mock(SubscriptionEventForwarder) + def objectUnderTest = new SubscriptionEventConsumer(subscriptionEventForwarder) @Autowired JsonObjectMapper jsonObjectMapper - def 'Consume valid message'() { + def 'Consume and forward valid CM create message'() { + given: 'an event with data category CM' + def jsonData = TestUtils.getResourceFileContent('avcSubscriptionCreationEvent.json') + def testEventSent = jsonObjectMapper.convertJsonString(jsonData, SubscriptionEvent.class) + and: 'notifications are enabled' + objectUnderTest.notificationFeatureEnabled = true + when: 'the valid event is consumed' + objectUnderTest.consumeSubscriptionEvent(testEventSent) + then: 'the event is forwarded' + 1 * subscriptionEventForwarder.forwardCreateSubscriptionEvent(testEventSent) + } + + def 'Consume valid CM create message where notifications are disabled'() { + given: 'an event with data category CM' + def jsonData = TestUtils.getResourceFileContent('avcSubscriptionCreationEvent.json') + def testEventSent = jsonObjectMapper.convertJsonString(jsonData, SubscriptionEvent.class) + and: 'notifications are disabled' + objectUnderTest.notificationFeatureEnabled = false + when: 'the valid event is consumed' + objectUnderTest.consumeSubscriptionEvent(testEventSent) + then: 'the event is forwarded' + 0 * subscriptionEventForwarder.forwardCreateSubscriptionEvent(testEventSent) + } + + def 'Consume valid FM message'() { given: 'an event' def jsonData = TestUtils.getResourceFileContent('avcSubscriptionCreationEvent.json') def testEventSent = jsonObjectMapper.convertJsonString(jsonData, SubscriptionEvent.class) - and: 'dataCategory is set' - testEventSent.getEvent().getDataType().setDataCategory(dataCategory) + and: 'dataCategory is set to FM' + testEventSent.getEvent().getDataType().setDataCategory("FM") when: 'the valid event is consumed' objectUnderTest.consumeSubscriptionEvent(testEventSent) then: 'no exception is thrown' noExceptionThrown() - where: 'data category is changed' - dataCategory << [ 'CM' , 'FM' ] + and: 'No event is forwarded' + 0 * subscriptionEventForwarder.forwardCreateSubscriptionEvent(*_) + } + + 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' + testEventSent.getEvent().getPredicates().setDatastore("operational") + when: 'the valid event is consumed' + objectUnderTest.consumeSubscriptionEvent(testEventSent) + then: 'an operation not yet supported exception is thrown' + thrown(OperationNotYetSupportedException) } + } diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/event/avc/SubscriptionEventForwarderSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/event/avc/SubscriptionEventForwarderSpec.groovy new file mode 100644 index 000000000..f9e801ddb --- /dev/null +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/event/avc/SubscriptionEventForwarderSpec.groovy @@ -0,0 +1,96 @@ +/* + * ============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.event.avc + +import com.fasterxml.jackson.databind.ObjectMapper +import org.onap.cps.ncmp.api.impl.event.EventsPublisher +import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle +import org.onap.cps.ncmp.api.inventory.InventoryPersistence +import org.onap.cps.ncmp.api.kafka.MessagingBaseSpec +import org.onap.cps.ncmp.event.model.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 +import org.springframework.kafka.core.KafkaTemplate +import org.springframework.util.concurrent.ListenableFuture; + +@SpringBootTest(classes = [ObjectMapper, JsonObjectMapper]) +class SubscriptionEventForwarderSpec extends MessagingBaseSpec { + + def mockInventoryPersistence = Mock(InventoryPersistence) + def mockSubscriptionEventPublisher = Mock(EventsPublisher<SubscriptionEvent>) + def objectUnderTest = new SubscriptionEventForwarder(mockInventoryPersistence, mockSubscriptionEventPublisher) + + @Autowired + JsonObjectMapper jsonObjectMapper + + def 'Forward valid CM create subscription'() { + given: 'an event' + def jsonData = TestUtils.getResourceFileContent('avcSubscriptionCreationEvent.json') + def testEventSent = jsonObjectMapper.convertJsonString(jsonData, SubscriptionEvent.class) + and: 'the InventoryPersistence returns private properties for the supplied CM Handles' + 1 * mockInventoryPersistence.getYangModelCmHandles(["CMHandle1", "CMHandle2", "CMHandle3"]) >> [ + createYangModelCmHandleWithDmiProperty(1, 1,"shape","circle"), + createYangModelCmHandleWithDmiProperty(2, 1,"shape","square"), + createYangModelCmHandleWithDmiProperty(3, 2,"shape","triangle") + ] + when: 'the valid event is forwarded' + objectUnderTest.forwardCreateSubscriptionEvent(testEventSent) + then: 'the event is forwarded twice with the CMHandle private properties and provides a valid listenable future' + 1 * mockSubscriptionEventPublisher.publishEvent("ncmp-dmi-cm-avc-subscription-DMIName1", "SCO-9989752-cm-subscription-001-DMIName1", + subscriptionEvent -> { + Map targets = subscriptionEvent.getEvent().getPredicates().getTargets().get(0) + targets["CMHandle1"] == ["shape":"circle"] + targets["CMHandle2"] == ["shape":"square"] + } + ) + 1 * mockSubscriptionEventPublisher.publishEvent("ncmp-dmi-cm-avc-subscription-DMIName2", "SCO-9989752-cm-subscription-001-DMIName2", + subscriptionEvent -> { + Map targets = subscriptionEvent.getEvent().getPredicates().getTargets().get(0) + targets["CMHandle3"] == ["shape":"triangle"] + } + ) + } + + def 'Forward CM create subscription where target CM Handles are #scenario'() { + given: 'an event' + def jsonData = TestUtils.getResourceFileContent('avcSubscriptionCreationEvent.json') + def testEventSent = jsonObjectMapper.convertJsonString(jsonData, SubscriptionEvent.class) + and: 'the target CMHandles are set to #scenario' + testEventSent.getEvent().getPredicates().setTargets(invalidTargets) + when: 'the event is forwarded' + objectUnderTest.forwardCreateSubscriptionEvent(testEventSent) + then: 'an operation not yet supported exception is thrown' + thrown(OperationNotYetSupportedException) + where: + scenario | invalidTargets + 'null' | null + 'empty' | [] + 'wildcard' | ['CMHandle*'] + } + + static def createYangModelCmHandleWithDmiProperty(id, dmiId,propertyName, propertyValue) { + return new YangModelCmHandle(id:"CMHandle" + id, dmiDataServiceName: "DMIName" + dmiId, dmiProperties: [new YangModelCmHandle.Property(propertyName,propertyValue)]) + } + +} diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/event/lcm/LcmEventsPublisherSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/event/lcm/LcmEventsPublisherSpec.groovy index 61bf33d19..f5b58a753 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/event/lcm/LcmEventsPublisherSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/event/lcm/LcmEventsPublisherSpec.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. @@ -22,6 +22,7 @@ package org.onap.cps.ncmp.api.impl.event.lcm import com.fasterxml.jackson.databind.ObjectMapper import org.apache.kafka.clients.consumer.KafkaConsumer +import org.onap.cps.ncmp.api.impl.event.EventsPublisher import org.onap.cps.ncmp.api.kafka.MessagingBaseSpec import org.onap.cps.ncmp.utils.TestUtils import org.onap.cps.utils.JsonObjectMapper @@ -35,7 +36,7 @@ import org.testcontainers.spock.Testcontainers import java.time.Duration -@SpringBootTest(classes = [LcmEventsPublisher, ObjectMapper, JsonObjectMapper]) +@SpringBootTest(classes = [EventsPublisher, ObjectMapper, JsonObjectMapper]) @Testcontainers @DirtiesContext class LcmEventsPublisherSpec extends MessagingBaseSpec { @@ -45,7 +46,7 @@ class LcmEventsPublisherSpec extends MessagingBaseSpec { def testTopic = 'ncmp-events-test' @SpringBean - LcmEventsPublisher lcmEventsPublisher = new LcmEventsPublisher(kafkaTemplate) + EventsPublisher<LcmEvent> lcmEventsPublisher = new EventsPublisher(kafkaTemplate) @Autowired JsonObjectMapper jsonObjectMapper diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/event/lcm/LcmEventsServiceSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/event/lcm/LcmEventsServiceSpec.groovy index ef399e1c6..4c632ddf0 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/event/lcm/LcmEventsServiceSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/event/lcm/LcmEventsServiceSpec.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. @@ -20,13 +20,14 @@ package org.onap.cps.ncmp.api.impl.event.lcm +import org.onap.cps.ncmp.api.impl.event.EventsPublisher import org.onap.ncmp.cmhandle.event.lcm.LcmEvent import org.springframework.kafka.KafkaException import spock.lang.Specification class LcmEventsServiceSpec extends Specification { - def mockLcmEventsPublisher = Mock(LcmEventsPublisher) + def mockLcmEventsPublisher = Mock(EventsPublisher) def objectUnderTest = new LcmEventsService(mockLcmEventsPublisher) diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/kafka/MessagingBaseSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/kafka/MessagingBaseSpec.groovy index bb0ce8745..337178e12 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/kafka/MessagingBaseSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/kafka/MessagingBaseSpec.groovy @@ -22,6 +22,7 @@ package org.onap.cps.ncmp.api.kafka import org.apache.kafka.common.serialization.StringDeserializer import org.apache.kafka.common.serialization.StringSerializer +import org.spockframework.spring.SpringBean import org.springframework.kafka.core.DefaultKafkaProducerFactory import org.springframework.kafka.core.KafkaTemplate import org.springframework.kafka.support.serializer.JsonSerializer @@ -62,7 +63,8 @@ class MessagingBaseSpec extends Specification { ] } - def kafkaTemplate = new KafkaTemplate<>(new DefaultKafkaProducerFactory<Integer, String>(producerConfigProperties())) + @SpringBean + KafkaTemplate kafkaTemplate = new KafkaTemplate<>(new DefaultKafkaProducerFactory<Integer, String>(producerConfigProperties())) @DynamicPropertySource static void registerKafkaProperties(DynamicPropertyRegistry dynamicPropertyRegistry) { diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/init/SubscriptionModelLoaderSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/init/SubscriptionModelLoaderSpec.groovy index 0e647ad87..907208228 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/init/SubscriptionModelLoaderSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/init/SubscriptionModelLoaderSpec.groovy @@ -26,9 +26,11 @@ import ch.qos.logback.core.read.ListAppender import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.onap.cps.api.CpsAdminService +import org.onap.cps.api.CpsDataService import org.onap.cps.api.CpsModuleService import org.onap.cps.ncmp.api.impl.exception.NcmpStartUpException import org.onap.cps.spi.exceptions.AlreadyDefinedException +import org.onap.cps.spi.exceptions.DataValidationException import org.onap.cps.spi.exceptions.SchemaSetNotFoundException import org.springframework.boot.SpringApplication import org.slf4j.LoggerFactory @@ -39,11 +41,13 @@ class SubscriptionModelLoaderSpec extends Specification { def mockCpsAdminService = Mock(CpsAdminService) def mockCpsModuleService = Mock(CpsModuleService) - def objectUnderTest = new SubscriptionModelLoader(mockCpsAdminService, mockCpsModuleService) + def mockCpsDataService = Mock(CpsDataService) + def objectUnderTest = new SubscriptionModelLoader(mockCpsAdminService, mockCpsModuleService, mockCpsDataService) def SUBSCRIPTION_DATASPACE_NAME = objectUnderTest.SUBSCRIPTION_DATASPACE_NAME; def SUBSCRIPTION_ANCHOR_NAME = objectUnderTest.SUBSCRIPTION_ANCHOR_NAME; def SUBSCRIPTION_SCHEMASET_NAME = objectUnderTest.SUBSCRIPTION_SCHEMASET_NAME; + def SUBSCRIPTION_REGISTRY_DATANODE_NAME = objectUnderTest.SUBSCRIPTION_REGISTRY_DATANODE_NAME; def sampleYangContentMap = ['subscription.yang':'module subscription { *sample content* }'] @@ -75,6 +79,8 @@ class SubscriptionModelLoaderSpec extends Specification { 1 * mockCpsModuleService.createSchemaSet(SUBSCRIPTION_DATASPACE_NAME, SUBSCRIPTION_SCHEMASET_NAME,sampleYangContentMap) and: 'the admin service to create an anchor set is called once' 1 * mockCpsAdminService.createAnchor(SUBSCRIPTION_DATASPACE_NAME, SUBSCRIPTION_SCHEMASET_NAME, SUBSCRIPTION_ANCHOR_NAME) + and: 'the data service to create a top level datanode is called once' + 1 * mockCpsDataService.saveData(SUBSCRIPTION_DATASPACE_NAME, SUBSCRIPTION_ANCHOR_NAME, '{"' + SUBSCRIPTION_REGISTRY_DATANODE_NAME + '":{}}', _) } def 'Create schema set from model file'() { @@ -138,6 +144,29 @@ class SubscriptionModelLoaderSpec extends Specification { thrown(NcmpStartUpException) } + def 'Create top level node fails due to an AlreadyDefined exception'() { + given: 'the saving of the node data will throw an Already Defined exception' + mockCpsDataService.saveData(*_) >> + { AlreadyDefinedException.forDataNode('/xpath', "sampleContextName", null) } + when: 'the method to onboard model is called' + objectUnderTest.onboardSubscriptionModel() + then: 'no exception thrown' + noExceptionThrown() + } + + def 'Create top level node fails due to any other exception'() { + given: 'the saving of the node data will throw an exception' + mockCpsDataService.saveData(*_) >> + { throw new DataValidationException("Invalid JSON", "JSON Data is invalid") } + when: 'the method to onboard model is called' + objectUnderTest.onboardSubscriptionModel() + then: 'the log message contains the correct exception message' + def debugMessage = appender.list[0].toString() + assert debugMessage.contains("Creating data node for subscription model failed: Invalid JSON") + and: 'exception is thrown' + thrown(NcmpStartUpException) + } + def 'Get file content as string'() { when: 'the method to get yang content is called' def response = objectUnderTest.getFileContentAsString() diff --git a/cps-ncmp-service/src/test/resources/avcSubscriptionCreationEvent.json b/cps-ncmp-service/src/test/resources/avcSubscriptionCreationEvent.json index 1d84c3a5f..63fca1f41 100644 --- a/cps-ncmp-service/src/test/resources/avcSubscriptionCreationEvent.json +++ b/cps-ncmp-service/src/test/resources/avcSubscriptionCreationEvent.json @@ -14,10 +14,10 @@ "schemaVersion": "1.0" }, "predicates": { - "datastore": "passthrough-operational", - "datastore-xpath-filter": "//_3gpp-nr-nrm-gnbdufunction:GNBDUFunction/ ", - "_3gpp-nr-nrm-nrcelldu": "NRCellDU" - - } + "targets" : ["CMHandle1", "CMHandle2", "CMHandle3"], + "datastore": "passthrough-running", + "xpath-filter": "//_3gpp-nr-nrm-gnbdufunction:GNBDUFunction/_3gpp-nr-nrm-nrcelldu:NRCellDU/ | //_3gpp-nr-nrm-gnbcuupfunction:GNBCUUPFunction// | //_3gpp-nr-nrm-gnbcucpfunction:GNBCUCPFunction/_3gpp-nr-nrm-nrcelldu:NRCellCU// | //_3gpp-nr-nrm-nrsectorcarrier:NRSectorCarrier//" } + +} }
\ No newline at end of file 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 3ea388242..475699113 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 @@ -256,12 +256,14 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService final Collection<String> xpaths, final FetchDescendantsOption fetchDescendantsOption) { final AnchorEntity anchorEntity = getAnchorEntity(dataspaceName, anchorName); - final Collection<FragmentEntity> fragmentEntities = getFragmentEntities(anchorEntity, xpaths); + final Collection<FragmentEntity> fragmentEntities = + getFragmentEntities(anchorEntity, xpaths, fetchDescendantsOption); return toDataNodes(fragmentEntities, fetchDescendantsOption); } private Collection<FragmentEntity> getFragmentEntities(final AnchorEntity anchorEntity, - final Collection<String> xpaths) { + final Collection<String> xpaths, + final FetchDescendantsOption fetchDescendantsOption) { final Collection<String> nonRootXpaths = new HashSet<>(xpaths); final boolean haveRootXpath = nonRootXpaths.removeIf(CpsDataPersistenceServiceImpl::isRootXpath); @@ -273,15 +275,15 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService log.warn("Error parsing xpath \"{}\": {}", xpath, e.getMessage()); } } - final Collection<FragmentEntity> fragmentEntities = - new HashSet<>(fragmentRepository.findByAnchorAndMultipleCpsPaths(anchorEntity.getId(), normalizedXpaths)); - if (haveRootXpath) { - final List<FragmentExtract> fragmentExtracts = fragmentRepository.findAllExtractsByAnchor(anchorEntity); - fragmentEntities.addAll(FragmentEntityArranger.toFragmentEntityTrees(anchorEntity, fragmentExtracts)); + normalizedXpaths.addAll(fragmentRepository.findAllXpathByAnchorAndParentIdIsNull(anchorEntity)); } - return fragmentEntities; + final List<FragmentExtract> fragmentExtracts = + fragmentRepository.findExtractsWithDescendants(anchorEntity.getId(), normalizedXpaths, + fetchDescendantsOption.getDepth()); + + return FragmentEntityArranger.toFragmentEntityTrees(anchorEntity, fragmentExtracts); } private FragmentEntity getFragmentEntity(final AnchorEntity anchorEntity, final String xpath) { @@ -325,7 +327,8 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService } fragmentEntities = fragmentRepository.findByAnchorAndCpsPath(anchorEntity.getId(), cpsPathQuery); if (cpsPathQuery.hasAncestorAxis()) { - fragmentEntities = getAncestorFragmentEntities(anchorEntity.getId(), cpsPathQuery, fragmentEntities); + final Collection<String> ancestorXpaths = processAncestorXpath(fragmentEntities, cpsPathQuery); + fragmentEntities = getFragmentEntities(anchorEntity, ancestorXpaths, fetchDescendantsOption); } return createDataNodesFromProxiedFragmentEntities(fetchDescendantsOption, anchorEntity, fragmentEntities); } @@ -346,19 +349,12 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService fragmentRepository.quickFindWithDescendants(anchorEntity.getId(), xpathRegex); fragmentEntities = FragmentEntityArranger.toFragmentEntityTrees(anchorEntity, fragmentExtracts); if (cpsPathQuery.hasAncestorAxis()) { - fragmentEntities = getAncestorFragmentEntities(anchorEntity.getId(), cpsPathQuery, fragmentEntities); + final Collection<String> ancestorXpaths = processAncestorXpath(fragmentEntities, cpsPathQuery); + fragmentEntities = getFragmentEntities(anchorEntity, ancestorXpaths, fetchDescendantsOption); } return createDataNodesFromFragmentEntities(fetchDescendantsOption, fragmentEntities); } - private Collection<FragmentEntity> getAncestorFragmentEntities(final int anchorId, - final CpsPathQuery cpsPathQuery, - final Collection<FragmentEntity> fragmentEntities) { - final Collection<String> ancestorXpaths = processAncestorXpath(fragmentEntities, cpsPathQuery); - return ancestorXpaths.isEmpty() ? Collections.emptyList() - : fragmentRepository.findByAnchorAndMultipleCpsPaths(anchorId, ancestorXpaths); - } - private List<DataNode> createDataNodesFromProxiedFragmentEntities( final FetchDescendantsOption fetchDescendantsOption, final AnchorEntity anchorEntity, @@ -490,14 +486,15 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService @Override public void updateDataNodesAndDescendants(final String dataspaceName, final String anchorName, - final List<DataNode> updatedDataNodes) { + final Collection<DataNode> updatedDataNodes) { final AnchorEntity anchorEntity = getAnchorEntity(dataspaceName, anchorName); final Map<String, DataNode> xpathToUpdatedDataNode = updatedDataNodes.stream() .collect(Collectors.toMap(DataNode::getXpath, dataNode -> dataNode)); final Collection<String> xpaths = xpathToUpdatedDataNode.keySet(); - final Collection<FragmentEntity> existingFragmentEntities = getFragmentEntities(anchorEntity, xpaths); + final Collection<FragmentEntity> existingFragmentEntities = + getFragmentEntities(anchorEntity, xpaths, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS); for (final FragmentEntity existingFragmentEntity : existingFragmentEntities) { final DataNode updatedDataNode = xpathToUpdatedDataNode.get(existingFragmentEntity.getXpath()); 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 996e55e77..426a4601c 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 @@ -37,7 +37,7 @@ import org.springframework.stereotype.Repository; @Repository
public interface FragmentRepository extends JpaRepository<FragmentEntity, Long>, FragmentRepositoryCpsPathQuery,
- FragmentRepositoryMultiPathQuery, FragmentNativeRepository {
+ FragmentNativeRepository {
Optional<FragmentEntity> findByAnchorAndXpath(AnchorEntity anchorEntity, String xpath);
@@ -68,9 +68,30 @@ public interface FragmentRepository extends JpaRepository<FragmentEntity, Long>, List<FragmentExtract> quickFindWithDescendants(@Param("anchorId") int anchorId,
@Param("xpathRegex") String xpathRegex);
- @Query("SELECT xpath FROM FragmentEntity f WHERE anchor = :anchor AND xpath IN :xpaths")
+ @Query("SELECT xpath FROM FragmentEntity WHERE anchor = :anchor AND xpath IN :xpaths")
List<String> findAllXpathByAnchorAndXpathIn(@Param("anchor") AnchorEntity anchorEntity,
@Param("xpaths") Collection<String> xpaths);
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
+ = "WITH RECURSIVE parent_search AS ("
+ + " SELECT id, 0 AS depth "
+ + " FROM fragment "
+ + " WHERE anchor_id = :anchorId AND xpath IN :xpaths "
+ + " UNION "
+ + " SELECT c.id, depth + 1 "
+ + " FROM fragment c INNER JOIN parent_search p ON c.parent_id = p.id"
+ + " WHERE depth <= (SELECT CASE WHEN :maxDepth = -1 THEN " + Integer.MAX_VALUE + " ELSE :maxDepth END) "
+ + ") "
+ + "SELECT f.id, anchor_id AS anchorId, xpath, f.parent_id AS parentId, CAST(attributes AS TEXT) AS attributes "
+ + "FROM fragment f INNER JOIN parent_search p ON f.id = p.id",
+ nativeQuery = true
+ )
+ List<FragmentExtract> findExtractsWithDescendants(@Param("anchorId") int anchorId,
+ @Param("xpaths") Collection<String> xpaths,
+ @Param("maxDepth") int maxDepth);
}
diff --git a/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryMultiPathQuery.java b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryMultiPathQuery.java deleted file mode 100644 index 9c34a459e..000000000 --- a/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryMultiPathQuery.java +++ /dev/null @@ -1,31 +0,0 @@ -/*- - * ============LICENSE_START======================================================= - * Copyright (C) 2022 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.repository; - -import java.util.Collection; -import java.util.List; -import org.onap.cps.spi.entities.FragmentEntity; - -public interface FragmentRepositoryMultiPathQuery { - - List<FragmentEntity> findByAnchorAndMultipleCpsPaths(Integer anchorId, Collection<String> cpsPathQuery); - -} diff --git a/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryMultiPathQueryImpl.java b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryMultiPathQueryImpl.java deleted file mode 100644 index 151fe97b3..000000000 --- a/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryMultiPathQueryImpl.java +++ /dev/null @@ -1,59 +0,0 @@ -/*- - * ============LICENSE_START======================================================= - * 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. - * 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.repository; - -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; -import javax.transaction.Transactional; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.onap.cps.spi.entities.FragmentEntity; - -@Slf4j -@RequiredArgsConstructor -public class FragmentRepositoryMultiPathQueryImpl implements FragmentRepositoryMultiPathQuery { - - @PersistenceContext - private final EntityManager entityManager; - - private final TempTableCreator tempTableCreator; - - @Override - @Transactional - public List<FragmentEntity> findByAnchorAndMultipleCpsPaths(final Integer anchorId, - final Collection<String> cpsPathQueryList) { - if (cpsPathQueryList.isEmpty()) { - return Collections.emptyList(); - } - final String tempTableName = tempTableCreator.createTemporaryTable( - "xpathTemporaryTable", cpsPathQueryList, "xpath"); - final String sql = String.format( - "SELECT * FROM FRAGMENT WHERE anchor_id = %d AND xpath IN (select xpath FROM %s);", - anchorId, tempTableName); - final List<FragmentEntity> fragmentEntities = entityManager.createNativeQuery(sql, FragmentEntity.class) - .getResultList(); - log.debug("Fetched {} fragment entities by anchor and cps path.", fragmentEntities.size()); - return fragmentEntities; - } -} diff --git a/cps-ri/src/main/java/org/onap/cps/spi/repository/TempTableCreator.java b/cps-ri/src/main/java/org/onap/cps/spi/repository/TempTableCreator.java index d798932c0..139a8b306 100644 --- a/cps-ri/src/main/java/org/onap/cps/spi/repository/TempTableCreator.java +++ b/cps-ri/src/main/java/org/onap/cps/spi/repository/TempTableCreator.java @@ -20,10 +20,8 @@ package org.onap.cps.spi.repository; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; @@ -66,24 +64,6 @@ public class TempTableCreator { return tempTableName; } - /** - * Create a uniquely named temporary table with a single column. - * - * @param prefix prefix for the table name (so you can recognize it) - * @param sqlData data to insert (strings only); each entry is a single row of data - * @param columnName column name - * @return a unique temporary table name with given prefix - */ - public String createTemporaryTable(final String prefix, - final Collection<String> sqlData, - final String columnName) { - final Collection<List<String>> tableData = new ArrayList<>(sqlData.size()); - for (final String entry : sqlData) { - tableData.add(Collections.singletonList(entry)); - } - return createTemporaryTable(prefix, tableData, columnName); - } - private static void defineColumns(final StringBuilder sqlStringBuilder, final String[] columnNames) { sqlStringBuilder.append('('); final Iterator<String> it = Arrays.stream(columnNames).iterator(); diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceIntegrationSpec.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceIntegrationSpec.groovy index 25f19f7a8..e60afa78d 100755 --- a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceIntegrationSpec.groovy +++ b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceIntegrationSpec.groovy @@ -307,7 +307,7 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase { 'root xpath' | ["/"] || 7 'empty (root) xpath' | [""] || 7 'root and top-level xpaths' | ["/", "/parent-200", "/parent-201"] || 7 - 'root and child xpaths' | ["/", "/parent-200/child-201"] || 8 + 'root and child xpaths' | ["/", "/parent-200/child-201"] || 7 } @Sql([CLEAR_DATA, SET_DATA]) 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 e74b4a745..3d7003d2a 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 @@ -26,6 +26,7 @@ import org.onap.cps.spi.FetchDescendantsOption import org.onap.cps.spi.entities.AnchorEntity import org.onap.cps.spi.entities.DataspaceEntity import org.onap.cps.spi.entities.FragmentEntity +import org.onap.cps.spi.entities.FragmentExtract import org.onap.cps.spi.exceptions.ConcurrencyException import org.onap.cps.spi.exceptions.DataValidationException import org.onap.cps.spi.model.DataNode @@ -150,9 +151,10 @@ class CpsDataPersistenceServiceSpec extends Specification { def 'Retrieving multiple data nodes.'() { given: 'fragment repository returns a collection of fragments' - def fragmentEntity1 = new FragmentEntity(xpath: '/xpath1', childFragments: []) - def fragmentEntity2 = new FragmentEntity(xpath: '/xpath2', childFragments: []) - mockFragmentRepository.findByAnchorAndMultipleCpsPaths(123, ['/xpath1', '/xpath2'] as Set<String>) >> [fragmentEntity1, fragmentEntity2] + mockFragmentRepository.findExtractsWithDescendants(123, ['/xpath1', '/xpath2'] as Set, _) >> [ + mockFragmentExtract(1, null, 123, '/xpath1', null), + mockFragmentExtract(2, null, 123, '/xpath2', null) + ] when: 'getting data nodes for 2 xpaths' def result = objectUnderTest.getDataNodesForMultipleXpaths('some-dataspace', 'some-anchor', ['/xpath1', '/xpath2'], FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) then: '2 data nodes are returned' @@ -203,8 +205,10 @@ class CpsDataPersistenceServiceSpec extends Specification { def 'update data node and descendants: #scenario'(){ given: 'the fragment repository returns fragment entities related to the xpath inputs' - mockFragmentRepository.findByAnchorAndMultipleCpsPaths(_, [] as Set) >> [] - mockFragmentRepository.findByAnchorAndMultipleCpsPaths(_, ['/test/xpath'] as Set) >> [new FragmentEntity(xpath: '/test/xpath', childFragments: [])] + mockFragmentRepository.findExtractsWithDescendants(_, [] as Set, _) >> [] + mockFragmentRepository.findExtractsWithDescendants(_, ['/test/xpath'] as Set, _) >> [ + mockFragmentExtract(1, null, 123, '/test/xpath', null) + ] when: 'replace data node tree' objectUnderTest.updateDataNodesAndDescendants('dataspaceName', 'anchorName', dataNodes) then: 'call fragment repository save all method' @@ -217,9 +221,10 @@ class CpsDataPersistenceServiceSpec extends Specification { def 'update data nodes and descendants'() { given: 'the fragment repository returns fragment entities related to the xpath inputs' - mockFragmentRepository.findByAnchorAndMultipleCpsPaths(_, ['/test/xpath1', '/test/xpath2'] as Set) >> [ - new FragmentEntity(xpath: '/test/xpath1', childFragments: [], anchor: anchorEntity), - new FragmentEntity(xpath: '/test/xpath2', childFragments: [], anchor: anchorEntity)] + mockFragmentRepository.findExtractsWithDescendants(123, ['/test/xpath1', '/test/xpath2'] as Set, _) >> [ + mockFragmentExtract(1, null, 123, '/test/xpath1', null), + mockFragmentExtract(2, null, 123, '/test/xpath2', null) + ] and: 'some data nodes with descendants' def dataNode1 = new DataNode(xpath: '/test/xpath1', leaves: ['id': 'testId1'], childDataNodes: [new DataNode(xpath: '/test/xpath1/child', leaves: ['id': 'childTestId1'])]) def dataNode2 = new DataNode(xpath: '/test/xpath2', leaves: ['id': 'testId2'], childDataNodes: [new DataNode(xpath: '/test/xpath2/child', leaves: ['id': 'childTestId2'])]) @@ -247,26 +252,39 @@ class CpsDataPersistenceServiceSpec extends Specification { def createDataNodesAndMockRepositoryMethodSupportingThem(Map<String, String> xpathToScenarioMap) { def dataNodes = [] - def fragmentEntities = [] + def fragmentExtracts = [] + def fragmentId = 1 xpathToScenarioMap.each { def xpath = it.key def scenario = it.value def dataNode = new DataNodeBuilder().withXpath(xpath).build() dataNodes.add(dataNode) - def fragmentEntity = new FragmentEntity(xpath: xpath, childFragments: []) - fragmentEntities.add(fragmentEntity) - mockFragmentRepository.getByAnchorAndXpath(_, xpath) >> fragmentEntity + def fragmentExtract = mockFragmentExtract(fragmentId, null, null, xpath, null) + fragmentExtracts.add(fragmentExtract) + def fragmentEntity = new FragmentEntity(id: fragmentId, xpath: xpath, childFragments: []) + mockFragmentRepository.getByDataspaceAndAnchorAndXpath(_, _, xpath) >> fragmentEntity if ('EXCEPTION' == scenario) { mockFragmentRepository.save(fragmentEntity) >> { throw new StaleStateException("concurrent updates") } } + fragmentId++ } - mockFragmentRepository.findByAnchorAndMultipleCpsPaths(_, xpathToScenarioMap.keySet()) >> fragmentEntities + mockFragmentRepository.findExtractsWithDescendants(_, xpathToScenarioMap.keySet(), _) >> fragmentExtracts return dataNodes } def mockFragmentWithJson(json) { - def fragmentEntity = new FragmentEntity(xpath: '/parent-01', childFragments: [], attributes: json) - mockFragmentRepository.findByAnchorAndMultipleCpsPaths(123, ['/parent-01'] as Set<String>) >> [fragmentEntity] + def fragmentExtract = mockFragmentExtract(456, null, null, '/parent-01', json) + mockFragmentRepository.findExtractsWithDescendants(123, ['/parent-01'] as Set, _) >> [fragmentExtract] + } + + def mockFragmentExtract(id, parentId, anchorId, xpath, attributes) { + def fragmentExtract = Mock(FragmentExtract) + fragmentExtract.getId() >> id + fragmentExtract.getParentId() >> parentId + fragmentExtract.getAnchorId() >> anchorId + fragmentExtract.getXpath() >> xpath + fragmentExtract.getAttributes() >> attributes + return fragmentExtract } } diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/performance/CpsDataPersistenceServicePerfTest.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/performance/CpsDataPersistenceServicePerfTest.groovy index 7f4716a7f..98ff211a6 100644 --- a/cps-ri/src/test/groovy/org/onap/cps/spi/performance/CpsDataPersistenceServicePerfTest.groovy +++ b/cps-ri/src/test/groovy/org/onap/cps/spi/performance/CpsDataPersistenceServicePerfTest.groovy @@ -72,8 +72,8 @@ class CpsDataPersistenceServicePerfTest extends CpsPersistencePerfSpecBase { assert countDataNodes(result[0]) == TOTAL_NUMBER_OF_NODES where: 'the following xPaths are used' scenario | xpath || allowedDuration - 'parent' | PERF_TEST_PARENT || 5000 - 'root' | '' || 500 + 'parent' | PERF_TEST_PARENT || 500 + 'root' | '/' || 500 } def 'Query parent data node with many descendants by cps-path'() { @@ -82,8 +82,8 @@ class CpsDataPersistenceServicePerfTest extends CpsPersistencePerfSpecBase { def result = objectUnderTest.queryDataNodes(PERF_DATASPACE, PERF_ANCHOR, '//perf-parent-1' , INCLUDE_ALL_DESCENDANTS) stopWatch.stop() def readDurationInMillis = stopWatch.getTotalTimeMillis() - then: 'read duration is under 500 milliseconds' - recordAndAssertPerformance('Query with many descendants', 500, readDurationInMillis) + then: 'read duration is under 350 milliseconds' + recordAndAssertPerformance('Query with many descendants', 350, readDurationInMillis) and: 'data node is returned with all the descendants populated' assert countDataNodes(result) == TOTAL_NUMBER_OF_NODES } @@ -97,8 +97,8 @@ class CpsDataPersistenceServicePerfTest extends CpsPersistencePerfSpecBase { def readDurationInMillis = stopWatch.getTotalTimeMillis() then: 'the returned number of entities equal to the number of children * number of grandchildren' assert result.size() == xpathsToAllGrandChildren.size() - and: 'it took less then 5000ms' - recordAndAssertPerformance('Find multiple xpaths', 5000, readDurationInMillis) + and: 'it took less then 1000ms' + recordAndAssertPerformance('Find multiple xpaths', 1000, readDurationInMillis) } def 'Query many descendants by cps-path with #scenario'() { @@ -131,8 +131,8 @@ class CpsDataPersistenceServicePerfTest extends CpsPersistencePerfSpecBase { objectUnderTest.updateDataNodesAndDescendants(PERF_DATASPACE, PERF_ANCHOR, dataNodes) stopWatch.stop() def updateDurationInMillis = stopWatch.getTotalTimeMillis() - then: 'update duration is under 900 milliseconds' - recordAndAssertPerformance('Update data nodes with descendants', 900, updateDurationInMillis) + then: 'update duration is under 500 milliseconds' + recordAndAssertPerformance('Update data nodes with descendants', 500, updateDurationInMillis) } def 'Update data nodes without descendants'() { diff --git a/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java b/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java index 721d4a9fb..cd14795ad 100755 --- a/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java +++ b/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java @@ -30,9 +30,7 @@ import static org.onap.cps.notification.Operation.UPDATE; import io.micrometer.core.annotation.Timed; import java.time.OffsetDateTime; -import java.util.ArrayList; import java.util.Collection; -import java.util.List; import java.util.Map; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; @@ -81,10 +79,10 @@ public class CpsDataServiceImpl implements CpsDataService { public void saveData(final String dataspaceName, final String anchorName, final String nodeData, final OffsetDateTime observedTimestamp, final ContentType contentType) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); - final Collection<DataNode> dataNodes = - buildDataNodes(dataspaceName, anchorName, ROOT_NODE_XPATH, nodeData, contentType); + final Anchor anchor = cpsAdminService.getAnchor(dataspaceName, anchorName); + final Collection<DataNode> dataNodes = buildDataNodes(anchor, ROOT_NODE_XPATH, nodeData, contentType); cpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName, dataNodes); - processDataUpdatedEventAsync(dataspaceName, anchorName, ROOT_NODE_XPATH, CREATE, observedTimestamp); + processDataUpdatedEventAsync(anchor, ROOT_NODE_XPATH, CREATE, observedTimestamp); } @Override @@ -100,10 +98,10 @@ public class CpsDataServiceImpl implements CpsDataService { final String nodeData, final OffsetDateTime observedTimestamp, final ContentType contentType) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); - final Collection<DataNode> dataNodes = - buildDataNodes(dataspaceName, anchorName, parentNodeXpath, nodeData, contentType); + final Anchor anchor = cpsAdminService.getAnchor(dataspaceName, anchorName); + final Collection<DataNode> dataNodes = buildDataNodes(anchor, parentNodeXpath, nodeData, contentType); cpsDataPersistenceService.addChildDataNodes(dataspaceName, anchorName, parentNodeXpath, dataNodes); - processDataUpdatedEventAsync(dataspaceName, anchorName, parentNodeXpath, CREATE, observedTimestamp); + processDataUpdatedEventAsync(anchor, parentNodeXpath, CREATE, observedTimestamp); } @Override @@ -112,11 +110,12 @@ public class CpsDataServiceImpl implements CpsDataService { public void saveListElements(final String dataspaceName, final String anchorName, final String parentNodeXpath, final String jsonData, final OffsetDateTime observedTimestamp) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); + final Anchor anchor = cpsAdminService.getAnchor(dataspaceName, anchorName); final Collection<DataNode> listElementDataNodeCollection = - buildDataNodes(dataspaceName, anchorName, parentNodeXpath, jsonData, ContentType.JSON); + buildDataNodes(anchor, parentNodeXpath, jsonData, ContentType.JSON); cpsDataPersistenceService.addListElements(dataspaceName, anchorName, parentNodeXpath, listElementDataNodeCollection); - processDataUpdatedEventAsync(dataspaceName, anchorName, parentNodeXpath, UPDATE, observedTimestamp); + processDataUpdatedEventAsync(anchor, parentNodeXpath, UPDATE, observedTimestamp); } @Override @@ -125,11 +124,12 @@ public class CpsDataServiceImpl implements CpsDataService { public void saveListElementsBatch(final String dataspaceName, final String anchorName, final String parentNodeXpath, final Collection<String> jsonDataList, final OffsetDateTime observedTimestamp) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); + final Anchor anchor = cpsAdminService.getAnchor(dataspaceName, anchorName); final Collection<Collection<DataNode>> listElementDataNodeCollections = - buildDataNodes(dataspaceName, anchorName, parentNodeXpath, jsonDataList, ContentType.JSON); + buildDataNodes(anchor, parentNodeXpath, jsonDataList, ContentType.JSON); cpsDataPersistenceService.addMultipleLists(dataspaceName, anchorName, parentNodeXpath, listElementDataNodeCollections); - processDataUpdatedEventAsync(dataspaceName, anchorName, parentNodeXpath, UPDATE, observedTimestamp); + processDataUpdatedEventAsync(anchor, parentNodeXpath, UPDATE, observedTimestamp); } @Override @@ -159,10 +159,11 @@ public class CpsDataServiceImpl implements CpsDataService { public void updateNodeLeaves(final String dataspaceName, final String anchorName, final String parentNodeXpath, final String jsonData, final OffsetDateTime observedTimestamp) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); - final DataNode dataNode = buildDataNode(dataspaceName, anchorName, parentNodeXpath, jsonData, ContentType.JSON); - cpsDataPersistenceService - .updateDataLeaves(dataspaceName, anchorName, dataNode.getXpath(), dataNode.getLeaves()); - processDataUpdatedEventAsync(dataspaceName, anchorName, parentNodeXpath, UPDATE, observedTimestamp); + final Anchor anchor = cpsAdminService.getAnchor(dataspaceName, anchorName); + final DataNode dataNode = buildDataNode(anchor, parentNodeXpath, jsonData, ContentType.JSON); + cpsDataPersistenceService.updateDataLeaves(dataspaceName, anchorName, dataNode.getXpath(), + dataNode.getLeaves()); + processDataUpdatedEventAsync(anchor, parentNodeXpath, UPDATE, observedTimestamp); } @Override @@ -173,13 +174,13 @@ public class CpsDataServiceImpl implements CpsDataService { final String dataNodeUpdatesAsJson, final OffsetDateTime observedTimestamp) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); + final Anchor anchor = cpsAdminService.getAnchor(dataspaceName, anchorName); final Collection<DataNode> dataNodeUpdates = - buildDataNodes(dataspaceName, anchorName, - parentNodeXpath, dataNodeUpdatesAsJson, ContentType.JSON); + buildDataNodes(anchor, parentNodeXpath, dataNodeUpdatesAsJson, ContentType.JSON); for (final DataNode dataNodeUpdate : dataNodeUpdates) { - processDataNodeUpdate(dataspaceName, anchorName, dataNodeUpdate); + processDataNodeUpdate(anchor, dataNodeUpdate); } - processDataUpdatedEventAsync(dataspaceName, anchorName, parentNodeXpath, UPDATE, observedTimestamp); + processDataUpdatedEventAsync(anchor, parentNodeXpath, UPDATE, observedTimestamp); } @Override @@ -210,11 +211,10 @@ public class CpsDataServiceImpl implements CpsDataService { final String parentNodeXpath, final String jsonData, final OffsetDateTime observedTimestamp) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); - final Collection<DataNode> dataNodes = - buildDataNodes(dataspaceName, anchorName, parentNodeXpath, jsonData, ContentType.JSON); - final ArrayList<DataNode> nodes = new ArrayList<>(dataNodes); - cpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName, nodes); - processDataUpdatedEventAsync(dataspaceName, anchorName, parentNodeXpath, UPDATE, observedTimestamp); + final Anchor anchor = cpsAdminService.getAnchor(dataspaceName, anchorName); + final Collection<DataNode> dataNodes = buildDataNodes(anchor, parentNodeXpath, jsonData, ContentType.JSON); + cpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName, dataNodes); + processDataUpdatedEventAsync(anchor, parentNodeXpath, UPDATE, observedTimestamp); } @Override @@ -224,11 +224,11 @@ public class CpsDataServiceImpl implements CpsDataService { final Map<String, String> nodesJsonData, final OffsetDateTime observedTimestamp) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); - final List<DataNode> dataNodes = buildDataNodes(dataspaceName, anchorName, nodesJsonData); + final Anchor anchor = cpsAdminService.getAnchor(dataspaceName, anchorName); + final Collection<DataNode> dataNodes = buildDataNodes(anchor, nodesJsonData); cpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName, dataNodes); nodesJsonData.keySet().forEach(nodeXpath -> - processDataUpdatedEventAsync(dataspaceName, anchorName, nodeXpath, - UPDATE, observedTimestamp)); + processDataUpdatedEventAsync(anchor, nodeXpath, UPDATE, observedTimestamp)); } @Override @@ -237,8 +237,9 @@ public class CpsDataServiceImpl implements CpsDataService { public void replaceListContent(final String dataspaceName, final String anchorName, final String parentNodeXpath, final String jsonData, final OffsetDateTime observedTimestamp) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); + final Anchor anchor = cpsAdminService.getAnchor(dataspaceName, anchorName); final Collection<DataNode> newListElements = - buildDataNodes(dataspaceName, anchorName, parentNodeXpath, jsonData, ContentType.JSON); + buildDataNodes(anchor, parentNodeXpath, jsonData, ContentType.JSON); replaceListContent(dataspaceName, anchorName, parentNodeXpath, newListElements, observedTimestamp); } @@ -248,8 +249,9 @@ public class CpsDataServiceImpl implements CpsDataService { public void replaceListContent(final String dataspaceName, final String anchorName, final String parentNodeXpath, final Collection<DataNode> dataNodes, final OffsetDateTime observedTimestamp) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); + final Anchor anchor = cpsAdminService.getAnchor(dataspaceName, anchorName); cpsDataPersistenceService.replaceListContent(dataspaceName, anchorName, parentNodeXpath, dataNodes); - processDataUpdatedEventAsync(dataspaceName, anchorName, parentNodeXpath, UPDATE, observedTimestamp); + processDataUpdatedEventAsync(anchor, parentNodeXpath, UPDATE, observedTimestamp); } @Override @@ -258,8 +260,9 @@ public class CpsDataServiceImpl implements CpsDataService { public void deleteDataNode(final String dataspaceName, final String anchorName, final String dataNodeXpath, final OffsetDateTime observedTimestamp) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); + final Anchor anchor = cpsAdminService.getAnchor(dataspaceName, anchorName); cpsDataPersistenceService.deleteDataNode(dataspaceName, anchorName, dataNodeXpath); - processDataUpdatedEventAsync(dataspaceName, anchorName, dataNodeXpath, DELETE, observedTimestamp); + processDataUpdatedEventAsync(anchor, dataNodeXpath, DELETE, observedTimestamp); } @Override @@ -269,8 +272,9 @@ public class CpsDataServiceImpl implements CpsDataService { final Collection<String> dataNodeXpaths, final OffsetDateTime observedTimestamp) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); cpsDataPersistenceService.deleteDataNodes(dataspaceName, anchorName, dataNodeXpaths); + final Anchor anchor = cpsAdminService.getAnchor(dataspaceName, anchorName); dataNodeXpaths.forEach(dataNodeXpath -> - processDataUpdatedEventAsync(dataspaceName, anchorName, dataNodeXpath, DELETE, observedTimestamp)); + processDataUpdatedEventAsync(anchor, dataNodeXpath, DELETE, observedTimestamp)); } @Override @@ -279,7 +283,8 @@ public class CpsDataServiceImpl implements CpsDataService { public void deleteDataNodes(final String dataspaceName, final String anchorName, final OffsetDateTime observedTimestamp) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); - processDataUpdatedEventAsync(dataspaceName, anchorName, ROOT_NODE_XPATH, DELETE, observedTimestamp); + final Anchor anchor = cpsAdminService.getAnchor(dataspaceName, anchorName); + processDataUpdatedEventAsync(anchor, ROOT_NODE_XPATH, DELETE, observedTimestamp); cpsDataPersistenceService.deleteDataNodes(dataspaceName, anchorName); } @@ -290,8 +295,8 @@ public class CpsDataServiceImpl implements CpsDataService { final OffsetDateTime observedTimestamp) { cpsValidator.validateNameCharacters(dataspaceName); cpsValidator.validateNameCharacters(anchorNames); - for (final String anchorName : anchorNames) { - processDataUpdatedEventAsync(dataspaceName, anchorName, ROOT_NODE_XPATH, DELETE, observedTimestamp); + for (final Anchor anchor : cpsAdminService.getAnchors(dataspaceName, anchorNames)) { + processDataUpdatedEventAsync(anchor, ROOT_NODE_XPATH, DELETE, observedTimestamp); } cpsDataPersistenceService.deleteDataNodes(dataspaceName, anchorNames); } @@ -302,16 +307,14 @@ public class CpsDataServiceImpl implements CpsDataService { public void deleteListOrListElement(final String dataspaceName, final String anchorName, final String listNodeXpath, final OffsetDateTime observedTimestamp) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); + final Anchor anchor = cpsAdminService.getAnchor(dataspaceName, anchorName); cpsDataPersistenceService.deleteListDataNode(dataspaceName, anchorName, listNodeXpath); - processDataUpdatedEventAsync(dataspaceName, anchorName, listNodeXpath, DELETE, observedTimestamp); + processDataUpdatedEventAsync(anchor, listNodeXpath, DELETE, observedTimestamp); } - private DataNode buildDataNode(final String dataspaceName, final String anchorName, - final String parentNodeXpath, final String nodeData, + private DataNode buildDataNode(final Anchor anchor, final String parentNodeXpath, final String nodeData, final ContentType contentType) { - - final Anchor anchor = cpsAdminService.getAnchor(dataspaceName, anchorName); - final SchemaContext schemaContext = getSchemaContext(dataspaceName, anchor.getSchemaSetName()); + final SchemaContext schemaContext = getSchemaContext(anchor); if (ROOT_NODE_XPATH.equals(parentNodeXpath)) { final ContainerNode containerNode = timedYangParser.parseData(contentType, nodeData, schemaContext); @@ -327,21 +330,15 @@ public class CpsDataServiceImpl implements CpsDataService { .build(); } - private List<DataNode> buildDataNodes(final String dataspaceName, final String anchorName, - final Map<String, String> nodesJsonData) { + private Collection<DataNode> buildDataNodes(final Anchor anchor, final Map<String, String> nodesJsonData) { return nodesJsonData.entrySet().stream().map(nodeJsonData -> - buildDataNode(dataspaceName, anchorName, nodeJsonData.getKey(), + buildDataNode(anchor, nodeJsonData.getKey(), nodeJsonData.getValue(), ContentType.JSON)).collect(Collectors.toList()); } - private Collection<DataNode> buildDataNodes(final String dataspaceName, - final String anchorName, - final String parentNodeXpath, - final String nodeData, - final ContentType contentType) { - - final Anchor anchor = cpsAdminService.getAnchor(dataspaceName, anchorName); - final SchemaContext schemaContext = getSchemaContext(dataspaceName, anchor.getSchemaSetName()); + private Collection<DataNode> buildDataNodes(final Anchor anchor, final String parentNodeXpath, + final String nodeData, final ContentType contentType) { + final SchemaContext schemaContext = getSchemaContext(anchor); if (ROOT_NODE_XPATH.equals(parentNodeXpath)) { final ContainerNode containerNode = timedYangParser.parseData(contentType, nodeData, schemaContext); @@ -365,37 +362,38 @@ public class CpsDataServiceImpl implements CpsDataService { return dataNodes; } - private Collection<Collection<DataNode>> buildDataNodes(final String dataspaceName, final String anchorName, - final String parentNodeXpath, final Collection<String> nodeDataList, final ContentType contentType) { + private Collection<Collection<DataNode>> buildDataNodes(final Anchor anchor, final String parentNodeXpath, + final Collection<String> nodeDataList, + final ContentType contentType) { return nodeDataList.stream() - .map(nodeData -> buildDataNodes(dataspaceName, anchorName, parentNodeXpath, nodeData, contentType)) - .collect(Collectors.toList()); + .map(nodeData -> buildDataNodes(anchor, parentNodeXpath, nodeData, contentType)) + .collect(Collectors.toList()); } - private void processDataUpdatedEventAsync(final String dataspaceName, final String anchorName, final String xpath, - final Operation operation, final OffsetDateTime observedTimestamp) { + private void processDataUpdatedEventAsync(final Anchor anchor, final String xpath, + final Operation operation, final OffsetDateTime observedTimestamp) { try { - notificationService.processDataUpdatedEvent(dataspaceName, anchorName, xpath, operation, observedTimestamp); + notificationService.processDataUpdatedEvent(anchor, xpath, operation, observedTimestamp); } catch (final Exception exception) { //If async message can't be queued for notification service, the initial request should not failed. log.error("Failed to send message to notification service", exception); } } - private SchemaContext getSchemaContext(final String dataspaceName, final String schemaSetName) { - return yangTextSchemaSourceSetCache.get(dataspaceName, schemaSetName).getSchemaContext(); + private SchemaContext getSchemaContext(final Anchor anchor) { + return yangTextSchemaSourceSetCache + .get(anchor.getDataspaceName(), anchor.getSchemaSetName()).getSchemaContext(); } - private void processDataNodeUpdate(final String dataspaceName, final String anchorName, - final DataNode dataNodeUpdate) { + private void processDataNodeUpdate(final Anchor anchor, final DataNode dataNodeUpdate) { if (dataNodeUpdate == null) { return; } - cpsDataPersistenceService.updateDataLeaves(dataspaceName, anchorName, dataNodeUpdate.getXpath(), - dataNodeUpdate.getLeaves()); + cpsDataPersistenceService.updateDataLeaves(anchor.getDataspaceName(), anchor.getName(), + dataNodeUpdate.getXpath(), dataNodeUpdate.getLeaves()); final Collection<DataNode> childDataNodeUpdates = dataNodeUpdate.getChildDataNodes(); for (final DataNode childDataNodeUpdate : childDataNodeUpdates) { - processDataNodeUpdate(dataspaceName, anchorName, childDataNodeUpdate); + processDataNodeUpdate(anchor, childDataNodeUpdate); } } diff --git a/cps-service/src/main/java/org/onap/cps/notification/NotificationService.java b/cps-service/src/main/java/org/onap/cps/notification/NotificationService.java index 7da3a6123..b9d40740e 100644 --- a/cps-service/src/main/java/org/onap/cps/notification/NotificationService.java +++ b/cps-service/src/main/java/org/onap/cps/notification/NotificationService.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 * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -70,21 +70,19 @@ public class NotificationService { /** * Process Data Updated Event and publishes the notification. * - * @param dataspaceName dataspaceName - * @param anchorName anchorName + * @param anchor anchor * @param xpath xpath of changed data node * @param operation operation * @param observedTimestamp observedTimestamp * @return future */ @Async("notificationExecutor") - public Future<Void> processDataUpdatedEvent(final String dataspaceName, final String anchorName, - final String xpath, final Operation operation, final OffsetDateTime observedTimestamp) { + public Future<Void> processDataUpdatedEvent(final Anchor anchor, final String xpath, final Operation operation, + final OffsetDateTime observedTimestamp) { - final Anchor anchor = cpsAdminService.getAnchor(dataspaceName, anchorName); log.debug("process data updated event for anchor '{}'", anchor); try { - if (shouldSendNotification(dataspaceName)) { + if (shouldSendNotification(anchor.getDataspaceName())) { final var cpsDataUpdatedEvent = cpsDataUpdatedEventFactory.createCpsDataUpdatedEvent(anchor, observedTimestamp, getRootNodeOperation(xpath, operation)); diff --git a/cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java b/cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java index 90e6ec761..f10443fda 100644 --- a/cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java +++ b/cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java @@ -153,7 +153,7 @@ public interface CpsDataPersistenceService { * @param anchorName anchor name * @param dataNodes data nodes */ - void updateDataNodesAndDescendants(String dataspaceName, String anchorName, final List<DataNode> dataNodes); + void updateDataNodesAndDescendants(String dataspaceName, String anchorName, final Collection<DataNode> dataNodes); /** * Replaces list content by removing all existing elements and inserting the given new elements 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 cf5e04dc4..02574995d 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 @@ -76,6 +76,14 @@ public class FetchDescendantsOption { } /** + * Get depth. + * @return depth: -1 for all descendants, 0 for no descendants, or positive value for fixed level of descendants + */ + public int getDepth() { + return depth; + } + + /** * get fetch descendants option for given descendant. * * @param fetchDescendantsOptionAsString fetch descendants option string diff --git a/cps-service/src/main/java/org/onap/cps/spi/exceptions/OperationNotYetSupportedException.java b/cps-service/src/main/java/org/onap/cps/spi/exceptions/OperationNotYetSupportedException.java new file mode 100644 index 000000000..6a4e2a098 --- /dev/null +++ b/cps-service/src/main/java/org/onap/cps/spi/exceptions/OperationNotYetSupportedException.java @@ -0,0 +1,40 @@ +/* + * ============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.exceptions; + +/** + * 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 { + + private static final long serialVersionUID = 1517903069236383746L; + + /** + * Constructor. + * + * @param details reason for the exception + */ + public OperationNotYetSupportedException(final String details) { + super("Operation Not Yet Supported Exception", details); + } +} 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 e304d28d8..faa5d2edb 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 @@ -61,7 +61,7 @@ class CpsDataServiceImplSpec extends Specification { def dataspaceName = 'some-dataspace' def anchorName = 'some-anchor' def schemaSetName = 'some-schema-set' - def anchor = Anchor.builder().name(anchorName).schemaSetName(schemaSetName).build() + def anchor = Anchor.builder().name(anchorName).dataspaceName(dataspaceName).schemaSetName(schemaSetName).build() def observedTimestamp = OffsetDateTime.now() def 'Saving multicontainer json data.'() { @@ -76,7 +76,7 @@ class CpsDataServiceImplSpec extends Specification { and: 'the CpsValidator is called on the dataspaceName and AnchorName' 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName) and: 'data updated event is sent to notification service' - 1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, '/', Operation.CREATE, observedTimestamp) + 1 * mockNotificationService.processDataUpdatedEvent(anchor, '/', Operation.CREATE, observedTimestamp) where: index | xpath 0 | '/first-container' @@ -96,7 +96,7 @@ class CpsDataServiceImplSpec extends Specification { and: 'the CpsValidator is called on the dataspaceName and AnchorName' 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName) and: 'data updated event is sent to notification service' - 1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, '/', Operation.CREATE, observedTimestamp) + 1 * mockNotificationService.processDataUpdatedEvent(anchor, '/', Operation.CREATE, observedTimestamp) where: 'given parameters' scenario | dataFile | contentType 'json' | 'test-tree.json' | ContentType.JSON @@ -129,7 +129,7 @@ class CpsDataServiceImplSpec extends Specification { and: 'the CpsValidator is called on the dataspaceName and AnchorName' 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName) and: 'data updated event is sent to notification service' - 1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, '/test-tree', Operation.CREATE, observedTimestamp) + 1 * mockNotificationService.processDataUpdatedEvent(anchor, '/test-tree', Operation.CREATE, observedTimestamp) } def 'Saving list element data fragment under existing node.'() { @@ -151,7 +151,7 @@ class CpsDataServiceImplSpec extends Specification { and: 'the CpsValidator is called on the dataspaceName and AnchorName' 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName) and: 'data updated event is sent to notification service' - 1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, '/test-tree', Operation.UPDATE, observedTimestamp) + 1 * mockNotificationService.processDataUpdatedEvent(anchor, '/test-tree', Operation.UPDATE, observedTimestamp) } def 'Saving collection of a batch with data fragment under existing node.'() { @@ -171,7 +171,7 @@ class CpsDataServiceImplSpec extends Specification { } } and: 'data updated event is sent to notification service' - 1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, '/test-tree', Operation.UPDATE, observedTimestamp) + 1 * mockNotificationService.processDataUpdatedEvent(anchor, '/test-tree', Operation.UPDATE, observedTimestamp) } def 'Saving empty list element data fragment.'() { @@ -219,7 +219,7 @@ class CpsDataServiceImplSpec extends Specification { and: 'the CpsValidator is called on the dataspaceName and AnchorName' 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName) and: 'data updated event is sent to notification service' - 1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, parentNodeXpath, Operation.UPDATE, observedTimestamp) + 1 * mockNotificationService.processDataUpdatedEvent(anchor, parentNodeXpath, Operation.UPDATE, observedTimestamp) where: 'following parameters were used' scenario | parentNodeXpath | jsonData || expectedNodeXpath | leaves 'top level node' | '/' | '{"test-tree": {"branch": []}}' || '/test-tree' | Collections.emptyMap() @@ -254,7 +254,7 @@ class CpsDataServiceImplSpec extends Specification { and: 'the CpsValidator is called on the dataspaceName and AnchorName' 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName) and: 'the data updated event is sent to the notification service' - 1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, '/bookstore', Operation.UPDATE, observedTimestamp) + 1 * mockNotificationService.processDataUpdatedEvent(anchor, '/bookstore', Operation.UPDATE, observedTimestamp) } def 'Replace data node using singular data node: #scenario.'() { @@ -266,7 +266,7 @@ class CpsDataServiceImplSpec extends Specification { 1 * mockCpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName, { dataNode -> dataNode.xpath[0] == expectedNodeXpath }) and: 'data updated event is sent to notification service' - 1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, parentNodeXpath, Operation.UPDATE, observedTimestamp) + 1 * mockNotificationService.processDataUpdatedEvent(anchor, parentNodeXpath, Operation.UPDATE, observedTimestamp) and: 'the CpsValidator is called on the dataspaceName and AnchorName' 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName) where: 'following parameters were used' @@ -284,8 +284,8 @@ class CpsDataServiceImplSpec extends Specification { 1 * mockCpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName, { dataNode -> dataNode.xpath == expectedNodeXpath}) and: 'data updated event is sent to notification service' - 1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, nodesJsonData.keySet()[0], Operation.UPDATE, observedTimestamp) - 1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, nodesJsonData.keySet()[1], Operation.UPDATE, observedTimestamp) + 1 * mockNotificationService.processDataUpdatedEvent(anchor, nodesJsonData.keySet()[0], Operation.UPDATE, observedTimestamp) + 1 * mockNotificationService.processDataUpdatedEvent(anchor, nodesJsonData.keySet()[1], Operation.UPDATE, observedTimestamp) and: 'the CpsValidator is called on the dataspaceName and AnchorName' 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName) where: 'following parameters were used' @@ -313,7 +313,7 @@ class CpsDataServiceImplSpec extends Specification { and: 'the CpsValidator is called on the dataspaceName and AnchorName twice' 2 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName) and: 'data updated event is sent to notification service' - 1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, '/test-tree', Operation.UPDATE, observedTimestamp) + 1 * mockNotificationService.processDataUpdatedEvent(anchor, '/test-tree', Operation.UPDATE, observedTimestamp) } def 'Replace whole list content with empty list element.'() { @@ -336,7 +336,7 @@ class CpsDataServiceImplSpec extends Specification { and: 'the CpsValidator is called on the dataspaceName and AnchorName' 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName) and: 'data updated event is sent to notification service' - 1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, '/test-tree/branch', Operation.DELETE, observedTimestamp) + 1 * mockNotificationService.processDataUpdatedEvent(anchor, '/test-tree/branch', Operation.DELETE, observedTimestamp) } def 'Delete multiple list elements under existing node.'() { @@ -349,7 +349,7 @@ class CpsDataServiceImplSpec extends Specification { and: 'the CpsValidator is called on the dataspaceName and AnchorName' 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName) and: 'two data updated events are sent to notification service' - 2 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, _, Operation.DELETE, observedTimestamp) + 2 * mockNotificationService.processDataUpdatedEvent(anchor, _, Operation.DELETE, observedTimestamp) } def 'Delete data node under anchor and dataspace.'() { @@ -362,7 +362,7 @@ class CpsDataServiceImplSpec extends Specification { and: 'the CpsValidator is called on the dataspaceName and AnchorName' 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName) and: 'data updated event is sent to notification service' - 1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, '/data-node', Operation.DELETE, observedTimestamp) + 1 * mockNotificationService.processDataUpdatedEvent(anchor, '/data-node', Operation.DELETE, observedTimestamp) } def 'Delete all data nodes for a given anchor and dataspace.'() { @@ -371,7 +371,7 @@ class CpsDataServiceImplSpec extends Specification { when: 'delete data node 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(dataspaceName, anchorName, '/', Operation.DELETE, observedTimestamp) + 1 * mockNotificationService.processDataUpdatedEvent(anchor, '/', Operation.DELETE, observedTimestamp) and: 'the CpsValidator is called on the dataspaceName and AnchorName' 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName) and: 'the persistence service method is invoked with the correct parameters' @@ -381,10 +381,13 @@ class CpsDataServiceImplSpec extends Specification { 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') + mockCpsAdminService.getAnchors(dataspaceName, ['anchor1', 'anchor2']) >> + [new Anchor(name: 'anchor1', dataspaceName: dataspaceName), + new Anchor(name: 'anchor2', dataspaceName: dataspaceName)] when: 'delete data node method is invoked with correct parameters' objectUnderTest.deleteDataNodes(dataspaceName, ['anchor1', 'anchor2'], observedTimestamp) then: 'data updated events are sent to notification service before the delete' - 2 * mockNotificationService.processDataUpdatedEvent(dataspaceName, _, '/', Operation.DELETE, observedTimestamp) + 2 * mockNotificationService.processDataUpdatedEvent(_, '/', Operation.DELETE, observedTimestamp) and: 'the CpsValidator is called on the dataspace name and the anchor names' 2 * mockCpsValidator.validateNameCharacters(_) and: 'the persistence service method is invoked with the correct parameters' @@ -431,4 +434,4 @@ class CpsDataServiceImplSpec extends Specification { 1 * mockCpsDataPersistenceService.lockAnchor('some-sessionId', 'some-dataspaceName', 'some-anchorName', 250L) } -}
\ No newline at end of file +} diff --git a/cps-service/src/test/groovy/org/onap/cps/api/impl/E2ENetworkSliceSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/api/impl/E2ENetworkSliceSpec.groovy index 8ed7aede6..75f29746d 100755 --- a/cps-service/src/test/groovy/org/onap/cps/api/impl/E2ENetworkSliceSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/api/impl/E2ENetworkSliceSpec.groovy @@ -90,7 +90,7 @@ class E2ENetworkSliceSpec extends Specification { def jsonData = TestUtils.getResourceFileContent('e2e/basic/cps-Cavsta-Data.txt')
and : 'all the further dependencies are mocked '
mockCpsAdminService.getAnchor(dataspaceName, anchorName) >>
- new Anchor().builder().name(anchorName).schemaSetName(schemaSetName).build()
+ new Anchor().builder().name(anchorName).schemaSetName(schemaSetName).dataspaceName(dataspaceName).build()
mockYangTextSchemaSourceSetCache.get(dataspaceName, schemaSetName) >>
YangTextSchemaSourceSetBuilder.of(yangResourcesNameToContentMap)
mockModuleStoreService.getYangSchemaResources(dataspaceName, schemaSetName) >> schemaContext
@@ -123,7 +123,7 @@ class E2ENetworkSliceSpec extends Specification { def jsonData = TestUtils.getResourceFileContent('e2e/basic/cps-ran-inventory-data.json')
and : 'all the further dependencies are mocked '
mockCpsAdminService.getAnchor('someDataspace', 'someAnchor') >>
- new Anchor().builder().name('someAnchor').schemaSetName('someSchemaSet').build()
+ new Anchor().builder().name('someAnchor').schemaSetName('someSchemaSet').dataspaceName(dataspaceName).build()
mockYangTextSchemaSourceSetCache.get('someDataspace', 'someSchemaSet') >> YangTextSchemaSourceSetBuilder.of(yangResourcesNameToContentMap)
mockModuleStoreService.getYangSchemaResources('someDataspace', 'someSchemaSet') >> schemaContext
when: 'saveData method is invoked'
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 a996195c0..2ef468bb5 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 @@ -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 * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -72,7 +72,7 @@ class NotificationServiceSpec extends Specification { given: 'notification is disabled' spyNotificationProperties.isEnabled() >> false when: 'dataUpdatedEvent is received' - objectUnderTest.processDataUpdatedEvent(dataspaceName, anchorName, '/', Operation.CREATE, myObservedTimestamp) + objectUnderTest.processDataUpdatedEvent(anchor, '/', Operation.CREATE, myObservedTimestamp) then: 'the notification is not sent' 0 * mockNotificationPublisher.sendNotification(_) } @@ -84,11 +84,9 @@ class NotificationServiceSpec extends Specification { def anchor = new Anchor('my-anchorname', dataspaceName, 'my-schemaset-name') and: 'event factory can create event successfully' def cpsDataUpdatedEvent = new CpsDataUpdatedEvent() - mockCpsDataUpdatedEventFactory.createCpsDataUpdatedEvent(anchor, myObservedTimestamp, Operation.CREATE) >> - cpsDataUpdatedEvent + mockCpsDataUpdatedEventFactory.createCpsDataUpdatedEvent(anchor, myObservedTimestamp, Operation.CREATE) >> cpsDataUpdatedEvent when: 'dataUpdatedEvent is received' - def future = objectUnderTest.processDataUpdatedEvent(dataspaceName, anchorName, - '/', Operation.CREATE, myObservedTimestamp) + def future = objectUnderTest.processDataUpdatedEvent(anchor, '/', Operation.CREATE, myObservedTimestamp) and: 'wait for async processing to complete' future.get(10, TimeUnit.SECONDS) then: 'async process completed successfully' @@ -109,7 +107,7 @@ class NotificationServiceSpec extends Specification { mockCpsDataUpdatedEventFactory.createCpsDataUpdatedEvent(anchor, myObservedTimestamp, expectedOperationInEvent) >> cpsDataUpdatedEvent when: 'dataUpdatedEvent is received for #xpath' - def future = objectUnderTest.processDataUpdatedEvent(dataspaceName, anchorName, xpath, operation, myObservedTimestamp) + def future = objectUnderTest.processDataUpdatedEvent(anchor, xpath, operation, myObservedTimestamp) and: 'wait for async processing to complete' future.get(10, TimeUnit.SECONDS) then: 'async process completed successfully' @@ -139,7 +137,7 @@ class NotificationServiceSpec extends Specification { mockCpsDataUpdatedEventFactory.createCpsDataUpdatedEvent(anchor, myObservedTimestamp, Operation.CREATE) >> { throw new Exception("Could not create event") } when: 'event is sent for processing' - def future = objectUnderTest.processDataUpdatedEvent(dataspaceName, anchorName, '/', Operation.CREATE, myObservedTimestamp) + def future = objectUnderTest.processDataUpdatedEvent(anchor, '/', Operation.CREATE, myObservedTimestamp) and: 'wait for async processing to complete' future.get(10, TimeUnit.SECONDS) then: 'async process completed successfully' 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 6fb6d844a..e75f1dce3 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 @@ -20,6 +20,8 @@ package org.onap.cps.integration.performance.base +import org.onap.cps.spi.FetchDescendantsOption + import java.time.OffsetDateTime import org.onap.cps.integration.base.CpsIntegrationSpecBase import org.onap.cps.rest.utils.MultipartFileUtil @@ -44,11 +46,21 @@ class CpsPerfTestBase extends PerfTestBase { } def createInitialData() { + createWarmupData() createLargeBookstoresData() addOpenRoadModel() addOpenRoadData() } + def createWarmupData() { + def data = "{\"bookstore\":{}}" + stopWatch.start() + addAnchorsWithData(1, CpsIntegrationSpecBase.BOOKSTORE_SCHEMA_SET, 'warmup', data) + stopWatch.stop() + def durationInMillis = stopWatch.getTotalTimeMillis() + recordAndAssertPerformance('Creating warmup anchor with tiny data tree', 250, durationInMillis) + } + def createLargeBookstoresData() { def data = CpsIntegrationSpecBase.readResourceDataFile('bookstore/largeModelData.json') stopWatch.start() @@ -80,7 +92,17 @@ class CpsPerfTestBase extends PerfTestBase { cpsAdminService.createAnchor(CPS_PERFORMANCE_TEST_DATASPACE, schemaSetName, anchorNamePrefix + it) cpsDataService.saveData(CPS_PERFORMANCE_TEST_DATASPACE, anchorNamePrefix + it, data, OffsetDateTime.now()) } + } + def 'Warm the database'() { + when: 'get data nodes for warmup anchor' + stopWatch.start() + def result = cpsDataService.getDataNodes(CPS_PERFORMANCE_TEST_DATASPACE, 'warmup1', '/', FetchDescendantsOption.OMIT_DESCENDANTS) + assert countDataNodesInTree(result) == 1 + stopWatch.stop() + def durationInMillis = stopWatch.getTotalTimeMillis() + then: 'all data is read within 15 seconds (warm up not critical)' + recordAndAssertPerformance("Warming database", 15_000, durationInMillis) } } 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 753faf44f..30e8bf23d 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 @@ -42,10 +42,10 @@ class GetPerfTest extends CpsPerfTestBase { recordAndAssertPerformance("Read datatrees using ${scenario}", durationLimit, durationInMillis) where: 'the following xpaths are used' scenario | anchorPrefix | xpath || durationLimit | expectedNumberOfDataNodes - 'bookstore root' | 'bookstore' | '/' || 25_000 | 78 - 'bookstore top element' | 'bookstore' | '/bookstore' || 1_000 | 78 - 'openroadm root' | 'openroadm' | '/' || 1_000 | 2151 - 'openroadm top element' | 'openroadm' | '/openroadm-devices' || 10_000 | 2151 + 'bookstore root' | 'bookstore' | '/' || 130 | 78 + 'bookstore top element' | 'bookstore' | '/bookstore' || 130 | 78 + 'openroadm root' | 'openroadm' | '/' || 750 | 2151 + 'openroadm top element' | 'openroadm' | '/openroadm-devices' || 750 | 2151 } } diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/performance/ncmp/CmHandleQueryPerfTest.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/performance/ncmp/CmHandleQueryPerfTest.groovy index 939281a73..87327030c 100644 --- a/integration-test/src/test/groovy/org/onap/cps/integration/performance/ncmp/CmHandleQueryPerfTest.groovy +++ b/integration-test/src/test/groovy/org/onap/cps/integration/performance/ncmp/CmHandleQueryPerfTest.groovy @@ -21,12 +21,11 @@ package org.onap.cps.integration.performance.ncmp import java.util.stream.Collectors - +import org.onap.cps.integration.performance.base.NcmpRegistryPerfTestBase +import org.springframework.dao.DataAccessResourceFailureException import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS -import org.onap.cps.integration.performance.base.NcmpRegistryPerfTestBase - class CmHandleQueryPerfTest extends NcmpRegistryPerfTestBase { def objectUnderTest @@ -52,14 +51,13 @@ class CmHandleQueryPerfTest extends NcmpRegistryPerfTestBase { assert countDataNodesInTree(result) == 5 * 999 } - def 'Multiple get limitation: 32,764 (~ 2^15) xpaths.'() { + def 'Multiple get limit exceeded: 32,764 (~ 2^15) xpaths.'() { given: 'more than 32,764 xpaths)' - def xpaths = [] - (0..32_765).each { xpaths.add("/size/of/this/path/does/not/matter/for/limit[@id='" + it + "']") } - when: 'get single get is executed to get all the parent objects and their descendants' + def xpaths = (0..32_764).collect(i -> "/size/of/this/path/does/not/matter/for/limit[@id='" + i + "']") + when: 'single get is executed to get all the parent objects and their descendants' cpsDataService.getDataNodesForMultipleXpaths(NCMP_PERFORMANCE_TEST_DATASPACE, REGISTRY_ANCHOR, xpaths, INCLUDE_ALL_DESCENDANTS) - then: 'no exception is thrown (limit is not present in current implementation)' - noExceptionThrown() + then: 'an exception is thrown' + thrown(DataAccessResourceFailureException.class) } } |