diff options
9 files changed, 171 insertions, 43 deletions
diff --git a/cps-application/src/main/resources/application.yml b/cps-application/src/main/resources/application.yml index fcd91c26d7..e185bf23f7 100644 --- a/cps-application/src/main/resources/application.yml +++ b/cps-application/src/main/resources/application.yml @@ -72,12 +72,14 @@ notification: data-updated:
enabled: false
topic: ${CPS_CHANGE_EVENT_TOPIC:cps.cfg-state-events}
+ filters:
+ enabled-dataspaces: ${DATASPACE_FILTER_PATTERNS:""}
springdoc:
swagger-ui:
url: /openapi.yml
path: /swagger-ui/index.html
-
+
security:
# comma-separated uri patterns which do not require authorization
permit-uri: /manage/**,/swagger-ui/**,/swagger-resources/**,/v3/api-docs
diff --git a/cps-rest/src/main/java/org/onap/cps/rest/controller/DataRestController.java b/cps-rest/src/main/java/org/onap/cps/rest/controller/DataRestController.java index bad66dd4e8..5c79472a4c 100755 --- a/cps-rest/src/main/java/org/onap/cps/rest/controller/DataRestController.java +++ b/cps-rest/src/main/java/org/onap/cps/rest/controller/DataRestController.java @@ -96,6 +96,13 @@ public class DataRestController implements CpsDataApi { return new ResponseEntity<>(HttpStatus.OK); } + @Override + public ResponseEntity<Void> deleteListNodeElements(final String dataspaceName, final String anchorName, + final String listNodeXpath) { + cpsDataService.deleteListNodeData(dataspaceName, anchorName, listNodeXpath); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + private static boolean isRootXpath(final String xpath) { return ROOT_XPATH.equals(xpath); } diff --git a/cps-rest/src/main/resources/static/cpsData.yml b/cps-rest/src/main/resources/static/cpsData.yml index 7e9f71d62b..9c4f3334e1 100644 --- a/cps-rest/src/main/resources/static/cpsData.yml +++ b/cps-rest/src/main/resources/static/cpsData.yml @@ -97,6 +97,26 @@ listNodeByDataspaceAndAnchor: '403': $ref: 'components.yml#/components/responses/Forbidden' + delete: + description: Delete list-node child elements under existing node for a given anchor and dataspace + tags: + - cps-data + summary: Delete list-node child element(s) under existing parent node + operationId: deleteListNodeElements + parameters: + - $ref: 'components.yml#/components/parameters/dataspaceNameInPath' + - $ref: 'components.yml#/components/parameters/anchorNameInPath' + - $ref: 'components.yml#/components/parameters/requiredXpathInQuery' + responses: + '204': + $ref: 'components.yml#/components/responses/NoContent' + '400': + $ref: 'components.yml#/components/responses/BadRequest' + '401': + $ref: 'components.yml#/components/responses/Unauthorized' + '403': + $ref: 'components.yml#/components/responses/Forbidden' + nodesByDataspaceAndAnchor: post: description: Create a node for a given anchor and dataspace diff --git a/cps-rest/src/test/groovy/org/onap/cps/rest/controller/DataRestControllerSpec.groovy b/cps-rest/src/test/groovy/org/onap/cps/rest/controller/DataRestControllerSpec.groovy index 8675f42a51..d3d42e3065 100755 --- a/cps-rest/src/test/groovy/org/onap/cps/rest/controller/DataRestControllerSpec.groovy +++ b/cps-rest/src/test/groovy/org/onap/cps/rest/controller/DataRestControllerSpec.groovy @@ -24,6 +24,7 @@ package org.onap.cps.rest.controller import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post @@ -235,4 +236,18 @@ class DataRestControllerSpec extends Specification { then: 'the java API was called with the correct parameters' 1 * mockCpsDataService.replaceListNodeData(dataspaceName, anchorName, parentNodeXpath, jsonData) } + + def 'Delete list node child elements.'() { + given: 'list node xpath' + def listNodeXpath = 'list node xpath' + when: 'delete is invoked list-node endpoint' + def response = mvc.perform( + delete("$dataNodeBaseEndpoint/anchors/$anchorName/list-node") + .param('xpath', listNodeXpath) + ).andReturn().response + then: 'a success response is returned' + response.status == HttpStatus.NO_CONTENT.value() + then: 'the java API was called with the correct parameters' + 1 * mockCpsDataService.deleteListNodeData(dataspaceName, anchorName, listNodeXpath) + } } diff --git a/cps-service/pom.xml b/cps-service/pom.xml index 6856f7c671..c69ead09e9 100644 --- a/cps-service/pom.xml +++ b/cps-service/pom.xml @@ -91,6 +91,10 @@ <artifactId>spring-context</artifactId>
</dependency>
<dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-validation</artifactId>
+ </dependency>
+ <dependency>
<!-- For parsing JSON object -->
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
diff --git a/cps-service/src/main/java/org/onap/cps/notification/NotificationProperties.java b/cps-service/src/main/java/org/onap/cps/notification/NotificationProperties.java new file mode 100644 index 0000000000..eb75e3f75e --- /dev/null +++ b/cps-service/src/main/java/org/onap/cps/notification/NotificationProperties.java @@ -0,0 +1,41 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2021 Bell Canada. All rights reserved. + * ================================================================================ + * 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.notification; + +import java.util.Collections; +import java.util.Map; +import javax.validation.constraints.NotNull; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import org.springframework.validation.annotation.Validated; + +@ConfigurationProperties(prefix = "notification.data-updated") +@Component +@Data +@Validated +public class NotificationProperties { + + @NotNull + private String topic; + private Map<String, String> filters = Collections.emptyMap(); + @NotNull + private boolean enabled = false; +} 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 67fc54ba2b..9cb2c52e01 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 @@ -18,39 +18,56 @@ package org.onap.cps.notification; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.regex.Pattern; +import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @Service @Slf4j public class NotificationService { - private boolean dataUpdatedEventNotificationEnabled; + private NotificationProperties notificationProperties; private NotificationPublisher notificationPublisher; private CpsDataUpdatedEventFactory cpsDataUpdatedEventFactory; private NotificationErrorHandler notificationErrorHandler; + private List<Pattern> dataspacePatterns; /** * Create an instance of Notification Subscriber. * - * @param dataUpdatedEventNotificationEnabled notification can be enabled by setting - * 'notification.data-updated.enabled=true' in application properties - * @param notificationPublisher notification Publisher - * @param cpsDataUpdatedEventFactory to create CPSDataUpdatedEvent - * @param notificationErrorHandler error handler + * @param notificationProperties properties for notification + * @param notificationPublisher notification Publisher + * @param cpsDataUpdatedEventFactory to create CPSDataUpdatedEvent + * @param notificationErrorHandler error handler */ @Autowired public NotificationService( - @Value("${notification.data-updated.enabled}") final boolean dataUpdatedEventNotificationEnabled, + final NotificationProperties notificationProperties, final NotificationPublisher notificationPublisher, final CpsDataUpdatedEventFactory cpsDataUpdatedEventFactory, final NotificationErrorHandler notificationErrorHandler) { - this.dataUpdatedEventNotificationEnabled = dataUpdatedEventNotificationEnabled; + this.notificationProperties = notificationProperties; this.notificationPublisher = notificationPublisher; this.cpsDataUpdatedEventFactory = cpsDataUpdatedEventFactory; this.notificationErrorHandler = notificationErrorHandler; + this.dataspacePatterns = getDataspaceFilterPatterns(notificationProperties); + } + + private List<Pattern> getDataspaceFilterPatterns(final NotificationProperties notificationProperties) { + if (notificationProperties.isEnabled()) { + return Arrays.stream(notificationProperties.getFilters() + .getOrDefault("enabled-dataspaces", "") + .split(",")) + .map(filterPattern -> Pattern.compile(filterPattern, Pattern.CASE_INSENSITIVE)) + .collect(Collectors.toList()); + } else { + return Collections.emptyList(); + } } /** @@ -62,7 +79,7 @@ public class NotificationService { public void processDataUpdatedEvent(final String dataspaceName, final String anchorName) { log.debug("process data updated event for dataspace '{}' & anchor '{}'", dataspaceName, anchorName); try { - if (shouldSendNotification()) { + if (shouldSendNotification(dataspaceName)) { final var cpsDataUpdatedEvent = cpsDataUpdatedEventFactory.createCpsDataUpdatedEvent(dataspaceName, anchorName); log.debug("data updated event to be published {}", cpsDataUpdatedEvent); @@ -80,8 +97,11 @@ public class NotificationService { /* Add more complex rules based on dataspace and anchor later */ - private boolean shouldSendNotification() { - return dataUpdatedEventNotificationEnabled; + private boolean shouldSendNotification(final String dataspaceName) { + + return notificationProperties.isEnabled() + && dataspacePatterns.stream() + .anyMatch(pattern -> pattern.matcher(dataspaceName).find()); } } 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 a74279548e..b60d093236 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 @@ -20,59 +20,76 @@ package org.onap.cps.notification import org.onap.cps.event.model.CpsDataUpdatedEvent +import org.spockframework.spring.SpringBean +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.context.ContextConfiguration +import spock.lang.Shared import spock.lang.Specification +@SpringBootTest +@EnableConfigurationProperties +@ContextConfiguration(classes = [NotificationProperties]) class NotificationServiceSpec extends Specification { - def mockNotificationPublisher = Mock(NotificationPublisher) - def spyNotificationErrorHandler = Spy(new NotificationErrorHandler()) - def mockCpsDataUpdatedEventFactory = Mock(CpsDataUpdatedEventFactory) + @SpringBean + NotificationPublisher mockNotificationPublisher = Mock() + @SpringBean + NotificationErrorHandler spyNotificationErrorHandler = Spy(new NotificationErrorHandler()) + @SpringBean + CpsDataUpdatedEventFactory mockCpsDataUpdatedEventFactory = Mock() - def objectUnderTest = new NotificationService(true, mockNotificationPublisher, - mockCpsDataUpdatedEventFactory, spyNotificationErrorHandler) + @Autowired + NotificationProperties notificationProperties + NotificationProperties spyNotificationProperties - def myDataspaceName = 'my-dataspace' + @Shared + def myDataspacePublishedName = 'my-dataspace-published' def myAnchorName = 'my-anchorname' def 'Skip sending notification when disabled.'() { - given: 'notification is disabled' - objectUnderTest.dataUpdatedEventNotificationEnabled = false - + def objectUnderTest = createNotificationService(false) when: 'dataUpdatedEvent is received' - objectUnderTest.processDataUpdatedEvent(myDataspaceName, myAnchorName) - + objectUnderTest.processDataUpdatedEvent(myDataspacePublishedName, myAnchorName) then: 'the notification is not sent' 0 * mockNotificationPublisher.sendNotification(_) } - def 'Send notification when enabled.'() { - + def 'Send notification when enabled: #scenario.'() { given: 'notification is enabled' - objectUnderTest.dataUpdatedEventNotificationEnabled = true + def objectUnderTest = createNotificationService(true) and: 'event factory can create event successfully' def cpsDataUpdatedEvent = new CpsDataUpdatedEvent() - mockCpsDataUpdatedEventFactory.createCpsDataUpdatedEvent(myDataspaceName, myAnchorName) >> cpsDataUpdatedEvent - + mockCpsDataUpdatedEventFactory.createCpsDataUpdatedEvent(dataspaceName, myAnchorName) >> cpsDataUpdatedEvent when: 'dataUpdatedEvent is received' - objectUnderTest.processDataUpdatedEvent(myDataspaceName, myAnchorName) - - then: 'notification is sent with correct event' - 1 * mockNotificationPublisher.sendNotification(cpsDataUpdatedEvent) + objectUnderTest.processDataUpdatedEvent(dataspaceName, myAnchorName) + then: 'notification is sent' + expectedSendNotificationCount * mockNotificationPublisher.sendNotification(cpsDataUpdatedEvent) + where: + scenario | dataspaceName || expectedSendNotificationCount + 'dataspace name does not match filter' | 'does-not-match-pattern' || 0 + 'dataspace name matches filter' | myDataspacePublishedName || 1 } - def 'Error handling in notification service.'(){ - given: 'event factory can not create event successfully' - mockCpsDataUpdatedEventFactory.createCpsDataUpdatedEvent(myDataspaceName, myAnchorName) >> - { throw new Exception("Could not create event") } - + def 'Error handling in notification service.'() { + given: 'notification is enabled' + def objectUnderTest = createNotificationService(true) + and: 'event factory can not create event successfully' + mockCpsDataUpdatedEventFactory.createCpsDataUpdatedEvent(myDataspacePublishedName, myAnchorName) >> + { throw new Exception("Could not create event") } when: 'event is sent for processing' - objectUnderTest.processDataUpdatedEvent(myDataspaceName, myAnchorName) - + objectUnderTest.processDataUpdatedEvent(myDataspacePublishedName, myAnchorName) then: 'error is handled and not thrown to caller' notThrown Exception - 1 * spyNotificationErrorHandler.onException(_,_,_,_) - + 1 * spyNotificationErrorHandler.onException(_, _, _, _) } + NotificationService createNotificationService(boolean notificationEnabled) { + spyNotificationProperties = Spy(notificationProperties) + spyNotificationProperties.isEnabled() >> notificationEnabled + return new NotificationService(spyNotificationProperties, mockNotificationPublisher, + mockCpsDataUpdatedEventFactory, spyNotificationErrorHandler) + } } diff --git a/cps-service/src/test/resources/application.yml b/cps-service/src/test/resources/application.yml index c934486fc9..94f7e818f4 100644 --- a/cps-service/src/test/resources/application.yml +++ b/cps-service/src/test/resources/application.yml @@ -17,8 +17,10 @@ notification: data-updated: - topic: cps-event + filters: + enabled-dataspaces: ".*-published,.*-important" enabled: true + topic: cps-event spring: kafka: |