diff options
author | 2025-03-05 13:48:20 +0000 | |
---|---|---|
committer | 2025-03-05 13:48:20 +0000 | |
commit | a2c05f69b9dacee2ce8c12bbb80174aa60116a2a (patch) | |
tree | 3937f0872243acfae50a3644607e54fbcbd2190f | |
parent | 597931a5aa14a542bf7f6971bb6db8dd026372dd (diff) | |
parent | a6e58e73589e12cd52992bdf6177971dc88dd4c5 (diff) |
Merge "Add APIs to control notification subscription"
15 files changed, 820 insertions, 19 deletions
diff --git a/cps-rest/docs/openapi/components.yml b/cps-rest/docs/openapi/components.yml index 1a7e4308d9..43a311872a 100644 --- a/cps-rest/docs/openapi/components.yml +++ b/cps-rest/docs/openapi/components.yml @@ -1,7 +1,7 @@ # ============LICENSE_START======================================================= # Copyright (c) 2021-2022 Bell Canada. # Modifications Copyright (C) 2021-2023 Nordix Foundation -# Modifications Copyright (C) 2022-2024 TechMahindra Ltd. +# Modifications Copyright (C) 2022-2025 TechMahindra Ltd. # Modifications Copyright (C) 2022 Deutsche Telekom AG # ================================================================================ # Licensed under the Apache License, Version 2.0 (the "License"); @@ -157,7 +157,12 @@ components: name: "Funny" target-data: name: "Comic" - + NotificationSubscriptionsDataSample: + value: + cps-notification-subscriptions:dataspaces: + dataspace: + - name: dataspace01 + - name: dataspace02 parameters: dataspaceNameInQuery: name: dataspace-name @@ -236,6 +241,19 @@ components: value: /shops/bookstore list attributes xpath: value: /shops/bookstore/categories[@code=1] + notificationSubscriptionXpathInQuery: + name: xpath + in: query + description: For more details on xpath, please refer https://docs.onap.org/projects/onap-cps/en/latest/xpath.html + required: true + schema: + type: string + default: /dataspaces + examples: + subscription by dataspace xpath: + value: /dataspaces/dataspace[@name='dataspace01'] + subscription by anchor xpath: + value: /dataspaces/dataspace[@name='dataspace01']/anchors/anchor[@name='anchor01'] requiredXpathInQuery: name: xpath in: query diff --git a/cps-rest/docs/openapi/cpsAdminV2.yml b/cps-rest/docs/openapi/cpsAdminV2.yml index e501ad8b15..af2572a1f0 100644 --- a/cps-rest/docs/openapi/cpsAdminV2.yml +++ b/cps-rest/docs/openapi/cpsAdminV2.yml @@ -1,5 +1,5 @@ # ============LICENSE_START======================================================= -# Copyright (C) 2022 TechMahindra Ltd. +# Copyright (C) 2022-2025 TechMahindra Ltd. # ================================================================================ # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -87,3 +87,75 @@ schemaSet: $ref: 'components.yml#/components/responses/Conflict' '500': $ref: 'components.yml#/components/responses/InternalServerError' + +notificationSubscription: + get: + description: Get cps notification subscription + tags: + - cps-admin + summary: Get cps notification subscription + operationId: getNotificationSubscription + parameters: + - $ref: 'components.yml#/components/parameters/notificationSubscriptionXpathInQuery' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: 'components.yml#/components/examples/NotificationSubscriptionsDataSample' + '400': + $ref: 'components.yml#/components/responses/BadRequest' + '403': + $ref: 'components.yml#/components/responses/Forbidden' + '409': + $ref: 'components.yml#/components/responses/Conflict' + '500': + $ref: 'components.yml#/components/responses/InternalServerError' + post: + description: Create cps notification subscription + tags: + - cps-admin + summary: Create cps notification subscription + operationId: createNotificationSubscription + parameters: + - $ref: 'components.yml#/components/parameters/notificationSubscriptionXpathInQuery' + requestBody: + required: true + content: + application/json: + schema: + type: object + examples: + dataSample: + $ref: 'components.yml#/components/examples/NotificationSubscriptionsDataSample' + responses: + '201': + $ref: 'components.yml#/components/responses/CreatedV2' + '400': + $ref: 'components.yml#/components/responses/BadRequest' + '403': + $ref: 'components.yml#/components/responses/Forbidden' + '409': + $ref: 'components.yml#/components/responses/Conflict' + '500': + $ref: 'components.yml#/components/responses/InternalServerError' + delete: + description: Delete cps notification subscription + tags: + - cps-admin + summary: Delete cps notification subscription + operationId: deleteNotificationSubscription + parameters: + - $ref: 'components.yml#/components/parameters/notificationSubscriptionXpathInQuery' + responses: + '204': + $ref: 'components.yml#/components/responses/NoContent' + '400': + $ref: 'components.yml#/components/responses/BadRequest' + '403': + $ref: 'components.yml#/components/responses/Forbidden' + '409': + $ref: 'components.yml#/components/responses/Conflict' + '500': + $ref: 'components.yml#/components/responses/InternalServerError'
\ No newline at end of file diff --git a/cps-rest/docs/openapi/openapi.yml b/cps-rest/docs/openapi/openapi.yml index c85bf7cac7..09c454b1da 100644 --- a/cps-rest/docs/openapi/openapi.yml +++ b/cps-rest/docs/openapi/openapi.yml @@ -2,7 +2,7 @@ # Copyright (C) 2021-2025 Nordix Foundation # Modifications Copyright (C) 2021 Pantheon.tech # Modifications Copyright (C) 2021 Bell Canada. -# Modifications Copyright (C) 2022-2024 TechMahindra Ltd. +# Modifications Copyright (C) 2022-2025 TechMahindra Ltd. # ================================================================================ # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -115,5 +115,8 @@ paths: /v2/dataspaces/{dataspace-name}/nodes/query: $ref: 'cpsQueryV2.yml#/nodesByDataspaceAndCpsPath' + /v2/notification-subscription: + $ref: 'cpsAdminV2.yml#/notificationSubscription' + security: - basicAuth: [] diff --git a/cps-rest/src/main/java/org/onap/cps/rest/controller/AdminRestController.java b/cps-rest/src/main/java/org/onap/cps/rest/controller/AdminRestController.java index 4c6bd6cdc5..01a9746af0 100755 --- a/cps-rest/src/main/java/org/onap/cps/rest/controller/AdminRestController.java +++ b/cps-rest/src/main/java/org/onap/cps/rest/controller/AdminRestController.java @@ -3,7 +3,7 @@ * Copyright (C) 2020-2025 Nordix Foundation * Modifications Copyright (C) 2020-2021 Bell Canada. * Modifications Copyright (C) 2021 Pantheon.tech - * Modifications Copyright (C) 2022 TechMahindra Ltd. + * Modifications Copyright (C) 2022-2025 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,11 +31,13 @@ import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.onap.cps.api.CpsAnchorService; import org.onap.cps.api.CpsDataspaceService; import org.onap.cps.api.CpsModuleService; +import org.onap.cps.api.CpsNotificationService; import org.onap.cps.api.model.Anchor; import org.onap.cps.api.model.Dataspace; import org.onap.cps.api.model.SchemaSet; @@ -43,6 +45,7 @@ import org.onap.cps.rest.api.CpsAdminApi; import org.onap.cps.rest.model.AnchorDetails; import org.onap.cps.rest.model.DataspaceDetails; import org.onap.cps.rest.model.SchemaSetDetails; +import org.onap.cps.utils.JsonObjectMapper; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RequestMapping; @@ -58,6 +61,8 @@ public class AdminRestController implements CpsAdminApi { private final CpsModuleService cpsModuleService; private final CpsRestInputMapper cpsRestInputMapper; private final CpsAnchorService cpsAnchorService; + private final CpsNotificationService cpsNotificationService; + private final JsonObjectMapper jsonObjectMapper; /** * Create a dataspace. @@ -280,4 +285,25 @@ public class AdminRestController implements CpsAdminApi { final DataspaceDetails dataspaceDetails = cpsRestInputMapper.toDataspaceDetails(dataspace); return new ResponseEntity<>(dataspaceDetails, HttpStatus.OK); } + + @Override + public ResponseEntity<Void> createNotificationSubscription(final String xpath, + final Object notificationSubscriptionAsJson) { + cpsNotificationService.createNotificationSubscription( + jsonObjectMapper.asJsonString(notificationSubscriptionAsJson), xpath); + return new ResponseEntity<>(HttpStatus.CREATED); + } + + @Override + public ResponseEntity<Void> deleteNotificationSubscription(final String xpath) { + cpsNotificationService.deleteNotificationSubscription(xpath); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + + @Override + public ResponseEntity<Object> getNotificationSubscription(final String xpath) { + final List<Map<String, Object>> dataMaps = cpsNotificationService.getNotificationSubscription(xpath); + return new ResponseEntity<>(jsonObjectMapper.asJsonString(dataMaps), HttpStatus.OK); + } + } diff --git a/cps-rest/src/test/groovy/org/onap/cps/rest/controller/AdminRestControllerSpec.groovy b/cps-rest/src/test/groovy/org/onap/cps/rest/controller/AdminRestControllerSpec.groovy index 0d189783fd..6d1ca40cd9 100755 --- a/cps-rest/src/test/groovy/org/onap/cps/rest/controller/AdminRestControllerSpec.groovy +++ b/cps-rest/src/test/groovy/org/onap/cps/rest/controller/AdminRestControllerSpec.groovy @@ -3,7 +3,7 @@ * Copyright (C) 2020-2021 Pantheon.tech * Modifications Copyright (C) 2020-2021 Bell Canada. * Modifications Copyright (C) 2021-2025 Nordix Foundation - * Modifications Copyright (C) 2022 TechMahindra Ltd. + * Modifications Copyright (C) 2022-2025 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,15 +23,26 @@ package org.onap.cps.rest.controller +import com.fasterxml.jackson.databind.ObjectMapper + +import static org.onap.cps.api.parameters.CascadeDeleteAllowed.CASCADE_DELETE_PROHIBITED +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put + import org.mapstruct.factory.Mappers import org.onap.cps.api.CpsAnchorService import org.onap.cps.api.CpsDataspaceService import org.onap.cps.api.CpsModuleService +import org.onap.cps.api.CpsNotificationService import org.onap.cps.api.exceptions.AlreadyDefinedException import org.onap.cps.api.exceptions.SchemaSetInUseException import org.onap.cps.api.model.Anchor import org.onap.cps.api.model.Dataspace import org.onap.cps.api.model.SchemaSet +import org.onap.cps.utils.JsonObjectMapper import org.spockframework.spring.SpringBean import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Value @@ -63,8 +74,15 @@ class AdminRestControllerSpec extends Specification { CpsAnchorService mockCpsAnchorService = Mock() @SpringBean + CpsNotificationService mockCpsNotificationService = Mock() + + @SpringBean CpsRestInputMapper cpsRestInputMapper = Mappers.getMapper(CpsRestInputMapper) + @SpringBean + JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(new ObjectMapper()) + + @Autowired MockMvc mvc @@ -393,6 +411,48 @@ class AdminRestControllerSpec extends Specification { response.status == HttpStatus.NO_CONTENT.value() } + def 'Add notification subscription'() { + given: 'an endpoint and its payload' + def notificationSubscriptionEndpoint = "$basePath/v2/notification-subscription" + def xpath = '/dataspaces' + def jsonPayload = '{"dataspace":[{"name":"ds01"}]}' + when: 'post request is performed' + def response = + mvc.perform( + post(notificationSubscriptionEndpoint) + .contentType(MediaType.APPLICATION_JSON) + .content(jsonPayload)) + .andReturn().response + then: 'notification service method is invoked with expected parameter' + 1 * mockCpsNotificationService.createNotificationSubscription(jsonPayload, xpath) + and: 'HTTP response code indicates success' + response.status == HttpStatus.CREATED.value() + } + + def 'delete notification subscription'() { + given: 'an endpoint and xpath' + def notificationSubscriptionEndpoint = "$basePath/v2/notification-subscription" + def xpath = '/dataspaces' + when: 'delete request is performed' + def response = mvc.perform(delete(notificationSubscriptionEndpoint).param('xpath', xpath)).andReturn().response + then: 'notification service method is invoked with expected parameter' + 1 * mockCpsNotificationService.deleteNotificationSubscription(xpath) + and: 'HTTP response code indicates success' + response.status == HttpStatus.NO_CONTENT.value() + } + + def 'Get notification subscription.'() { + given: 'an endpoint and xpath' + def notificationSubscriptionEndpoint = "$basePath/v2/notification-subscription" + def xpath = '/dataspaces' + when: 'get notification subscription is invoked' + def response = mvc.perform(get(notificationSubscriptionEndpoint).param('xpath', xpath)).andReturn().response + then: 'HTTP response code indicates success' + response.status == HttpStatus.OK.value() + and: 'notification service is called with proper parameters' + 1 * mockCpsNotificationService.getNotificationSubscription(xpath) + } + def createMultipartFile(filename, content) { return new MockMultipartFile("file", filename, "text/plain", content.getBytes()) } diff --git a/cps-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy b/cps-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy index f0fc4cca62..4e1d27cda2 100644 --- a/cps-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy +++ b/cps-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy @@ -3,7 +3,7 @@ * Copyright (C) 2020 Pantheon.tech * Modifications Copyright (C) 2021-2023 Nordix Foundation * Modifications Copyright (C) 2021 Bell Canada. - * Modifications Copyright (C) 2022 TechMahindra Ltd. + * Modifications Copyright (C) 2022-2025 TechMahindra Ltd. * Modifications Copyright (C) 2022 Deutsche Telekom AG * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); @@ -30,6 +30,7 @@ import org.onap.cps.api.CpsDataspaceService import org.onap.cps.api.CpsAnchorService import org.onap.cps.api.CpsDataService import org.onap.cps.api.CpsModuleService +import org.onap.cps.api.CpsNotificationService import org.onap.cps.api.CpsQueryService import org.onap.cps.rest.controller.CpsRestInputMapper import org.onap.cps.api.exceptions.AlreadyDefinedException @@ -87,6 +88,9 @@ class CpsRestExceptionHandlerSpec extends Specification { @SpringBean PrefixResolver prefixResolver = Mock() + @SpringBean + CpsNotificationService mockCpsNotificationService = Mock() + @Autowired MockMvc mvc diff --git a/cps-service/src/main/java/org/onap/cps/api/CpsNotificationService.java b/cps-service/src/main/java/org/onap/cps/api/CpsNotificationService.java new file mode 100644 index 0000000000..ae437753c0 --- /dev/null +++ b/cps-service/src/main/java/org/onap/cps/api/CpsNotificationService.java @@ -0,0 +1,35 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2025 TechMahindra Ltd. + * ================================================================================ + * 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.api; + +import java.util.List; +import java.util.Map; + +public interface CpsNotificationService { + + void createNotificationSubscription(String notificationSubscriptionAsJson, String xpath); + + void deleteNotificationSubscription(String xpath); + + boolean isNotificationEnabled(String dataspaceName, String anchorName); + + List<Map<String, Object>> getNotificationSubscription(String xpath); +} diff --git a/cps-service/src/main/java/org/onap/cps/events/CpsDataUpdateEventsService.java b/cps-service/src/main/java/org/onap/cps/events/CpsDataUpdateEventsService.java index f1b5ff8d10..50441adac5 100644 --- a/cps-service/src/main/java/org/onap/cps/events/CpsDataUpdateEventsService.java +++ b/cps-service/src/main/java/org/onap/cps/events/CpsDataUpdateEventsService.java @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2024 TechMahindra Ltd. + * Copyright (C) 2024-2025 TechMahindra Ltd. * Copyright (C) 2024 Nordix Foundation. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); @@ -28,6 +28,7 @@ import java.util.HashMap; import java.util.Map; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.onap.cps.api.CpsNotificationService; import org.onap.cps.api.model.Anchor; import org.onap.cps.events.model.CpsDataUpdatedEvent; import org.onap.cps.events.model.Data; @@ -43,6 +44,8 @@ public class CpsDataUpdateEventsService { private final EventsPublisher<CpsDataUpdatedEvent> eventsPublisher; + private final CpsNotificationService cpsNotificationService; + @Value("${app.cps.data-updated.topic:cps-data-updated-events}") private String topicName; @@ -63,7 +66,7 @@ public class CpsDataUpdateEventsService { @Timed(value = "cps.dataupdate.events.publish", description = "Time taken to publish Data Update event") public void publishCpsDataUpdateEvent(final Anchor anchor, final String xpath, final Operation operation, final OffsetDateTime observedTimestamp) { - if (notificationsEnabled && cpsChangeEventNotificationsEnabled) { + if (notificationsEnabled && cpsChangeEventNotificationsEnabled && isNotificationEnabledForAnchor(anchor)) { final CpsDataUpdatedEvent cpsDataUpdatedEvent = createCpsDataUpdatedEvent(anchor, observedTimestamp, xpath, operation); final String updateEventId = anchor.getDataspaceName() + ":" + anchor.getName(); @@ -78,6 +81,10 @@ public class CpsDataUpdateEventsService { } } + private boolean isNotificationEnabledForAnchor(final Anchor anchor) { + return cpsNotificationService.isNotificationEnabled(anchor.getDataspaceName(), anchor.getName()); + } + private CpsDataUpdatedEvent createCpsDataUpdatedEvent(final Anchor anchor, final OffsetDateTime observedTimestamp, final String xpath, final Operation rootNodeOperation) { diff --git a/cps-service/src/main/java/org/onap/cps/impl/CpsNotificationServiceImpl.java b/cps-service/src/main/java/org/onap/cps/impl/CpsNotificationServiceImpl.java new file mode 100644 index 0000000000..5030ad04c6 --- /dev/null +++ b/cps-service/src/main/java/org/onap/cps/impl/CpsNotificationServiceImpl.java @@ -0,0 +1,140 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2025 TechMahindra Ltd. + * ================================================================================ + * 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.impl; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.onap.cps.api.CpsAnchorService; +import org.onap.cps.api.CpsNotificationService; +import org.onap.cps.api.exceptions.DataNodeNotFoundException; +import org.onap.cps.api.model.Anchor; +import org.onap.cps.api.model.DataNode; +import org.onap.cps.api.parameters.FetchDescendantsOption; +import org.onap.cps.cpspath.parser.CpsPathUtil; +import org.onap.cps.spi.CpsDataPersistenceService; +import org.onap.cps.utils.ContentType; +import org.onap.cps.utils.DataMapUtils; +import org.onap.cps.utils.PrefixResolver; +import org.onap.cps.utils.YangParser; +import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode; +import org.springframework.stereotype.Service; + +@Service +@Slf4j +@RequiredArgsConstructor +public class CpsNotificationServiceImpl implements CpsNotificationService { + + private final CpsAnchorService cpsAnchorService; + + private final CpsDataPersistenceService cpsDataPersistenceService; + + private final YangParser yangParser; + + private final PrefixResolver prefixResolver; + + private static final String ADMIN_DATASPACE = "CPS-Admin"; + private static final String ANCHOR_NAME = "cps-notification-subscriptions"; + private static final String DATASPACE_SUBSCRIPTION_XPATH_FORMAT = "/dataspaces/dataspace[@name='%s']"; + private static final String ANCHORS_SUBSCRIPTION_XPATH_FORMAT = "/dataspaces/dataspace[@name='%s']/anchors"; + private static final String ANCHOR_SUBSCRIPTION_XPATH_FORMAT = + "/dataspaces/dataspace[@name='%s']/anchors/anchor[@name='%s']"; + + @Override + public void createNotificationSubscription(final String notificationSubscriptionAsJson, final String xpath) { + + final Anchor anchor = cpsAnchorService.getAnchor(ADMIN_DATASPACE, ANCHOR_NAME); + final Collection<DataNode> dataNodes = + buildDataNodesWithParentNodeXpath(anchor, xpath, notificationSubscriptionAsJson, ContentType.JSON); + cpsDataPersistenceService.addListElements(ADMIN_DATASPACE, ANCHOR_NAME, xpath, + dataNodes); + } + + @Override + public void deleteNotificationSubscription(final String xpath) { + cpsDataPersistenceService.deleteDataNode(ADMIN_DATASPACE, ANCHOR_NAME, xpath); + } + + @Override + public List<Map<String, Object>> getNotificationSubscription(final String xpath) { + final Collection<DataNode> dataNodes = + cpsDataPersistenceService.getDataNodes(ADMIN_DATASPACE, ANCHOR_NAME, xpath, + FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS); + final List<Map<String, Object>> dataMaps = new ArrayList<>(dataNodes.size()); + final Anchor anchor = cpsAnchorService.getAnchor(ADMIN_DATASPACE, ANCHOR_NAME); + for (final DataNode dataNode: dataNodes) { + final String prefix = prefixResolver.getPrefix(anchor, dataNode.getXpath()); + final Map<String, Object> dataMap = DataMapUtils.toDataMapWithIdentifier(dataNode, prefix); + dataMaps.add(dataMap); + } + return dataMaps; + } + + @Override + public boolean isNotificationEnabled(final String dataspaceName, final String anchorName) { + return (isNotificationEnabledForAnchor(dataspaceName, anchorName) + || notificationEnabledForAllAnchors(dataspaceName)); + } + + private boolean isNotificationEnabledForAnchor(final String dataspaceName, final String anchorName) { + final String xpath = String.format(ANCHOR_SUBSCRIPTION_XPATH_FORMAT, dataspaceName, anchorName); + return isNotificationEnabledForXpath(xpath); + } + + private boolean isNotificationEnabledForXpath(final String xpath) { + try { + cpsDataPersistenceService.getDataNodes(ADMIN_DATASPACE, ANCHOR_NAME, xpath, + FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS); + } catch (final DataNodeNotFoundException e) { + return false; + } + return true; + } + + private boolean notificationEnabledForAllAnchors(final String dataspaceName) { + final String dataspaceSubscriptionXpath = String.format(DATASPACE_SUBSCRIPTION_XPATH_FORMAT, dataspaceName); + return (isNotificationEnabledForXpath(dataspaceSubscriptionXpath) + && noIndividualAnchorEnabledInDataspace(dataspaceName)); + } + + private boolean noIndividualAnchorEnabledInDataspace(final String dataspaceName) { + final String xpathForAnchors = String.format(ANCHORS_SUBSCRIPTION_XPATH_FORMAT, dataspaceName); + return !isNotificationEnabledForXpath(xpathForAnchors); + } + + + private Collection<DataNode> buildDataNodesWithParentNodeXpath(final Anchor anchor, final String parentNodeXpath, + final String nodeData, + final ContentType contentType) { + + final String normalizedParentNodeXpath = CpsPathUtil.getNormalizedXpath(parentNodeXpath); + final ContainerNode containerNode = + yangParser.parseData(contentType, nodeData, anchor, normalizedParentNodeXpath); + final Collection<DataNode> dataNodes = new DataNodeBuilder() + .withParentNodeXpath(normalizedParentNodeXpath) + .withContainerNode(containerNode) + .buildCollection(); + return dataNodes; + } +}
\ No newline at end of file diff --git a/cps-service/src/main/java/org/onap/cps/init/CpsNotificationSubscriptionModelLoader.java b/cps-service/src/main/java/org/onap/cps/init/CpsNotificationSubscriptionModelLoader.java index 0b7d1609ff..bf60f8d49a 100644 --- a/cps-service/src/main/java/org/onap/cps/init/CpsNotificationSubscriptionModelLoader.java +++ b/cps-service/src/main/java/org/onap/cps/init/CpsNotificationSubscriptionModelLoader.java @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2024 TechMahindra Ltd. + * Copyright (C) 2024-2025 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/cps-service/src/test/groovy/org/onap/cps/events/CpsDataUpdateEventsServiceSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/events/CpsDataUpdateEventsServiceSpec.groovy index 5dee8fc28b..6d9ff12060 100644 --- a/cps-service/src/test/groovy/org/onap/cps/events/CpsDataUpdateEventsServiceSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/events/CpsDataUpdateEventsServiceSpec.groovy @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2024 TechMahindra Ltd. + * Copyright (C) 2024-2025 TechMahindra Ltd. * Copyright (C) 2024 Nordix Foundation. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,6 +25,7 @@ import static org.onap.cps.events.model.Data.Operation.CREATE import static org.onap.cps.events.model.Data.Operation.DELETE import static org.onap.cps.events.model.Data.Operation.UPDATE +import org.onap.cps.api.CpsNotificationService import com.fasterxml.jackson.databind.ObjectMapper import io.cloudevents.CloudEvent import io.cloudevents.core.CloudEventUtils @@ -41,8 +42,13 @@ import java.time.OffsetDateTime class CpsDataUpdateEventsServiceSpec extends Specification { def mockEventsPublisher = Mock(EventsPublisher) def objectMapper = new ObjectMapper(); + def mockCpsNotificationService = Mock(CpsNotificationService) - def objectUnderTest = new CpsDataUpdateEventsService(mockEventsPublisher) + def objectUnderTest = new CpsDataUpdateEventsService(mockEventsPublisher, mockCpsNotificationService) + + def setup() { + mockCpsNotificationService.isNotificationEnabled('dataspace01', 'anchor01') >> true + } def 'Create and Publish cps update event where events are #scenario'() { given: 'an anchor, operation and observed timestamp' diff --git a/cps-service/src/test/groovy/org/onap/cps/impl/CpsNotificationServiceImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/impl/CpsNotificationServiceImplSpec.groovy new file mode 100644 index 0000000000..0f563272d1 --- /dev/null +++ b/cps-service/src/test/groovy/org/onap/cps/impl/CpsNotificationServiceImplSpec.groovy @@ -0,0 +1,166 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2025 TechMahindra Ltd. + * ================================================================================ + * 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.impl + +import com.fasterxml.jackson.databind.ObjectMapper +import org.onap.cps.api.CpsAnchorService +import org.onap.cps.api.exceptions.DataNodeNotFoundException +import org.onap.cps.api.exceptions.DataValidationException +import org.onap.cps.api.model.Anchor +import org.onap.cps.api.parameters.FetchDescendantsOption; +import org.onap.cps.spi.CpsDataPersistenceService +import org.onap.cps.utils.JsonObjectMapper +import org.onap.cps.utils.PrefixResolver +import org.onap.cps.utils.YangParser +import org.onap.cps.TestUtils +import org.onap.cps.utils.YangParserHelper +import org.onap.cps.yang.TimedYangTextSchemaSourceSetBuilder +import org.onap.cps.yang.YangTextSchemaSourceSet +import org.onap.cps.yang.YangTextSchemaSourceSetBuilder +import org.springframework.test.context.ContextConfiguration + +import spock.lang.Specification + +@ContextConfiguration(classes = [ObjectMapper, JsonObjectMapper]) +class CpsNotificationServiceImplSpec extends Specification { + + def dataspaceName = 'CPS-Admin' + def anchorName = 'cps-notification-subscriptions' + def schemaSetName = 'cps-notification-subscriptions' + def anchor = new Anchor(anchorName, dataspaceName, schemaSetName) + + def mockCpsDataPersistenceService = Mock(CpsDataPersistenceService) + def mockCpsAnchorService = Mock(CpsAnchorService) + def mockYangTextSchemaSourceSetCache = Mock(YangTextSchemaSourceSetCache) + def mockTimedYangTextSchemaSourceSetBuilder = Mock(TimedYangTextSchemaSourceSetBuilder) + def yangParser = new YangParser(new YangParserHelper(), mockYangTextSchemaSourceSetCache, mockTimedYangTextSchemaSourceSetBuilder) + def mockPrefixResolver = Mock(PrefixResolver) + def objectUnderTest = new CpsNotificationServiceImpl(mockCpsAnchorService, mockCpsDataPersistenceService, yangParser, mockPrefixResolver) + + def 'add notification subscription for list of dataspaces'() { + given: 'details for notification subscription and subscription root node xpath' + def notificationSubscriptionAsjson = '{"dataspace":[{"name":"ds01"},{"name":"ds02"}]}' + def xpath = '/dataspaces' + and: 'schema set for given anchor and dataspace references notification subscription model' + setupSchemaSetMocks('cps-notification-subscriptions@2024-07-03.yang') + and: 'anchor is provided' + mockCpsAnchorService.getAnchor(dataspaceName, anchorName) >> anchor + when: 'create notification subscription is called' + objectUnderTest.createNotificationSubscription(notificationSubscriptionAsjson, xpath) + then: 'the persistence service is called once with the correct parameters' + 1 * mockCpsDataPersistenceService.addListElements('CPS-Admin', 'cps-notification-subscriptions', xpath, { dataNodeCollection -> + { + assert dataNodeCollection.size() == 2 + assert dataNodeCollection.collect { it.getXpath() } + .containsAll(['/dataspaces/dataspace[@name=\'ds01\']', '/dataspaces/dataspace[@name=\'ds02\']']) + } + }) + } + + def 'add notification subscription fails with exception'() { + given: 'details for notification subscription' + def jsonData = '{"dataspace":[{"name":"ds01"},{"name":"ds02"}]}' + and: 'schema set for given anchor and dataspace references invalid data model' + setupSchemaSetMocks('test-tree.yang') + and: 'anchor is provided' + mockCpsAnchorService.getAnchor(dataspaceName, anchorName) >> anchor + when: 'create notification subscription is called' + objectUnderTest.createNotificationSubscription(jsonData, '/somepath') + then: 'data validation exception is thrown ' + thrown(DataValidationException) + } + + def 'delete notification subscription for given xpath'() { + given: 'details for notification subscription' + def xpath = '/some/path' + when: 'delete notification subscription is called' + objectUnderTest.deleteNotificationSubscription(xpath) + then: 'the persistence service is called once with the correct parameters' + 1 * mockCpsDataPersistenceService.deleteDataNode(dataspaceName, anchorName, xpath) + } + + def 'get notification subscription for given xpath'() { + given: 'details for notification subscription' + def xpath = '/some/path' + and: 'persistence service returns data nodes for subscribed data' + mockCpsDataPersistenceService.getDataNodes(dataspaceName, anchorName, + xpath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> + [new DataNodeBuilder().withXpath('/some/path').withLeaves([leaf: 'dataspace', leafList: ['ds01', 'ds02']]).build()] + when: 'delete notification subscription is called' + def result = objectUnderTest.getNotificationSubscription(xpath) + then: 'the result is a json representation of the data node(s) returned by the data persistence service' + assert result.get(0).toString() == '{path={leaf=dataspace, leafList=[ds01, ds02]}}' + } + + def 'is notification enabled for given anchor'() { + given: 'data nodes available for given anchor' + mockCpsDataPersistenceService.getDataNodes(dataspaceName, anchorName, "/dataspaces", FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> + [new DataNodeBuilder().withXpath('/xpath-1').build()] + when: 'is notification enabled is called' + boolean isNotificationEnabled = objectUnderTest.isNotificationEnabled(dataspaceName, anchorName) + then: 'the notification is enabled' + assert isNotificationEnabled + } + + def 'is notification disabled for given anchor'() { + given: 'data nodes not available for given anchor' + mockCpsDataPersistenceService.getDataNodes(dataspaceName, anchorName, "/dataspaces/dataspace[@name='ds01']/anchors/anchor[@name='anchor-01']", FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> + { throw new DataNodeNotFoundException(dataspaceName, anchorName) } + when: 'is notification enabled is called' + boolean isNotificationEnabled = objectUnderTest.isNotificationEnabled('ds01', 'anchor-01') + then: 'the notification is disabled' + assert !isNotificationEnabled + } + + def 'is notification enabled for all anchors in a dataspace'() { + given: 'data nodes available for given dataspace' + mockCpsDataPersistenceService.getDataNodes(dataspaceName, anchorName, "/dataspaces/dataspace[@name='ds01']", FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> + [new DataNodeBuilder().withXpath('/xpath-1').build()] + and: 'data nodes not available for any specific anchor' + mockCpsDataPersistenceService.getDataNodes(dataspaceName, anchorName, "/dataspaces/dataspace[@name='ds01']/anchors", FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> + { throw new DataNodeNotFoundException(dataspaceName, anchorName) } + when: 'is notification enabled is called' + boolean isNotificationEnabled = objectUnderTest.notificationEnabledForAllAnchors('ds01') + then: 'the notification is enabled' + assert isNotificationEnabled + } + + def 'is notification disabled for all anchors in a dataspace'() { + given: 'data nodes available for given dataspace' + mockCpsDataPersistenceService.getDataNodes(dataspaceName, anchorName, "/dataspaces/dataspace[@name='ds01']", FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> + [new DataNodeBuilder().withXpath('/xpath-1').build()] + and: 'data nodes also available for any specific anchor' + mockCpsDataPersistenceService.getDataNodes(dataspaceName, anchorName, "/dataspaces/dataspace[@name='ds01']/anchors", FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> + [new DataNodeBuilder().withXpath('/xpath-1').build()] + when: 'is notification enabled is called' + boolean isNotificationEnabled = objectUnderTest.notificationEnabledForAllAnchors('ds01') + then: 'the notification is disabled' + assert !isNotificationEnabled + } + + def setupSchemaSetMocks(String... yangResources) { + def mockYangTextSchemaSourceSet = Mock(YangTextSchemaSourceSet) + mockYangTextSchemaSourceSetCache.get(dataspaceName, schemaSetName) >> mockYangTextSchemaSourceSet + def yangResourceNameToContent = TestUtils.getYangResourcesAsMap(yangResources) + def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext() + mockYangTextSchemaSourceSet.getSchemaContext() >> schemaContext + } +} diff --git a/cps-service/src/test/groovy/org/onap/cps/init/CpsNotificationSubscriptionModelLoaderSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/init/CpsNotificationSubscriptionModelLoaderSpec.groovy index 0d515f90ac..1e2dc54424 100644 --- a/cps-service/src/test/groovy/org/onap/cps/init/CpsNotificationSubscriptionModelLoaderSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/init/CpsNotificationSubscriptionModelLoaderSpec.groovy @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2024 TechMahindra Ltd. + * Copyright (C) 2024-2025 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -78,10 +78,4 @@ class CpsNotificationSubscriptionModelLoaderSpec extends Specification { and: 'the data service to create a top level datanode is called once' 1 * mockCpsDataService.saveData(CPS_DATASPACE_NAME, ANCHOR_NAME, '{"dataspaces":{}}', _) } - - private void assertLogContains(String message) { - def logs = loggingListAppender.list.toString() - assert logs.contains(message) - } - } diff --git a/cps-service/src/test/resources/cps-notification-subscriptions@2024-07-03.yang b/cps-service/src/test/resources/cps-notification-subscriptions@2024-07-03.yang new file mode 100644 index 0000000000..1cab7923ea --- /dev/null +++ b/cps-service/src/test/resources/cps-notification-subscriptions@2024-07-03.yang @@ -0,0 +1,48 @@ +module cps-notification-subscriptions { + yang-version 1.1; + namespace "org:onap:cps"; + + prefix cps-notification-subscriptions; + + revision "2024-08-05" { + description + "First release of cps notification subscriptions model"; + } + container dataspaces { + + list dataspace { + key "name"; + + leaf name { + type string; + } + + container anchors { + + list anchor { + key "name"; + + leaf name { + type string; + } + + container xpaths { + + list xpath { + key "path"; + leaf path { + type string; + } + } + } + } + } + leaf-list subscriptionIds { + type string; + } + leaf topic { + type string; + } + } + } +}
\ No newline at end of file diff --git a/docs/api/swagger/cps/openapi.yaml b/docs/api/swagger/cps/openapi.yaml index f6baadc8ea..154548797b 100644 --- a/docs/api/swagger/cps/openapi.yaml +++ b/docs/api/swagger/cps/openapi.yaml @@ -2562,6 +2562,208 @@ paths: tags: - cps-query x-codegen-request-body-name: xpath + /v2/notification-subscription: + delete: + description: Delete cps notification subscription + operationId: deleteNotificationSubscription + parameters: + - description: "For more details on xpath, please refer https://docs.onap.org/projects/onap-cps/en/latest/xpath.html" + examples: + subscription by dataspace xpath: + value: "/dataspaces/dataspace[@name='dataspace01']" + subscription by anchor xpath: + value: "/dataspaces/dataspace[@name='dataspace01']/anchors/anchor[@name='anchor01']" + in: query + name: xpath + required: true + schema: + default: /dataspaces + type: string + responses: + "204": + content: {} + description: No Content + "400": + content: + application/json: + example: + status: 400 + message: Bad Request + details: The provided request is not valid + schema: + $ref: '#/components/schemas/ErrorMessage' + description: Bad Request + "403": + content: + application/json: + example: + status: 403 + message: Request Forbidden + details: This request is forbidden + schema: + $ref: '#/components/schemas/ErrorMessage' + description: Forbidden + "409": + content: + application/json: + example: + status: 409 + message: Conflicting request + details: The request cannot be processed as the resource is in use. + schema: + $ref: '#/components/schemas/ErrorMessage' + description: Conflict + "500": + content: + application/json: + example: + status: 500 + message: Internal Server Error + details: Internal Server Error occurred + schema: + $ref: '#/components/schemas/ErrorMessage' + description: Internal Server Error + summary: Delete cps notification subscription + tags: + - cps-admin + get: + description: Get cps notification subscription + operationId: getNotificationSubscription + parameters: + - description: "For more details on xpath, please refer https://docs.onap.org/projects/onap-cps/en/latest/xpath.html" + examples: + subscription by dataspace xpath: + value: "/dataspaces/dataspace[@name='dataspace01']" + subscription by anchor xpath: + value: "/dataspaces/dataspace[@name='dataspace01']/anchors/anchor[@name='anchor01']" + in: query + name: xpath + required: true + schema: + default: /dataspaces + type: string + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/NotificationSubscriptionsDataSample' + description: OK + "400": + content: + application/json: + example: + status: 400 + message: Bad Request + details: The provided request is not valid + schema: + $ref: '#/components/schemas/ErrorMessage' + description: Bad Request + "403": + content: + application/json: + example: + status: 403 + message: Request Forbidden + details: This request is forbidden + schema: + $ref: '#/components/schemas/ErrorMessage' + description: Forbidden + "409": + content: + application/json: + example: + status: 409 + message: Conflicting request + details: The request cannot be processed as the resource is in use. + schema: + $ref: '#/components/schemas/ErrorMessage' + description: Conflict + "500": + content: + application/json: + example: + status: 500 + message: Internal Server Error + details: Internal Server Error occurred + schema: + $ref: '#/components/schemas/ErrorMessage' + description: Internal Server Error + summary: Get cps notification subscription + tags: + - cps-admin + post: + description: Create cps notification subscription + operationId: createNotificationSubscription + parameters: + - description: "For more details on xpath, please refer https://docs.onap.org/projects/onap-cps/en/latest/xpath.html" + examples: + subscription by dataspace xpath: + value: "/dataspaces/dataspace[@name='dataspace01']" + subscription by anchor xpath: + value: "/dataspaces/dataspace[@name='dataspace01']/anchors/anchor[@name='anchor01']" + in: query + name: xpath + required: true + schema: + default: /dataspaces + type: string + requestBody: + content: + application/json: + examples: + dataSample: + $ref: '#/components/examples/NotificationSubscriptionsDataSample' + value: null + schema: + type: object + required: true + responses: + "201": + description: Created without response body + "400": + content: + application/json: + example: + status: 400 + message: Bad Request + details: The provided request is not valid + schema: + $ref: '#/components/schemas/ErrorMessage' + description: Bad Request + "403": + content: + application/json: + example: + status: 403 + message: Request Forbidden + details: This request is forbidden + schema: + $ref: '#/components/schemas/ErrorMessage' + description: Forbidden + "409": + content: + application/json: + example: + status: 409 + message: Conflicting request + details: The request cannot be processed as the resource is in use. + schema: + $ref: '#/components/schemas/ErrorMessage' + description: Conflict + "500": + content: + application/json: + example: + status: 500 + message: Internal Server Error + details: Internal Server Error occurred + schema: + $ref: '#/components/schemas/ErrorMessage' + description: Internal Server Error + summary: Create cps notification subscription + tags: + - cps-admin components: examples: dataSample: @@ -2615,6 +2817,12 @@ components: name: SciFi - code: 2 name: kids + NotificationSubscriptionsDataSample: + value: + cps-notification-subscriptions:dataspaces: + dataspace: + - name: dataspace01 + - name: dataspace02 parameters: dataspaceNameInQuery: description: dataspace-name @@ -2795,6 +3003,19 @@ components: schema: example: 10 type: integer + notificationSubscriptionXpathInQuery: + description: "For more details on xpath, please refer https://docs.onap.org/projects/onap-cps/en/latest/xpath.html" + examples: + subscription by dataspace xpath: + value: "/dataspaces/dataspace[@name='dataspace01']" + subscription by anchor xpath: + value: "/dataspaces/dataspace[@name='dataspace01']/anchors/anchor[@name='anchor01']" + in: query + name: xpath + required: true + schema: + default: /dataspaces + type: string responses: Created: content: @@ -2956,6 +3177,7 @@ components: type: string title: Module reference object type: object + NotificationSubscriptionsDataSample: {} getDeltaByDataspaceAnchorAndPayload_request: properties: json: |