diff options
76 files changed, 2874 insertions, 759 deletions
diff --git a/cps-application/pom.xml b/cps-application/pom.xml index 9990cdd5a9..c689c75752 100755 --- a/cps-application/pom.xml +++ b/cps-application/pom.xml @@ -4,6 +4,7 @@ Copyright (c) 2021 Pantheon.tech. Modifications Copyright (C) 2021 Bell Canada. Modifications Copyright (C) 2021 Nordix Foundation + Modifications Copyright (C) 2022 Deutsche Telekom AG ================================================================================ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -114,6 +115,10 @@ <groupId>com.tngtech.archunit</groupId> <artifactId>archunit-junit5</artifactId> </dependency> + <dependency> + <groupId>com.fasterxml.jackson.dataformat</groupId> + <artifactId>jackson-dataformat-xml</artifactId> + </dependency> </dependencies> <build> diff --git a/cps-application/src/main/resources/application.yml b/cps-application/src/main/resources/application.yml index e3ffd04d7b..b5b10b0f74 100644 --- a/cps-application/src/main/resources/application.yml +++ b/cps-application/src/main/resources/application.yml @@ -98,6 +98,8 @@ app: ncmp:
async-m2m:
topic: ${NCMP_ASYNC_M2M_TOPIC:ncmp-async-m2m}
+ avc:
+ subscription-topic: ${NCMP_CM_AVC_SUBSCRIPTION:cm-avc-subscription}
lcm:
events:
topic: ${LCM_EVENTS_TOPIC:ncmp-events}
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 new file mode 100644 index 0000000000..5ab446cbbe --- /dev/null +++ b/cps-ncmp-events/src/main/resources/schemas/avc-subscription-event-v1.json @@ -0,0 +1,101 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "urn:cps:org.onap.cps.ncmp.events:avc-subscription-event:v1", + "$ref": "#/definitions/SubscriptionEvent", + "definitions": { + "SubscriptionEvent": { + "description": "The payload for avc subscription event.", + "type": "object", + "properties": { + "version": { + "description": "The event type version", + "type": "string" + }, + "eventType": { + "description": "The event type", + "type": "string", + "enum": ["CREATE"] + }, + "event": { + "$ref": "#/definitions/event" + } + }, + "required": [ + "version", + "eventContent" + ], + "additionalProperties": false + }, + "event": { + "description": "The event content.", + "type": "object", + "properties": { + "subscription": { + "description": "The subscription details.", + "type": "object", + "properties": { + "clientID": { + "description": "The clientID", + "type": "string" + }, + "name": { + "description": "The name of the subscription", + "type": "string" + }, + "isTagged": { + "description": "optional parameter, default is no", + "type": "boolean", + "default": false + } + }, + "required": [ + "clientID", + "name" + ] + }, + "dataType": { + "description": "The datatype content.", + "type": "object", + "properties": { + "dataspace": { + "description": "The dataspace name", + "type": "string" + }, + "dataCategory": { + "description": "The category type of the data", + "type": "string" + }, + "dataProvider": { + "description": "The provider name of the data", + "type": "string" + }, + "schemaName": { + "description": "The name of the schema", + "type": "string" + }, + "schemaVersion": { + "description": "The version of the schema", + "type": "string" + } + } + }, + "required": [ + "dataspace", + "dataCategory", + "dataProvider", + "schemaName", + "schemaVersion" + ], + "predicates": { + "description": "Additional values to be added into the subscription", + "existingJavaType" : "java.util.Map<String,Object>", + "type" : "object" + } + } + }, + "required": [ + "subscription", + "dataType" + ] + } +}
\ No newline at end of file diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandlerQueryServiceImpl.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandlerQueryServiceImpl.java index a8fc6d7057..b67ae0c19e 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandlerQueryServiceImpl.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandlerQueryServiceImpl.java @@ -49,7 +49,6 @@ import org.onap.cps.ncmp.api.inventory.enums.PropertyType; import org.onap.cps.ncmp.api.models.CmHandleQueryServiceParameters; import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle; import org.onap.cps.spi.exceptions.DataValidationException; -import org.onap.cps.spi.model.Anchor; import org.onap.cps.spi.model.ConditionProperties; import org.onap.cps.spi.model.DataNode; import org.springframework.stereotype.Service; @@ -105,7 +104,8 @@ public class NetworkCmProxyCmHandlerQueryServiceImpl implements NetworkCmProxyCm if (moduleNamesForQuery.isEmpty()) { return combinedQueryResult.keySet(); } - final Set<String> moduleNameQueryResult = getNamesOfAnchorsWithGivenModules(moduleNamesForQuery); + final Set<String> moduleNameQueryResult = + new HashSet<>(inventoryPersistence.getCmHandleIdsWithGivenModules(moduleNamesForQuery)); if (combinedQueryResult == NO_QUERY_TO_EXECUTE) { return moduleNameQueryResult; @@ -209,7 +209,8 @@ public class NetworkCmProxyCmHandlerQueryServiceImpl implements NetworkCmProxyCm if (moduleNamesForQuery.isEmpty()) { return previousQueryResult; } - final Collection<String> cmHandleIdsByModuleName = getNamesOfAnchorsWithGivenModules(moduleNamesForQuery); + final Collection<String> cmHandleIdsByModuleName = + inventoryPersistence.getCmHandleIdsWithGivenModules(moduleNamesForQuery); if (cmHandleIdsByModuleName.isEmpty()) { return Collections.emptyMap(); } @@ -260,11 +261,6 @@ public class NetworkCmProxyCmHandlerQueryServiceImpl implements NetworkCmProxyCm return cmHandleQueries.combineCmHandleQueries(cpsPathQueryResult, propertiesQueryResult); } - private Set<String> getNamesOfAnchorsWithGivenModules(final Collection<String> moduleNamesForQuery) { - final Collection<Anchor> anchors = inventoryPersistence.queryAnchors(moduleNamesForQuery); - return anchors.parallelStream().map(Anchor::getName).collect(Collectors.toSet()); - } - private Collection<String> getModuleNamesForQuery(final List<ConditionProperties> conditionProperties) { final List<String> result = new ArrayList<>(); getConditions(conditionProperties, CmHandleQueryConditions.HAS_ALL_MODULES.getConditionName()) 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 new file mode 100644 index 0000000000..1f0324693a --- /dev/null +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/event/avc/SubscriptionEventConsumer.java @@ -0,0 +1,53 @@ +/* + * ============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.ncmp.api.impl.event.avc; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.onap.cps.ncmp.event.model.SubscriptionEvent; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.stereotype.Component; + + +@Component +@Slf4j +@RequiredArgsConstructor +public class SubscriptionEventConsumer { + + /** + * Consume the specified event. + * + * @param subscriptionEvent the event to be consumed + */ + @KafkaListener(topics = "${app.ncmp.avc.subscription-topic}") + public void consumeSubscriptionEvent(final SubscriptionEvent subscriptionEvent) { + if ("CM".equals(subscriptionEvent.getEvent().getDataType().getDataCategory())) { + log.debug("Consuming event {} ...", subscriptionEvent.toString()); + if ("CREATE".equals(subscriptionEvent.getEventType().value())) { + log.info("Subscription for ClientID {} with name{} ...", + subscriptionEvent.getEvent().getSubscription().getClientID(), + subscriptionEvent.getEvent().getSubscription().getName()); + } + } else { + log.trace("Non-CM subscription event ignored"); + } + } +} diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/InventoryPersistence.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/InventoryPersistence.java index b29825e7c0..6d006d9e2a 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/InventoryPersistence.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/InventoryPersistence.java @@ -24,7 +24,6 @@ import java.util.Collection; import java.util.Map; import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle; import org.onap.cps.spi.FetchDescendantsOption; -import org.onap.cps.spi.model.Anchor; import org.onap.cps.spi.model.DataNode; import org.onap.cps.spi.model.ModuleDefinition; import org.onap.cps.spi.model.ModuleReference; @@ -132,19 +131,12 @@ public interface InventoryPersistence { DataNode getCmHandleDataNode(String cmHandleId); /** - * Query anchors via module names. + * get CM handles that has given module names. * * @param moduleNamesForQuery module names - * @return Collection of anchors + * @return Collection of CM handle Ids */ - Collection<Anchor> queryAnchors(Collection<String> moduleNamesForQuery); - - /** - * Method to get all anchors. - * - * @return Collection of anchors - */ - Collection<Anchor> getAnchors(); + Collection<String> getCmHandleIdsWithGivenModules(Collection<String> moduleNamesForQuery); /** * Replaces list content by removing all existing elements and inserting the given new elements as data nodes. diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/InventoryPersistenceImpl.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/InventoryPersistenceImpl.java index adba198408..5b0b5eafde 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/InventoryPersistenceImpl.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/InventoryPersistenceImpl.java @@ -34,15 +34,13 @@ import java.util.List; import java.util.Map; 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.utils.YangDataConverter; import org.onap.cps.ncmp.api.impl.yangmodels.YangModelCmHandle; -import org.onap.cps.spi.CpsAdminPersistenceService; -import org.onap.cps.spi.CpsDataPersistenceService; import org.onap.cps.spi.FetchDescendantsOption; import org.onap.cps.spi.exceptions.SchemaSetNotFoundException; -import org.onap.cps.spi.model.Anchor; import org.onap.cps.spi.model.DataNode; import org.onap.cps.spi.model.ModuleDefinition; import org.onap.cps.spi.model.ModuleReference; @@ -69,9 +67,7 @@ public class InventoryPersistenceImpl implements InventoryPersistence { private final CpsModuleService cpsModuleService; - private final CpsDataPersistenceService cpsDataPersistenceService; - - private final CpsAdminPersistenceService cpsAdminPersistenceService; + private final CpsAdminService cpsAdminService; private final CpsValidator cpsValidator; @@ -161,7 +157,7 @@ public class InventoryPersistenceImpl implements InventoryPersistence { @Override public DataNode getDataNode(final String xpath, final FetchDescendantsOption fetchDescendantsOption) { - return cpsDataPersistenceService.getDataNode(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, + return cpsDataService.getDataNode(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, xpath, fetchDescendantsOption); } @@ -171,13 +167,8 @@ public class InventoryPersistenceImpl implements InventoryPersistence { } @Override - public Collection<Anchor> queryAnchors(final Collection<String> moduleNamesForQuery) { - return cpsAdminPersistenceService.queryAnchors(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, moduleNamesForQuery); - } - - @Override - public Collection<Anchor> getAnchors() { - return cpsAdminPersistenceService.getAnchors(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME); + public Collection<String> getCmHandleIdsWithGivenModules(final Collection<String> moduleNamesForQuery) { + return cpsAdminService.queryAnchorNames(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, moduleNamesForQuery); } @Override diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandlerQueryServiceSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandlerQueryServiceSpec.groovy index 201f6afe5a..05856d0ea8 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandlerQueryServiceSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandlerQueryServiceSpec.groovy @@ -29,11 +29,9 @@ import org.onap.cps.ncmp.api.models.NcmpServiceCmHandle import org.onap.cps.spi.FetchDescendantsOption import org.onap.cps.spi.exceptions.DataInUseException import org.onap.cps.spi.exceptions.DataValidationException -import org.onap.cps.spi.model.Anchor import org.onap.cps.spi.model.ConditionProperties import org.onap.cps.spi.model.DataNode import spock.lang.Specification - import java.util.stream.Collectors class NetworkCmProxyCmHandlerQueryServiceSpec extends Specification { @@ -110,20 +108,20 @@ class NetworkCmProxyCmHandlerQueryServiceSpec extends Specification { and: 'null is returned from the state and public property queries' cmHandleQueries.combineCmHandleQueries(*_) >> null and: '#scenario from the modules query' - mockInventoryPersistence.queryAnchors(*_) >> returnedAnchors + mockInventoryPersistence.getCmHandleIdsWithGivenModules(*_) >> cmHandleIdsFromService and: 'the same cmHandles are returned from the persistence service layer' - returnedAnchors.size() * mockInventoryPersistence.getDataNode(*_) >> returnedCmHandles + cmHandleIdsFromService.size() * mockInventoryPersistence.getDataNode(*_) >> returnedCmHandles when: 'the query is executed for both cm handle ids and details' def returnedCmHandlesJustIds = objectUnderTest.queryCmHandleIds(cmHandleQueryParameters) def returnedCmHandlesWithData = objectUnderTest.queryCmHandles(cmHandleQueryParameters) then: 'the correct expected cm handles ids are returned' - returnedCmHandlesJustIds == expectedCmHandleIds as Set + returnedCmHandlesJustIds == cmHandleIdsFromService as Set and: 'the correct cm handle data objects are returned' - returnedCmHandlesWithData.stream().map(dataNode -> dataNode.cmHandleId).collect(Collectors.toSet()) == expectedCmHandleIds as Set + returnedCmHandlesWithData.stream().map(dataNode -> dataNode.cmHandleId).collect(Collectors.toSet()) == cmHandleIdsFromService as Set where: 'the following data is used' - scenario | returnedAnchors | returnedCmHandles || expectedCmHandleIds - 'One anchor returned' | [new Anchor(name: 'some-cmhandle-id')] | someCmHandleDataNode || ['some-cmhandle-id'] - 'No anchors are returned' | [] | null || [] + scenario | cmHandleIdsFromService | returnedCmHandles + 'One anchor returned' | ['some-cmhandle-id'] | someCmHandleDataNode + 'No anchors are returned' | [] | null } def 'Retrieve cm handles with combined queries when #scenario.'() { @@ -136,7 +134,7 @@ class NetworkCmProxyCmHandlerQueryServiceSpec extends Specification { and: 'cmHandles are returned from the state and public property combined queries' cmHandleQueries.combineCmHandleQueries(*_) >> combinedQueryMap and: 'cmHandles are returned from the module names query' - mockInventoryPersistence.queryAnchors(['some-module-name']) >> anchorsForModuleQuery + mockInventoryPersistence.getCmHandleIdsWithGivenModules(['some-module-name']) >> anchorsForModuleQuery and: 'cmHandleQueries returns a datanode result' 2 * cmHandleQueries.queryCmHandleDataNodesByCpsPath(*_) >> [someCmHandleDataNode] when: 'the query is executed for both cm handle ids and details' @@ -147,12 +145,12 @@ class NetworkCmProxyCmHandlerQueryServiceSpec extends Specification { and: 'the correct cm handle data objects are returned' returnedCmHandlesWithData.stream().map(dataNode -> dataNode.cmHandleId).collect(Collectors.toSet()) == expectedCmHandleIds as Set where: 'the following data is used' - scenario | combinedQueryMap | anchorsForModuleQuery || expectedCmHandleIds - 'combined and modules queries intersect' | ['PNFDemo1' : new NcmpServiceCmHandle(cmHandleId:'PNFDemo1')] | [new Anchor(name: 'PNFDemo1'), new Anchor(name: 'PNFDemo2')] || ['PNFDemo1'] - 'only module query results exist' | [:] | [new Anchor(name: 'PNFDemo1'), new Anchor(name: 'PNFDemo2')] || [] - 'only combined query results exist' | ['PNFDemo1' : new NcmpServiceCmHandle(cmHandleId:'PNFDemo1'), 'PNFDemo2' : new NcmpServiceCmHandle(cmHandleId:'PNFDemo2')] | [] || [] - 'neither queries return results' | [:] | [] || [] - 'none intersect' | ['PNFDemo1' : new NcmpServiceCmHandle(cmHandleId:'PNFDemo1')] | [new Anchor(name: 'PNFDemo2')] || [] + scenario | combinedQueryMap | anchorsForModuleQuery || expectedCmHandleIds + 'combined and modules queries intersect' | ['PNFDemo1': new NcmpServiceCmHandle(cmHandleId: 'PNFDemo1')] | ['PNFDemo1', 'PNFDemo2'] || ['PNFDemo1'] + 'only module query results exist' | [:] | ['PNFDemo1', 'PNFDemo2'] || [] + 'only combined query results exist' | ['PNFDemo1': new NcmpServiceCmHandle(cmHandleId: 'PNFDemo1'), 'PNFDemo2': new NcmpServiceCmHandle(cmHandleId: 'PNFDemo2')] | [] || [] + 'neither queries return results' | [:] | [] || [] + 'none intersect' | ['PNFDemo1': new NcmpServiceCmHandle(cmHandleId: 'PNFDemo1')] | ['PNFDemo2'] || [] } def 'Retrieve cm handles when the query is empty.'() { 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 new file mode 100644 index 0000000000..20d60e3963 --- /dev/null +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/event/avc/SubscriptionEventConsumerSpec.groovy @@ -0,0 +1,52 @@ +/* + * ============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.ncmp.api.impl.event.avc + +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.utils.JsonObjectMapper +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest + +@SpringBootTest(classes = [SubscriptionEventConsumer, ObjectMapper, JsonObjectMapper]) +class SubscriptionEventConsumerSpec extends MessagingBaseSpec { + + def objectUnderTest = new SubscriptionEventConsumer() + + @Autowired + JsonObjectMapper jsonObjectMapper + + def 'Consume valid 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) + when: 'the valid event is consumed' + objectUnderTest.consumeSubscriptionEvent(testEventSent) + then: 'no exception is thrown' + noExceptionThrown() + where: 'data category is changed' + dataCategory << [ 'CM' , 'FM' ] + } +} diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/InventoryPersistenceImplSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/InventoryPersistenceImplSpec.groovy index c713aad3c4..355487f64a 100644 --- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/InventoryPersistenceImplSpec.groovy +++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/InventoryPersistenceImplSpec.groovy @@ -22,12 +22,11 @@ package org.onap.cps.ncmp.api.inventory import com.fasterxml.jackson.databind.ObjectMapper +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.yangmodels.YangModelCmHandle import org.onap.cps.spi.CascadeDeleteAllowed -import org.onap.cps.spi.CpsDataPersistenceService -import org.onap.cps.spi.CpsAdminPersistenceService import org.onap.cps.spi.FetchDescendantsOption import org.onap.cps.spi.model.DataNode import org.onap.cps.spi.model.ModuleDefinition @@ -36,7 +35,6 @@ import org.onap.cps.utils.JsonObjectMapper import org.onap.cps.spi.utils.CpsValidator import spock.lang.Shared import spock.lang.Specification - import java.time.OffsetDateTime import java.time.ZoneOffset import java.time.format.DateTimeFormatter @@ -52,14 +50,12 @@ class InventoryPersistenceImplSpec extends Specification { def mockCpsModuleService = Mock(CpsModuleService) - def mockCpsDataPersistenceService = Mock(CpsDataPersistenceService) - - def mockCpsAdminPersistenceService = Mock(CpsAdminPersistenceService) + def mockCpsAdminService = Mock(CpsAdminService) def mockCpsValidator = Mock(CpsValidator) def objectUnderTest = new InventoryPersistenceImpl(spiedJsonObjectMapper, mockCpsDataService, mockCpsModuleService, - mockCpsDataPersistenceService, mockCpsAdminPersistenceService, mockCpsValidator) + mockCpsAdminService, mockCpsValidator) def formattedDateAndTime = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ") .format(OffsetDateTime.of(2022, 12, 31, 20, 30, 40, 1, ZoneOffset.UTC)) @@ -84,7 +80,7 @@ class InventoryPersistenceImplSpec extends Specification { def "Retrieve CmHandle using datanode with #scenario."() { given: 'the cps data service returns a data node from the DMI registry' def dataNode = new DataNode(childDataNodes:childDataNodes, leaves: leaves) - mockCpsDataPersistenceService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry', xpath, INCLUDE_ALL_DESCENDANTS) >> dataNode + mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry', xpath, INCLUDE_ALL_DESCENDANTS) >> dataNode when: 'retrieving the yang modelled cm handle' def result = objectUnderTest.getYangModelCmHandle(cmHandleId) then: 'the result has the correct id and service names' @@ -111,7 +107,7 @@ class InventoryPersistenceImplSpec extends Specification { def "Handling missing service names as null."() { given: 'the cps data service returns a data node from the DMI registry with empty child and leaf attributes' def dataNode = new DataNode(childDataNodes:[], leaves: [:]) - mockCpsDataPersistenceService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry', xpath, INCLUDE_ALL_DESCENDANTS) >> dataNode + mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry', xpath, INCLUDE_ALL_DESCENDANTS) >> dataNode when: 'retrieving the yang modelled cm handle' def result = objectUnderTest.getYangModelCmHandle(cmHandleId) then: 'the service names are returned as null' @@ -239,7 +235,7 @@ class InventoryPersistenceImplSpec extends Specification { when: 'the method to get data nodes is called' objectUnderTest.getDataNode('sample xPath') then: 'the data persistence service method to get data node is invoked once' - 1 * mockCpsDataPersistenceService.getDataNode('NCMP-Admin','ncmp-dmi-registry','sample xPath', INCLUDE_ALL_DESCENDANTS) + 1 * mockCpsDataService.getDataNode('NCMP-Admin','ncmp-dmi-registry','sample xPath', INCLUDE_ALL_DESCENDANTS) } def 'Get cmHandle data node'() { @@ -248,21 +244,14 @@ class InventoryPersistenceImplSpec extends Specification { when: 'the method to get data nodes is called' objectUnderTest.getCmHandleDataNode('sample cmHandleId') then: 'the data persistence service method to get cmHandle data node is invoked once with expected xPath' - 1 * mockCpsDataPersistenceService.getDataNode('NCMP-Admin','ncmp-dmi-registry',expectedXPath, INCLUDE_ALL_DESCENDANTS) + 1 * mockCpsDataService.getDataNode('NCMP-Admin','ncmp-dmi-registry',expectedXPath, INCLUDE_ALL_DESCENDANTS) } - def 'Query anchors'() { - when: 'the method to query anchors is called' - objectUnderTest.queryAnchors(['sample-module-name']) + def 'Get CM handles that has given module names'() { + when: 'the method to get cm handles is called' + objectUnderTest.getCmHandleIdsWithGivenModules(['sample-module-name']) then: 'the admin persistence service method to query anchors is invoked once with the same parameter' - 1 * mockCpsAdminPersistenceService.queryAnchors('NFP-Operational',['sample-module-name']) - } - - def 'Get anchors'() { - when: 'the method to get anchors with no parameters is called' - objectUnderTest.getAnchors() - then: 'the admin persistence service method to query anchors is invoked once with a specific dataspace name' - 1 * mockCpsAdminPersistenceService.getAnchors('NFP-Operational') + 1 * mockCpsAdminService.queryAnchorNames('NFP-Operational',['sample-module-name']) } def 'Replace list content'() { diff --git a/cps-ncmp-service/src/test/resources/application.yml b/cps-ncmp-service/src/test/resources/application.yml index 8d8bfaf9b4..4009e564a8 100644 --- a/cps-ncmp-service/src/test/resources/application.yml +++ b/cps-ncmp-service/src/test/resources/application.yml @@ -16,6 +16,11 @@ # SPDX-License-Identifier: Apache-2.0 # ============LICENSE_END========================================================= +app: + ncmp: + avc: + subscription-topic: test-avc-subscription + ncmp: dmi: auth: diff --git a/cps-ncmp-service/src/test/resources/avcSubscriptionCreationEvent.json b/cps-ncmp-service/src/test/resources/avcSubscriptionCreationEvent.json new file mode 100644 index 0000000000..1d84c3a5f2 --- /dev/null +++ b/cps-ncmp-service/src/test/resources/avcSubscriptionCreationEvent.json @@ -0,0 +1,23 @@ +{ + "version": "1.0", + "eventType": "CREATE", + "event": { + "subscription": { + "clientID": "SCO-9989752", + "name": "cm-subscription-001" + }, + "dataType": { + "dataspace": "ALL", + "dataCategory": "CM", + "dataProvider": "CM-SERVICE", + "schemaName": "org.onap.ncmp:cm-network-avc-event.rfc8641", + "schemaVersion": "1.0" + }, + "predicates": { + "datastore": "passthrough-operational", + "datastore-xpath-filter": "//_3gpp-nr-nrm-gnbdufunction:GNBDUFunction/ ", + "_3gpp-nr-nrm-nrcelldu": "NRCellDU" + + } + } +}
\ No newline at end of file diff --git a/cps-path-parser/pom.xml b/cps-path-parser/pom.xml index d9c150881d..d0da105aae 100644 --- a/cps-path-parser/pom.xml +++ b/cps-path-parser/pom.xml @@ -89,4 +89,29 @@ </dependency> </dependencies> + <profiles> + <profile> + <id>default</id> + <activation> + <activeByDefault>true</activeByDefault> + </activation> + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <configuration> + <excludes> + <exclude>%regex[.*PerfTest.*]</exclude> + </excludes> + </configuration> + </plugin> + </plugins> + </build> + </profile> + <profile> + <id>include-performance</id> + </profile> + </profiles> + </project> diff --git a/cps-path-parser/src/main/antlr4/org/onap/cps/cpspath/parser/antlr4/CpsPath.g4 b/cps-path-parser/src/main/antlr4/org/onap/cps/cpspath/parser/antlr4/CpsPath.g4 index 40ad410a0d..db09b3c532 100644 --- a/cps-path-parser/src/main/antlr4/org/onap/cps/cpspath/parser/antlr4/CpsPath.g4 +++ b/cps-path-parser/src/main/antlr4/org/onap/cps/cpspath/parser/antlr4/CpsPath.g4 @@ -28,7 +28,9 @@ ancestorPath : yangElement ( SLASH yangElement)* ; textFunctionCondition : SLASH leafName OB KW_TEXT_FUNCTION EQ StringLiteral CB ; -prefix : ( SLASH yangElement)* SLASH containerName ; +parent : ( SLASH yangElement)* ; + +prefix : parent SLASH containerName ; descendant : SLASH prefix ; diff --git a/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathBuilder.java b/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathBuilder.java index 21f5173a98..3a9d70ebbc 100644 --- a/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathBuilder.java +++ b/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathBuilder.java @@ -22,7 +22,9 @@ package org.onap.cps.cpspath.parser; import static org.onap.cps.cpspath.parser.CpsPathPrefixType.DESCENDANT; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.onap.cps.cpspath.parser.antlr4.CpsPathBaseListener; import org.onap.cps.cpspath.parser.antlr4.CpsPathParser; @@ -50,6 +52,8 @@ public class CpsPathBuilder extends CpsPathBaseListener { boolean processingAncestorAxis = false; + private List<String> containerNames = new ArrayList<>(); + @Override public void exitInvalidPostFix(final CpsPathParser.InvalidPostFixContext ctx) { throw new PathParsingException(ctx.getText()); @@ -61,6 +65,11 @@ public class CpsPathBuilder extends CpsPathBaseListener { } @Override + public void exitParent(final CpsPathParser.ParentContext ctx) { + cpsPathQuery.setNormalizedParentPath(normalizedXpathBuilder.toString()); + } + + @Override public void exitIncorrectPrefix(final IncorrectPrefixContext ctx) { throw new PathParsingException("CPS path can only start with one or two slashes (/)"); } @@ -141,6 +150,7 @@ public class CpsPathBuilder extends CpsPathBaseListener { CpsPathQuery build() { cpsPathQuery.setNormalizedXpath(normalizedXpathBuilder.toString()); + cpsPathQuery.setContainerNames(containerNames); return cpsPathQuery; } @@ -150,10 +160,12 @@ public class CpsPathBuilder extends CpsPathBaseListener { @Override public void exitContainerName(final CpsPathParser.ContainerNameContext ctx) { + final String containerName = ctx.getText(); normalizedXpathBuilder.append("/") - .append(ctx.getText()); + .append(containerName); + containerNames.add(containerName); if (processingAncestorAxis) { - normalizedAncestorPathBuilder.append("/").append(ctx.getText()); + normalizedAncestorPathBuilder.append("/").append(containerName); } } diff --git a/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathQuery.java b/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathQuery.java index 53490f3a2d..c9df8df904 100644 --- a/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathQuery.java +++ b/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathQuery.java @@ -22,6 +22,7 @@ package org.onap.cps.cpspath.parser; import static org.onap.cps.cpspath.parser.CpsPathPrefixType.ABSOLUTE; +import java.util.List; import java.util.Map; import lombok.AccessLevel; import lombok.Getter; @@ -32,7 +33,9 @@ import lombok.Setter; public class CpsPathQuery { private String xpathPrefix; + private String normalizedParentPath; private String normalizedXpath; + private List<String> containerNames; private CpsPathPrefixType cpsPathPrefixType = ABSOLUTE; private String descendantName; private Map<String, Object> leavesData; diff --git a/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathUtil.java b/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathUtil.java index 97d7d1d760..60f0e2efcd 100644 --- a/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathUtil.java +++ b/cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathUtil.java @@ -20,6 +20,9 @@ package org.onap.cps.cpspath.parser; +import static org.onap.cps.cpspath.parser.CpsPathPrefixType.ABSOLUTE; + +import java.util.List; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @@ -45,8 +48,34 @@ public class CpsPathUtil { * @return a normalized xpath String. */ public static String getNormalizedXpath(final String xpathSource) { - final CpsPathBuilder cpsPathBuilder = getCpsPathBuilder(xpathSource); - return cpsPathBuilder.build().getNormalizedXpath(); + return getCpsPathBuilder(xpathSource).build().getNormalizedXpath(); + } + + /** + * Returns the parent xpath. + * + * @param xpathSource xpath + * @return the parent xpath String. + */ + public static String getNormalizedParentXpath(final String xpathSource) { + return getCpsPathBuilder(xpathSource).build().getNormalizedParentPath(); + } + + public static String[] getXpathNodeIdSequence(final String xpathSource) { + final List<String> containerNames = getCpsPathBuilder(xpathSource).build().getContainerNames(); + return containerNames.toArray(new String[containerNames.size()]); + } + + + /** + * Returns boolean indicating xpath is an absolute path to a list element. + * + * @param xpathSource xpath + * @return true if xpath is an absolute path to a list element + */ + public static boolean isPathToListElement(final String xpathSource) { + final CpsPathQuery cpsPathQuery = getCpsPathBuilder(xpathSource).build(); + return cpsPathQuery.getCpsPathPrefixType() == ABSOLUTE && cpsPathQuery.hasLeafConditions(); } /** @@ -57,8 +86,7 @@ public class CpsPathUtil { */ public static CpsPathQuery getCpsPathQuery(final String cpsPathSource) { - final CpsPathBuilder cpsPathBuilder = getCpsPathBuilder(cpsPathSource); - return cpsPathBuilder.build(); + return getCpsPathBuilder(cpsPathSource).build(); } private static CpsPathBuilder getCpsPathBuilder(final String cpsPathSource) { diff --git a/cps-path-parser/src/test/groovy/org/onap/cps/cpspath/parser/CpsPathUtilSpec.groovy b/cps-path-parser/src/test/groovy/org/onap/cps/cpspath/parser/CpsPathUtilSpec.groovy new file mode 100644 index 0000000000..d62f337b75 --- /dev/null +++ b/cps-path-parser/src/test/groovy/org/onap/cps/cpspath/parser/CpsPathUtilSpec.groovy @@ -0,0 +1,90 @@ +/* + * ============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.cpspath.parser + +import spock.lang.Specification + +class CpsPathUtilSpec extends Specification { + + def 'Normalized xpaths for list index values using #scenario'() { + when: 'xpath with #scenario is parsed' + def result = CpsPathUtil.getNormalizedXpath(xpath) + then: 'normalized path uses single quotes for leave values' + assert result == "/parent/child[@common-leaf-name='123']" + where: 'the following xpaths are used' + scenario | xpath + 'no quotes' | '/parent/child[@common-leaf-name=123]' + 'double quotes' | '/parent/child[@common-leaf-name="123"]' + 'single quotes' | "/parent/child[@common-leaf-name='123']" + } + + def 'Normalized parent xpaths'() { + when: 'a given xpath with #scenario is parsed' + def result = CpsPathUtil.getNormalizedParentXpath(xpath) + then: 'the result is the expected parent path' + assert result == expectedParentPath + where: 'the following xpaths are used' + scenario | xpath || expectedParentPath + 'no child' | '/parent' || '' + 'child and parent' | '/parent/child' || '/parent' + 'grand child' | '/parent/child/grandChild' || '/parent/child' + 'parent & top is list element' | '/parent[@id=1]/child' || "/parent[@id='1']" + 'parent is list element' | '/parent/child[@id=1]/grandChild' || "/parent/child[@id='1']" + 'parent is list element with /' | "/parent/child[@id='a/b']/grandChild" || "/parent/child[@id='a/b']" + 'parent is list element with [' | "/parent/child[@id='a[b']/grandChild" || "/parent/child[@id='a[b']" + 'parent is list element using "' | '/parent/child[@id="x"]/grandChild' || "/parent/child[@id='x']" + } + + def 'Get node ID sequence for given xpath'() { + when: 'a given xpath with #scenario is parsed' + def result = CpsPathUtil.getXpathNodeIdSequence(xpath) + then: 'the result is the expected node ID sequence' + assert result == expectedNodeIdSequence + where: 'the following xpaths are used' + scenario | xpath || expectedNodeIdSequence + 'no child' | '/parent' || ["parent"] + 'child and parent' | '/parent/child' || ["parent","child"] + 'grand child' | '/parent/child/grandChild' || ["parent","child","grandChild"] + 'parent & top is list element' | '/parent[@id=1]/child' || ["parent","child"] + 'parent is list element' | '/parent/child[@id=1]/grandChild' || ["parent","child","grandChild"] + 'parent is list element with /' | "/parent/child[@id='a/b']/grandChild" || ["parent","child","grandChild"] + 'parent is list element with [' | "/parent/child[@id='a[b']/grandChild" || ["parent","child","grandChild"] + } + + def 'Recognizing (absolute) xpaths to List elements'() { + expect: 'check for list returns the correct values' + assert CpsPathUtil.isPathToListElement(xpath) == expectList + where: 'the following xpaths are used' + xpath || expectList + '/parent[@id=1]' || true + '/parent[@id=1]/child' || false + '/parent/child[@id=1]' || true + '//child[@id=1]' || false + } + + def 'Parsing Exception'() { + when: 'a invalid xpath is parsed' + CpsPathUtil.getNormalizedXpath('///') + then: 'a path parsing exception is thrown' + thrown(PathParsingException) + } + +} diff --git a/cps-path-parser/src/test/groovy/org/onap/cps/cpspath/parser/performance/CpsPathUtilPerfTest.groovy b/cps-path-parser/src/test/groovy/org/onap/cps/cpspath/parser/performance/CpsPathUtilPerfTest.groovy new file mode 100644 index 0000000000..2ba20c1c5f --- /dev/null +++ b/cps-path-parser/src/test/groovy/org/onap/cps/cpspath/parser/performance/CpsPathUtilPerfTest.groovy @@ -0,0 +1,43 @@ +/* + * ============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.cpspath.parser.performance + +import org.onap.cps.cpspath.parser.CpsPathUtil +import org.springframework.util.StopWatch +import spock.lang.Specification + +class CpsPathUtilPerfTest extends Specification { + + def 'CPS Path Processing Performance Test.'() { + when: '20,000 paths are processed' + def stopWatch = new StopWatch() + stopWatch.start() + (1..10000).each { + CpsPathUtil.getNormalizedXpath('/long/path/to/see/if/it/adds/paring/time/significantly/parent/child[@common-leaf-name="123"]') + CpsPathUtil.getNormalizedXpath('//child[@other-leaf=1]/leaf-name[text()="search"]/ancestor::parent') + } + stopWatch.stop() + then: 'it takes less then 1,000 milliseconds' + // In CI this actually takes about 0.3-0.5 sec which is approx. 50+ parser executions per millisecond! + assert stopWatch.getTotalTimeMillis() < 1000 + } + +} diff --git a/cps-rest/docs/openapi/components.yml b/cps-rest/docs/openapi/components.yml index fb0947e54a..e700da6ea1 100644 --- a/cps-rest/docs/openapi/components.yml +++ b/cps-rest/docs/openapi/components.yml @@ -2,6 +2,7 @@ # Copyright (c) 2021-2022 Bell Canada. # Modifications Copyright (C) 2021-2022 Nordix Foundation # Modifications Copyright (C) 2022 TechMahindra Ltd. +# Modifications Copyright (C) 2022 Deutsche Telekom AG # ================================================================================ # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -106,6 +107,17 @@ components: name: SciFi - code: 02 name: kids + dataSampleXml: + value: + <stores xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"> + <bookstore xmlns="org:onap:ccsdk:sample"> + <bookstore-name>Chapters</bookstore-name> + <categories> + <code>1</code> + <name>SciFi</name> + </categories> + </bookstore> + </stores> parameters: dataspaceNameInQuery: @@ -211,6 +223,23 @@ components: schema: type: string example: '2021-03-21T00:10:34.030-0100' + apiVersionInPath: + name: apiVersion + in: path + description: apiVersion + required: true + schema: + type: string + enum: [v1, v2] + default: v2 + contentTypeHeader: + name: Content-Type + in: header + description: Content type header + schema: + type: string + example: 'application/json' + required: true responses: NotFound: @@ -279,6 +308,8 @@ components: schema: type: string example: my-resource + CreatedV2: + description: Created without response body InternalServerError: description: Internal Server Error content: diff --git a/cps-rest/docs/openapi/cpsAdmin.yml b/cps-rest/docs/openapi/cpsAdmin.yml index 595f6d7ec1..f60a9be6ff 100644 --- a/cps-rest/docs/openapi/cpsAdmin.yml +++ b/cps-rest/docs/openapi/cpsAdmin.yml @@ -19,27 +19,6 @@ # ============LICENSE_END========================================================= dataspaces: - post: - description: Create a new dataspace - tags: - - cps-admin - summary: Create a dataspace - operationId: createDataspace - parameters: - - $ref: 'components.yml#/components/parameters/dataspaceNameInQuery' - responses: - '201': - $ref: 'components.yml#/components/responses/Created' - '400': - $ref: 'components.yml#/components/responses/BadRequest' - '401': - $ref: 'components.yml#/components/responses/Unauthorized' - '403': - $ref: 'components.yml#/components/responses/Forbidden' - '409': - $ref: 'components.yml#/components/responses/Conflict' - '500': - $ref: 'components.yml#/components/responses/InternalServerError' delete: description: Delete a dataspace tags: @@ -47,6 +26,7 @@ dataspaces: summary: Delete a dataspace operationId: deleteDataspace parameters: + - $ref: 'components.yml#/components/parameters/apiVersionInPath' - $ref: 'components.yml#/components/parameters/dataspaceNameInQuery' responses: '204': @@ -63,34 +43,6 @@ dataspaces: $ref: 'components.yml#/components/responses/InternalServerError' schemaSet: - post: - description: Create a new schema set in the given dataspace - tags: - - cps-admin - summary: Create a schema set - operationId: createSchemaSet - parameters: - - $ref: 'components.yml#/components/parameters/dataspaceNameInPath' - - $ref: 'components.yml#/components/parameters/schemaSetNameInQuery' - requestBody: - required: true - content: - multipart/form-data: - schema: - $ref: 'components.yml#/components/schemas/MultipartFile' - responses: - '201': - $ref: 'components.yml#/components/responses/Created' - '400': - $ref: 'components.yml#/components/responses/BadRequest' - '401': - $ref: 'components.yml#/components/responses/Unauthorized' - '403': - $ref: 'components.yml#/components/responses/Forbidden' - '409': - $ref: 'components.yml#/components/responses/Conflict' - '500': - $ref: 'components.yml#/components/responses/InternalServerError' get: description: Read all schema sets, given a dataspace tags: @@ -98,6 +50,7 @@ schemaSet: summary: Get schema sets operationId: getSchemaSets parameters: + - $ref: 'components.yml#/components/parameters/apiVersionInPath' - $ref: 'components.yml#/components/parameters/dataspaceNameInPath' responses: '200': @@ -125,6 +78,7 @@ schemaSetBySchemaSetName: summary: Get a schema set operationId: getSchemaSet parameters: + - $ref: 'components.yml#/components/parameters/apiVersionInPath' - $ref: 'components.yml#/components/parameters/dataspaceNameInPath' - $ref: 'components.yml#/components/parameters/schemaSetNameInPath' responses: @@ -149,6 +103,7 @@ schemaSetBySchemaSetName: summary: Delete a schema set operationId: deleteSchemaSet parameters: + - $ref: 'components.yml#/components/parameters/apiVersionInPath' - $ref: 'components.yml#/components/parameters/dataspaceNameInPath' - $ref: 'components.yml#/components/parameters/schemaSetNameInPath' responses: @@ -173,6 +128,7 @@ anchorsByDataspace: summary: Get anchors operationId: getAnchors parameters: + - $ref: 'components.yml#/components/parameters/apiVersionInPath' - $ref: 'components.yml#/components/parameters/dataspaceNameInPath' responses: '200': @@ -191,29 +147,6 @@ anchorsByDataspace: $ref: 'components.yml#/components/responses/Forbidden' '500': $ref: 'components.yml#/components/responses/InternalServerError' - post: - description: Create a new anchor in the given dataspace - tags: - - cps-admin - summary: Create an anchor - operationId: createAnchor - parameters: - - $ref: 'components.yml#/components/parameters/dataspaceNameInPath' - - $ref: 'components.yml#/components/parameters/schemaSetNameInQuery' - - $ref: 'components.yml#/components/parameters/anchorNameInQuery' - responses: - '201': - $ref: 'components.yml#/components/responses/Created' - '400': - $ref: 'components.yml#/components/responses/BadRequest' - '401': - $ref: 'components.yml#/components/responses/Unauthorized' - '403': - $ref: 'components.yml#/components/responses/Forbidden' - '409': - $ref: 'components.yml#/components/responses/Conflict' - '500': - $ref: 'components.yml#/components/responses/InternalServerError' anchorByDataspaceAndAnchorName: get: @@ -223,6 +156,7 @@ anchorByDataspaceAndAnchorName: summary: Get an anchor operationId: getAnchor parameters: + - $ref: 'components.yml#/components/parameters/apiVersionInPath' - $ref: 'components.yml#/components/parameters/dataspaceNameInPath' - $ref: 'components.yml#/components/parameters/anchorNameInPath' responses: @@ -247,6 +181,7 @@ anchorByDataspaceAndAnchorName: summary: Delete an anchor operationId: deleteAnchor parameters: + - $ref: 'components.yml#/components/parameters/apiVersionInPath' - $ref: 'components.yml#/components/parameters/dataspaceNameInPath' - $ref: 'components.yml#/components/parameters/anchorNameInPath' responses: @@ -268,6 +203,8 @@ adminDataspaces: - cps-admin summary: Get all dataspaces operationId: getAllDataspaces + parameters: + - $ref: 'components.yml#/components/parameters/apiVersionInPath' responses: '200': description: OK @@ -294,6 +231,7 @@ adminDataspace: summary: Get a dataspace operationId: getDataspace parameters: + - $ref: 'components.yml#/components/parameters/apiVersionInPath' - $ref: 'components.yml#/components/parameters/dataspaceNameInPath' responses: '200': diff --git a/cps-rest/docs/openapi/cpsAdminV1Deprecated.yml b/cps-rest/docs/openapi/cpsAdminV1Deprecated.yml new file mode 100644 index 0000000000..56f7f1b4fc --- /dev/null +++ b/cps-rest/docs/openapi/cpsAdminV1Deprecated.yml @@ -0,0 +1,98 @@ +# ============LICENSE_START======================================================= +# Copyright (C) 2022 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========================================================= + +dataspaces: + post: + deprecated: true + description: Create a new dataspace + tags: + - cps-admin + summary: Create a dataspace + operationId: createDataspace + parameters: + - $ref: 'components.yml#/components/parameters/dataspaceNameInQuery' + responses: + '201': + $ref: 'components.yml#/components/responses/Created' + '400': + $ref: 'components.yml#/components/responses/BadRequest' + '401': + $ref: 'components.yml#/components/responses/Unauthorized' + '403': + $ref: 'components.yml#/components/responses/Forbidden' + '409': + $ref: 'components.yml#/components/responses/Conflict' + '500': + $ref: 'components.yml#/components/responses/InternalServerError' + +anchorsByDataspace: + post: + deprecated: true + description: Create a new anchor in the given dataspace + tags: + - cps-admin + summary: Create an anchor + operationId: createAnchor + parameters: + - $ref: 'components.yml#/components/parameters/dataspaceNameInPath' + - $ref: 'components.yml#/components/parameters/schemaSetNameInQuery' + - $ref: 'components.yml#/components/parameters/anchorNameInQuery' + responses: + '201': + $ref: 'components.yml#/components/responses/Created' + '400': + $ref: 'components.yml#/components/responses/BadRequest' + '401': + $ref: 'components.yml#/components/responses/Unauthorized' + '403': + $ref: 'components.yml#/components/responses/Forbidden' + '409': + $ref: 'components.yml#/components/responses/Conflict' + '500': + $ref: 'components.yml#/components/responses/InternalServerError' + +schemaSet: + post: + deprecated: true + description: Create a new schema set in the given dataspace + tags: + - cps-admin + summary: Create a schema set + operationId: createSchemaSet + parameters: + - $ref: 'components.yml#/components/parameters/dataspaceNameInPath' + - $ref: 'components.yml#/components/parameters/schemaSetNameInQuery' + requestBody: + required: true + content: + multipart/form-data: + schema: + $ref: 'components.yml#/components/schemas/MultipartFile' + responses: + '201': + $ref: 'components.yml#/components/responses/Created' + '400': + $ref: 'components.yml#/components/responses/BadRequest' + '401': + $ref: 'components.yml#/components/responses/Unauthorized' + '403': + $ref: 'components.yml#/components/responses/Forbidden' + '409': + $ref: 'components.yml#/components/responses/Conflict' + '500': + $ref: 'components.yml#/components/responses/InternalServerError' diff --git a/cps-rest/docs/openapi/cpsAdminV2.yml b/cps-rest/docs/openapi/cpsAdminV2.yml new file mode 100644 index 0000000000..14e2cfe047 --- /dev/null +++ b/cps-rest/docs/openapi/cpsAdminV2.yml @@ -0,0 +1,95 @@ +# ============LICENSE_START======================================================= +# Copyright (C) 2022 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========================================================= + +dataspaces: + post: + description: Create a new dataspace + tags: + - cps-admin + summary: Create a dataspace + operationId: createDataspaceV2 + parameters: + - $ref: 'components.yml#/components/parameters/dataspaceNameInQuery' + responses: + '201': + $ref: 'components.yml#/components/responses/CreatedV2' + '400': + $ref: 'components.yml#/components/responses/BadRequest' + '401': + $ref: 'components.yml#/components/responses/Unauthorized' + '403': + $ref: 'components.yml#/components/responses/Forbidden' + '409': + $ref: 'components.yml#/components/responses/Conflict' + '500': + $ref: 'components.yml#/components/responses/InternalServerError' + +anchorsByDataspace: + post: + description: Create a new anchor in the given dataspace + tags: + - cps-admin + summary: Create an anchor + operationId: createAnchorV2 + parameters: + - $ref: 'components.yml#/components/parameters/dataspaceNameInPath' + - $ref: 'components.yml#/components/parameters/schemaSetNameInQuery' + - $ref: 'components.yml#/components/parameters/anchorNameInQuery' + responses: + '201': + $ref: 'components.yml#/components/responses/CreatedV2' + '400': + $ref: 'components.yml#/components/responses/BadRequest' + '401': + $ref: 'components.yml#/components/responses/Unauthorized' + '403': + $ref: 'components.yml#/components/responses/Forbidden' + '409': + $ref: 'components.yml#/components/responses/Conflict' + '500': + $ref: 'components.yml#/components/responses/InternalServerError' + +schemaSet: + post: + description: Create a new schema set in the given dataspace + tags: + - cps-admin + summary: Create a schema set + operationId: createSchemaSetV2 + parameters: + - $ref: 'components.yml#/components/parameters/dataspaceNameInPath' + - $ref: 'components.yml#/components/parameters/schemaSetNameInQuery' + requestBody: + required: true + content: + multipart/form-data: + schema: + $ref: 'components.yml#/components/schemas/MultipartFile' + responses: + '201': + $ref: 'components.yml#/components/responses/CreatedV2' + '400': + $ref: 'components.yml#/components/responses/BadRequest' + '401': + $ref: 'components.yml#/components/responses/Unauthorized' + '403': + $ref: 'components.yml#/components/responses/Forbidden' + '409': + $ref: 'components.yml#/components/responses/Conflict' + '500': + $ref: 'components.yml#/components/responses/InternalServerError' diff --git a/cps-rest/docs/openapi/cpsData.yml b/cps-rest/docs/openapi/cpsData.yml index 265ee23ad1..0dc388706c 100644 --- a/cps-rest/docs/openapi/cpsData.yml +++ b/cps-rest/docs/openapi/cpsData.yml @@ -1,6 +1,8 @@ # ============LICENSE_START======================================================= # Copyright (c) 2021-2022 Bell Canada. # Modifications Copyright (C) 2021-2022 Nordix Foundation +# Modifications Copyright (C) 2022 TechMahindra Ltd. +# Modifications Copyright (C) 2022 Deutsche Telekom AG # ================================================================================ # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -25,6 +27,7 @@ nodeByDataspaceAndAnchor: summary: Get a node operationId: getNodeByDataspaceAndAnchor parameters: + - $ref: 'components.yml#/components/parameters/apiVersionInPath' - $ref: 'components.yml#/components/parameters/dataspaceNameInPath' - $ref: 'components.yml#/components/parameters/anchorNameInPath' - $ref: 'components.yml#/components/parameters/xpathInQuery' @@ -57,6 +60,7 @@ listElementByDataspaceAndAnchor: summary: Add list element(s) operationId: addListElements parameters: + - $ref: 'components.yml#/components/parameters/apiVersionInPath' - $ref: 'components.yml#/components/parameters/dataspaceNameInPath' - $ref: 'components.yml#/components/parameters/anchorNameInPath' - $ref: 'components.yml#/components/parameters/requiredXpathInQuery' @@ -88,6 +92,7 @@ listElementByDataspaceAndAnchor: summary: Replace list content operationId: replaceListContent parameters: + - $ref: 'components.yml#/components/parameters/apiVersionInPath' - $ref: 'components.yml#/components/parameters/dataspaceNameInPath' - $ref: 'components.yml#/components/parameters/anchorNameInPath' - $ref: 'components.yml#/components/parameters/requiredXpathInQuery' @@ -112,29 +117,6 @@ listElementByDataspaceAndAnchor: $ref: 'components.yml#/components/responses/Forbidden' '500': $ref: 'components.yml#/components/responses/InternalServerError' - delete: - description: Delete one or all list element(s) for a given anchor and dataspace - deprecated: true - tags: - - cps-data - summary: Delete one or all list element(s) - operationId: deleteListOrListElement - parameters: - - $ref: 'components.yml#/components/parameters/dataspaceNameInPath' - - $ref: 'components.yml#/components/parameters/anchorNameInPath' - - $ref: 'components.yml#/components/parameters/requiredXpathInQuery' - - $ref: 'components.yml#/components/parameters/observedTimestampInQuery' - 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' - '500': - $ref: 'components.yml#/components/responses/InternalServerError' nodesByDataspaceAndAnchor: post: @@ -144,19 +126,30 @@ nodesByDataspaceAndAnchor: summary: Create a node operationId: createNode parameters: + - $ref: 'components.yml#/components/parameters/apiVersionInPath' - $ref: 'components.yml#/components/parameters/dataspaceNameInPath' - $ref: 'components.yml#/components/parameters/anchorNameInPath' - $ref: 'components.yml#/components/parameters/xpathInQuery' - $ref: 'components.yml#/components/parameters/observedTimestampInQuery' + - $ref: 'components.yml#/components/parameters/contentTypeHeader' requestBody: required: true content: application/json: schema: - type: object + type: string examples: dataSample: $ref: 'components.yml#/components/examples/dataSample' + application/xml: + schema: + type: object # Workaround to show example + xml: + name: stores + examples: + dataSample: + $ref: 'components.yml#/components/examples/dataSampleXml' + responses: '201': $ref: 'components.yml#/components/responses/Created' @@ -177,6 +170,7 @@ nodesByDataspaceAndAnchor: summary: Update node leaves operationId: updateNodeLeaves parameters: + - $ref: 'components.yml#/components/parameters/apiVersionInPath' - $ref: 'components.yml#/components/parameters/dataspaceNameInPath' - $ref: 'components.yml#/components/parameters/anchorNameInPath' - $ref: 'components.yml#/components/parameters/xpathInQuery' @@ -208,6 +202,7 @@ nodesByDataspaceAndAnchor: summary: Delete a data node operationId: deleteDataNode parameters: + - $ref: 'components.yml#/components/parameters/apiVersionInPath' - $ref: 'components.yml#/components/parameters/dataspaceNameInPath' - $ref: 'components.yml#/components/parameters/anchorNameInPath' - $ref: 'components.yml#/components/parameters/xpathInQuery' @@ -230,6 +225,7 @@ nodesByDataspaceAndAnchor: summary: Replace a node with descendants operationId: replaceNode parameters: + - $ref: 'components.yml#/components/parameters/apiVersionInPath' - $ref: 'components.yml#/components/parameters/dataspaceNameInPath' - $ref: 'components.yml#/components/parameters/anchorNameInPath' - $ref: 'components.yml#/components/parameters/xpathInQuery' diff --git a/cps-rest/docs/openapi/cpsDataV1Deprecated.yml b/cps-rest/docs/openapi/cpsDataV1Deprecated.yml new file mode 100644 index 0000000000..194ca3e079 --- /dev/null +++ b/cps-rest/docs/openapi/cpsDataV1Deprecated.yml @@ -0,0 +1,42 @@ +# ============LICENSE_START======================================================= +# Copyright (C) 2022 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========================================================= + +listElementByDataspaceAndAnchor: + delete: + description: Delete one or all list element(s) for a given anchor and dataspace + deprecated: true + tags: + - cps-data + summary: Delete one or all list element(s) + operationId: deleteListOrListElement + parameters: + - $ref: 'components.yml#/components/parameters/dataspaceNameInPath' + - $ref: 'components.yml#/components/parameters/anchorNameInPath' + - $ref: 'components.yml#/components/parameters/requiredXpathInQuery' + - $ref: 'components.yml#/components/parameters/observedTimestampInQuery' + 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' + '500': + $ref: 'components.yml#/components/responses/InternalServerError' diff --git a/cps-rest/docs/openapi/cpsQuery.yml b/cps-rest/docs/openapi/cpsQuery.yml index dc0402d03e..45fc70c861 100644 --- a/cps-rest/docs/openapi/cpsQuery.yml +++ b/cps-rest/docs/openapi/cpsQuery.yml @@ -1,6 +1,7 @@ # ============LICENSE_START======================================================= # Copyright (C) 2021 Nordix Foundation # Modifications Copyright (c) 2022 Bell Canada. +# Modifications Copyright (c) 2022 TechMahindra Ltd. # ================================================================================ # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -25,6 +26,7 @@ nodesByDataspaceAndAnchorAndCpsPath: summary: Query data nodes operationId: getNodesByDataspaceAndAnchorAndCpsPath parameters: + - $ref: 'components.yml#/components/parameters/apiVersionInPath' - $ref: 'components.yml#/components/parameters/dataspaceNameInPath' - $ref: 'components.yml#/components/parameters/anchorNameInPath' - $ref: 'components.yml#/components/parameters/cpsPathInQuery' diff --git a/cps-rest/docs/openapi/openapi.yml b/cps-rest/docs/openapi/openapi.yml index e02d6a6715..0918b56c3d 100644 --- a/cps-rest/docs/openapi/openapi.yml +++ b/cps-rest/docs/openapi/openapi.yml @@ -51,36 +51,57 @@ tags: paths: /v1/dataspaces: + $ref: 'cpsAdminV1Deprecated.yml#/dataspaces' + + /{apiVersion}/dataspaces: $ref: 'cpsAdmin.yml#/dataspaces' - /v1/admin/dataspaces: + /v2/dataspaces: + $ref: 'cpsAdminV2.yml#/dataspaces' + + /{apiVersion}/admin/dataspaces: $ref: 'cpsAdmin.yml#/adminDataspaces' - /v1/admin/dataspaces/{dataspace-name}: + /{apiVersion}/admin/dataspaces/{dataspace-name}: $ref: 'cpsAdmin.yml#/adminDataspace' /v1/dataspaces/{dataspace-name}/anchors: + $ref: 'cpsAdminV1Deprecated.yml#/anchorsByDataspace' + + /v2/dataspaces/{dataspace-name}/anchors: + $ref: 'cpsAdminV2.yml#/anchorsByDataspace' + + /{apiVersion}/dataspaces/{dataspace-name}/anchors: $ref: 'cpsAdmin.yml#/anchorsByDataspace' - /v1/dataspaces/{dataspace-name}/anchors/{anchor-name}: + /{apiVersion}/dataspaces/{dataspace-name}/anchors/{anchor-name}: $ref: 'cpsAdmin.yml#/anchorByDataspaceAndAnchorName' /v1/dataspaces/{dataspace-name}/schema-sets: + $ref: 'cpsAdminV1Deprecated.yml#/schemaSet' + + /v2/dataspaces/{dataspace-name}/schema-sets: + $ref: 'cpsAdminV2.yml#/schemaSet' + + /{apiVersion}/dataspaces/{dataspace-name}/schema-sets: $ref: 'cpsAdmin.yml#/schemaSet' - /v1/dataspaces/{dataspace-name}/schema-sets/{schema-set-name}: + /{apiVersion}/dataspaces/{dataspace-name}/schema-sets/{schema-set-name}: $ref: 'cpsAdmin.yml#/schemaSetBySchemaSetName' - /v1/dataspaces/{dataspace-name}/anchors/{anchor-name}/node: + /{apiVersion}/dataspaces/{dataspace-name}/anchors/{anchor-name}/node: $ref: 'cpsData.yml#/nodeByDataspaceAndAnchor' - /v1/dataspaces/{dataspace-name}/anchors/{anchor-name}/nodes: + /{apiVersion}/dataspaces/{dataspace-name}/anchors/{anchor-name}/nodes: $ref: 'cpsData.yml#/nodesByDataspaceAndAnchor' /v1/dataspaces/{dataspace-name}/anchors/{anchor-name}/list-nodes: + $ref: 'cpsDataV1Deprecated.yml#/listElementByDataspaceAndAnchor' + + /{apiVersion}/dataspaces/{dataspace-name}/anchors/{anchor-name}/list-nodes: $ref: 'cpsData.yml#/listElementByDataspaceAndAnchor' - /v1/dataspaces/{dataspace-name}/anchors/{anchor-name}/nodes/query: + /{apiVersion}/dataspaces/{dataspace-name}/anchors/{anchor-name}/nodes/query: $ref: 'cpsQuery.yml#/nodesByDataspaceAndAnchorAndCpsPath' security: 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 285a15c6a3..b8ba08915c 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 @@ -69,13 +69,25 @@ public class AdminRestController implements CpsAdminApi { } /** + * Create a dataspace without returning any response body. + * + * @param dataspaceName dataspace name + * @return a {@Link ResponseEntity} of created dataspace name & {@link HttpStatus} CREATED + */ + @Override + public ResponseEntity<Void> createDataspaceV2(@NotNull @Valid final String dataspaceName) { + cpsAdminService.createDataspace(dataspaceName); + return new ResponseEntity<>(HttpStatus.CREATED); + } + + /** * Delete a dataspace. * * @param dataspaceName name of dataspace to be deleted * @return a {@Link ResponseEntity} of {@link HttpStatus} NO_CONTENT */ @Override - public ResponseEntity<Void> deleteDataspace(final String dataspaceName) { + public ResponseEntity<Void> deleteDataspace(final String apiVersion, final String dataspaceName) { cpsAdminService.deleteDataspace(dataspaceName); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } @@ -96,14 +108,31 @@ public class AdminRestController implements CpsAdminApi { } /** + * Create a {@link SchemaSet}. + * + * @param multipartFile multipart file + * @param schemaSetName schemaset name + * @param dataspaceName dataspace name + * @return a {@Link ResponseEntity} of created schema set without any response body & {@link HttpStatus} CREATED + */ + @Override + public ResponseEntity<Void> createSchemaSetV2(@NotNull @Valid final String schemaSetName, + final String dataspaceName, @Valid final MultipartFile multipartFile) { + cpsModuleService.createSchemaSet(dataspaceName, schemaSetName, extractYangResourcesMap(multipartFile)); + return new ResponseEntity<>(HttpStatus.CREATED); + } + + /** * Get {@link SchemaSetDetails} based on dataspace name & {@link SchemaSet} name. * + * @param apiVersion api version * @param dataspaceName dataspace name * @param schemaSetName schemaset name * @return a {@Link ResponseEntity} of {@Link SchemaSetDetails} & {@link HttpStatus} OK */ @Override - public ResponseEntity<SchemaSetDetails> getSchemaSet(final String dataspaceName, final String schemaSetName) { + public ResponseEntity<SchemaSetDetails> getSchemaSet(final String apiVersion, + final String dataspaceName, final String schemaSetName) { final var schemaSet = cpsModuleService.getSchemaSet(dataspaceName, schemaSetName); final var schemaSetDetails = cpsRestInputMapper.toSchemaSetDetails(schemaSet); return new ResponseEntity<>(schemaSetDetails, HttpStatus.OK); @@ -112,11 +141,12 @@ public class AdminRestController implements CpsAdminApi { /** * Get list of schema sets for a given dataspace name. * + * @param apiVersion api version * @param dataspaceName dataspace name * @return a {@Link ResponseEntity} of schema sets & {@link HttpStatus} OK */ @Override - public ResponseEntity<List<SchemaSetDetails>> getSchemaSets(final String dataspaceName) { + public ResponseEntity<List<SchemaSetDetails>> getSchemaSets(final String apiVersion, final String dataspaceName) { final Collection<SchemaSet> schemaSets = cpsModuleService.getSchemaSets(dataspaceName); final List<SchemaSetDetails> schemaSetDetails = schemaSets.stream().map(cpsRestInputMapper::toSchemaSetDetails) .collect(Collectors.toList()); @@ -126,12 +156,14 @@ public class AdminRestController implements CpsAdminApi { /** * Delete a {@link SchemaSet} based on given dataspace name & schemaset name. * + * @param apiVersion api version * @param dataspaceName dataspace name * @param schemaSetName schemaset name * @return a {@Link ResponseEntity} of {@link HttpStatus} NO_CONTENT */ @Override - public ResponseEntity<Void> deleteSchemaSet(final String dataspaceName, final String schemaSetName) { + public ResponseEntity<Void> deleteSchemaSet(final String apiVersion, + final String dataspaceName, final String schemaSetName) { cpsModuleService.deleteSchemaSet(dataspaceName, schemaSetName, CASCADE_DELETE_PROHIBITED); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } @@ -152,14 +184,31 @@ public class AdminRestController implements CpsAdminApi { } /** + * Create an anchor. + * + * @param dataspaceName dataspace name + * @param schemaSetName schema set name + * @param anchorName anchorName + * @return a ResponseEntity without response body & {@link HttpStatus} CREATED + */ + @Override + public ResponseEntity<Void> createAnchorV2(final String dataspaceName, @NotNull @Valid final String schemaSetName, + @NotNull @Valid final String anchorName) { + cpsAdminService.createAnchor(dataspaceName, schemaSetName, anchorName); + return new ResponseEntity<>(HttpStatus.CREATED); + } + + /** * Delete an {@link Anchor} based on given dataspace name & anchor name. * + * @param apiVersion api version * @param dataspaceName dataspace name * @param anchorName anchor name * @return a {@Link ResponseEntity} of {@link HttpStatus} NO_CONTENT */ @Override - public ResponseEntity<Void> deleteAnchor(final String dataspaceName, final String anchorName) { + public ResponseEntity<Void> deleteAnchor(final String apiVersion, + final String dataspaceName, final String anchorName) { cpsAdminService.deleteAnchor(dataspaceName, anchorName); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } @@ -167,12 +216,14 @@ public class AdminRestController implements CpsAdminApi { /** * Get an {@link Anchor} based on given dataspace name & anchor name. * + * @param apiVersion api version * @param dataspaceName dataspace name * @param anchorName anchor name * @return a {@Link ResponseEntity} of an {@Link AnchorDetails} & {@link HttpStatus} OK */ @Override - public ResponseEntity<AnchorDetails> getAnchor(final String dataspaceName, final String anchorName) { + public ResponseEntity<AnchorDetails> getAnchor(final String apiVersion, + final String dataspaceName, final String anchorName) { final var anchor = cpsAdminService.getAnchor(dataspaceName, anchorName); final var anchorDetails = cpsRestInputMapper.toAnchorDetails(anchor); return new ResponseEntity<>(anchorDetails, HttpStatus.OK); @@ -181,11 +232,13 @@ public class AdminRestController implements CpsAdminApi { /** * Get all {@link Anchor} based on given dataspace name. * + * @param apiVersion api version * @param dataspaceName dataspace name * @return a {@Link ResponseEntity} of all {@Link AnchorDetails} & {@link HttpStatus} OK */ @Override - public ResponseEntity<List<AnchorDetails>> getAnchors(final String dataspaceName) { + public ResponseEntity<List<AnchorDetails>> getAnchors(final String apiVersion, + final String dataspaceName) { final Collection<Anchor> anchors = cpsAdminService.getAnchors(dataspaceName); final List<AnchorDetails> anchorDetails = anchors.stream().map(cpsRestInputMapper::toAnchorDetails) .collect(Collectors.toList()); @@ -193,7 +246,7 @@ public class AdminRestController implements CpsAdminApi { } @Override - public ResponseEntity<List<DataspaceDetails>> getAllDataspaces() { + public ResponseEntity<List<DataspaceDetails>> getAllDataspaces(final String apiVersion) { final Collection<Dataspace> dataspaces = cpsAdminService.getAllDataspaces(); final List<DataspaceDetails> dataspaceDetails = dataspaces.stream().map(cpsRestInputMapper::toDataspaceDetails) .collect(Collectors.toList()); @@ -201,7 +254,7 @@ public class AdminRestController implements CpsAdminApi { } @Override - public ResponseEntity<DataspaceDetails> getDataspace(final String dataspaceName) { + public ResponseEntity<DataspaceDetails> getDataspace(final String apiVersion, final String dataspaceName) { final Dataspace dataspace = cpsAdminService.getDataspace(dataspaceName); final DataspaceDetails dataspaceDetails = cpsRestInputMapper.toDataspaceDetails(dataspace); return new ResponseEntity<>(dataspaceDetails, HttpStatus.OK); 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 fdce9bee6b..30bed12775 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 @@ -3,6 +3,8 @@ * Copyright (C) 2020-2022 Bell Canada. * Modifications Copyright (C) 2021 Pantheon.tech * Modifications Copyright (C) 2021-2022 Nordix Foundation + * Modifications Copyright (C) 2022 TechMahindra Ltd. + * Modifications Copyright (C) 2022 Deutsche Telekom AG * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,11 +33,14 @@ import org.onap.cps.api.CpsDataService; import org.onap.cps.rest.api.CpsDataApi; import org.onap.cps.spi.FetchDescendantsOption; import org.onap.cps.spi.model.DataNode; +import org.onap.cps.utils.ContentType; import org.onap.cps.utils.DataMapUtils; import org.onap.cps.utils.JsonObjectMapper; import org.onap.cps.utils.PrefixResolver; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -53,21 +58,26 @@ public class DataRestController implements CpsDataApi { private final PrefixResolver prefixResolver; @Override - public ResponseEntity<String> createNode(final String dataspaceName, final String anchorName, - final Object jsonData, final String parentNodeXpath, final String observedTimestamp) { - final String jsonDataAsString = jsonObjectMapper.asJsonString(jsonData); + public ResponseEntity<String> createNode(@RequestHeader(value = "Content-Type") final String contentTypeHeader, + final String apiVersion, + final String dataspaceName, final String anchorName, + final String nodeData, final String parentNodeXpath, + final String observedTimestamp) { + final ContentType contentType = contentTypeHeader.contains(MediaType.APPLICATION_XML_VALUE) ? ContentType.XML + : ContentType.JSON; if (isRootXpath(parentNodeXpath)) { - cpsDataService.saveData(dataspaceName, anchorName, jsonDataAsString, - toOffsetDateTime(observedTimestamp)); + cpsDataService.saveData(dataspaceName, anchorName, nodeData, + toOffsetDateTime(observedTimestamp), contentType); } else { cpsDataService.saveData(dataspaceName, anchorName, parentNodeXpath, - jsonDataAsString, toOffsetDateTime(observedTimestamp)); + nodeData, toOffsetDateTime(observedTimestamp), contentType); } return new ResponseEntity<>(HttpStatus.CREATED); } @Override - public ResponseEntity<Void> deleteDataNode(final String dataspaceName, final String anchorName, + public ResponseEntity<Void> deleteDataNode(final String apiVersion, + final String dataspaceName, final String anchorName, final String xpath, final String observedTimestamp) { cpsDataService.deleteDataNode(dataspaceName, anchorName, xpath, toOffsetDateTime(observedTimestamp)); @@ -75,7 +85,7 @@ public class DataRestController implements CpsDataApi { } @Override - public ResponseEntity<String> addListElements(final String parentNodeXpath, + public ResponseEntity<String> addListElements(final String parentNodeXpath, final String apiVersion, final String dataspaceName, final String anchorName, final Object jsonData, final String observedTimestamp) { cpsDataService.saveListElements(dataspaceName, anchorName, parentNodeXpath, jsonObjectMapper.asJsonString(jsonData), toOffsetDateTime(observedTimestamp)); @@ -83,8 +93,8 @@ public class DataRestController implements CpsDataApi { } @Override - public ResponseEntity<Object> getNodeByDataspaceAndAnchor(final String dataspaceName, final String anchorName, - final String xpath, final Boolean includeDescendants) { + public ResponseEntity<Object> getNodeByDataspaceAndAnchor(final String apiVersion, + final String dataspaceName, final String anchorName, final String xpath, final Boolean includeDescendants) { final FetchDescendantsOption fetchDescendantsOption = Boolean.TRUE.equals(includeDescendants) ? FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS : FetchDescendantsOption.OMIT_DESCENDANTS; final DataNode dataNode = cpsDataService.getDataNode(dataspaceName, anchorName, xpath, @@ -94,7 +104,7 @@ public class DataRestController implements CpsDataApi { } @Override - public ResponseEntity<Object> updateNodeLeaves(final String dataspaceName, + public ResponseEntity<Object> updateNodeLeaves(final String apiVersion, final String dataspaceName, final String anchorName, final Object jsonData, final String parentNodeXpath, final String observedTimestamp) { cpsDataService.updateNodeLeaves(dataspaceName, anchorName, parentNodeXpath, jsonObjectMapper.asJsonString(jsonData), toOffsetDateTime(observedTimestamp)); @@ -102,7 +112,8 @@ public class DataRestController implements CpsDataApi { } @Override - public ResponseEntity<Object> replaceNode(final String dataspaceName, final String anchorName, + public ResponseEntity<Object> replaceNode(final String apiVersion, + final String dataspaceName, final String anchorName, final Object jsonData, final String parentNodeXpath, final String observedTimestamp) { cpsDataService .updateDataNodeAndDescendants(dataspaceName, anchorName, parentNodeXpath, @@ -112,7 +123,7 @@ public class DataRestController implements CpsDataApi { @Override public ResponseEntity<Object> replaceListContent(final String parentNodeXpath, - final String dataspaceName, final String anchorName, final Object jsonData, + final String apiVersion, final String dataspaceName, final String anchorName, final Object jsonData, final String observedTimestamp) { cpsDataService.replaceListContent(dataspaceName, anchorName, parentNodeXpath, jsonObjectMapper.asJsonString(jsonData), toOffsetDateTime(observedTimestamp)); diff --git a/cps-rest/src/main/java/org/onap/cps/rest/controller/QueryRestController.java b/cps-rest/src/main/java/org/onap/cps/rest/controller/QueryRestController.java index 577ad9c262..3e162ae683 100644 --- a/cps-rest/src/main/java/org/onap/cps/rest/controller/QueryRestController.java +++ b/cps-rest/src/main/java/org/onap/cps/rest/controller/QueryRestController.java @@ -2,6 +2,7 @@ * ============LICENSE_START======================================================= * Copyright (C) 2021-2022 Nordix Foundation * Modifications Copyright (C) 2022 Bell Canada. + * Modifications Copyright (C) 2022 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -48,8 +49,8 @@ public class QueryRestController implements CpsQueryApi { private final PrefixResolver prefixResolver; @Override - public ResponseEntity<Object> getNodesByDataspaceAndAnchorAndCpsPath(final String dataspaceName, - final String anchorName, final String cpsPath, final Boolean includeDescendants) { + public ResponseEntity<Object> getNodesByDataspaceAndAnchorAndCpsPath(final String apiVersion, + final String dataspaceName, final String anchorName, final String cpsPath, final Boolean includeDescendants) { final FetchDescendantsOption fetchDescendantsOption = Boolean.TRUE.equals(includeDescendants) ? FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS : FetchDescendantsOption.OMIT_DESCENDANTS; final Collection<DataNode> dataNodes = 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 7120ce49f3..f81efd6698 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 @@ -73,21 +73,23 @@ class AdminRestControllerSpec extends Specification { def anchor = new Anchor(name: anchorName, dataspaceName: dataspaceName, schemaSetName: schemaSetName) def dataspace = new Dataspace(name: dataspaceName) - def 'Create new dataspace.'() { - given: 'an endpoint' - def createDataspaceEndpoint = "$basePath/v1/dataspaces" + def 'Create new dataspace with #scenario.'() { when: 'post is invoked' def response = mvc.perform( - post(createDataspaceEndpoint) + post("/cps/api/${apiVersion}/dataspaces") .param('dataspace-name', dataspaceName)) .andReturn().response then: 'service method is invoked with expected parameters' 1 * mockCpsAdminService.createDataspace(dataspaceName) and: 'dataspace is create successfully' response.status == HttpStatus.CREATED.value() - } - + assert response.getContentAsString() == expectedResponseBody + where: 'following cases are tested' + scenario | apiVersion || expectedResponseBody + 'V1 API' | 'v1' || 'my_dataspace' + 'V2 API' | 'v2' || '' + } def 'Create dataspace over existing with same name.'() { given: 'an endpoint' def createDataspaceEndpoint = "$basePath/v1/dataspaces" @@ -129,16 +131,14 @@ class AdminRestControllerSpec extends Specification { response.getContentAsString().contains("dataspace-test2") } - def 'Create schema set from yang file.'() { + def 'Create schema set from yang file with #scenario.'() { def yangResourceMapCapture given: 'single yang file' def multipartFile = createMultipartFile("filename.yang", "content") - and: 'an endpoint' - def schemaSetEndpoint = "$basePath/v1/dataspaces/$dataspaceName/schema-sets" when: 'file uploaded with schema set create request' def response = mvc.perform( - multipart(schemaSetEndpoint) + multipart("/cps/api/${apiVersion}/dataspaces/my_dataspace/schema-sets") .file(multipartFile) .param('schema-set-name', schemaSetName)) .andReturn().response @@ -147,19 +147,22 @@ class AdminRestControllerSpec extends Specification { { args -> yangResourceMapCapture = args[2] } yangResourceMapCapture['filename.yang'] == 'content' and: 'response code indicates success' - response.status == HttpStatus.CREATED.value() + assert response.status == HttpStatus.CREATED.value() + assert response.getContentAsString() == expectedResponseBody + where: 'following cases are tested' + scenario | apiVersion || expectedResponseBody + 'V1 API' | 'v1' || 'my_schema_set' + 'V2 API' | 'v2' || '' } - def 'Create schema set from zip archive.'() { + def 'Create schema set from zip archive with #scenario.'() { def yangResourceMapCapture given: 'zip archive with multiple .yang files inside' def multipartFile = createZipMultipartFileFromResource("/yang-files-set.zip") - and: 'an endpoint' - def schemaSetEndpoint = "$basePath/v1/dataspaces/$dataspaceName/schema-sets" when: 'file uploaded with schema set create request' def response = mvc.perform( - multipart(schemaSetEndpoint) + multipart("/cps/api/${apiVersion}/dataspaces/my_dataspace/schema-sets") .file(multipartFile) .param('schema-set-name', schemaSetName)) .andReturn().response @@ -169,25 +172,33 @@ class AdminRestControllerSpec extends Specification { yangResourceMapCapture['assembly.yang'] == "fake assembly content 1\n" yangResourceMapCapture['component.yang'] == "fake component content 1\n" and: 'response code indicates success' - response.status == HttpStatus.CREATED.value() + assert response.status == HttpStatus.CREATED.value() + assert response.getContentAsString() == expectedResponseBody + where: 'following cases are tested' + scenario | apiVersion || expectedResponseBody + 'V1 API' | 'v1' || 'my_schema_set' + 'V2 API' | 'v2' || '' } - def 'Create a schema set from a yang file that is greater than 1MB.'() { + def 'Create a schema set from a yang file that is greater than 1MB #scenario.'() { given: 'a yang file greater than 1MB' def multipartFile = createMultipartFileFromResource("/model-over-1mb.yang") - and: 'an endpoint' - def schemaSetEndpoint = "$basePath/v1/dataspaces/$dataspaceName/schema-sets" when: 'a file is uploaded to the create schema set endpoint' def response = mvc.perform( - multipart(schemaSetEndpoint) + multipart("/cps/api/${apiVersion}/dataspaces/my_dataspace/schema-sets") .file(multipartFile) .param('schema-set-name', schemaSetName)) .andReturn().response then: 'the associated service method is invoked' 1 * mockCpsModuleService.createSchemaSet(dataspaceName, schemaSetName, _) and: 'the response code indicates success' - response.status == HttpStatus.CREATED.value() + assert response.status == HttpStatus.CREATED.value() + assert response.getContentAsString() == expectedResponseBody + where: 'following cases are tested' + scenario | apiVersion || expectedResponseBody + 'V1 API' | 'v1' || 'my_schema_set' + 'V2 API' | 'v2' || '' } def 'Create schema set from zip archive having #caseDescriptor.'() { @@ -293,23 +304,26 @@ class AdminRestControllerSpec extends Specification { '"my_schema_set"},{"dataspaceName":"my_dataspace","moduleReferences":[],"name":"test-schemaset"}]' } - def 'Create Anchor.'() { + def 'Create Anchor with #scenario.'() { given: 'request parameters' def requestParams = new LinkedMultiValueMap<>() requestParams.add('schema-set-name', schemaSetName) requestParams.add('anchor-name', anchorName) - and: 'an endpoint' - def anchorEndpoint = "$basePath/v1/dataspaces/$dataspaceName/anchors" when: 'post is invoked' def response = mvc.perform( - post(anchorEndpoint).contentType(MediaType.APPLICATION_JSON) + post("/cps/api/${apiVersion}/dataspaces/my_dataspace/anchors") + .contentType(MediaType.APPLICATION_JSON) .params(requestParams as MultiValueMap)) - .andReturn().response + .andReturn().response then: 'anchor is created successfully' 1 * mockCpsAdminService.createAnchor(dataspaceName, schemaSetName, anchorName) - response.status == HttpStatus.CREATED.value() - response.getContentAsString().contains(anchorName) + assert response.status == HttpStatus.CREATED.value() + assert response.getContentAsString() == expectedResponseBody + where: 'following cases are tested' + scenario | apiVersion || expectedResponseBody + 'V1 API' | 'v1' || 'my_anchor' + 'V2 API' | 'v2' || '' } def 'Get existing anchor.'() { 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 53da3e6599..94f62f8c22 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 @@ -3,6 +3,7 @@ * Copyright (C) 2021-2022 Nordix Foundation * Modifications Copyright (C) 2021 Pantheon.tech * Modifications Copyright (C) 2021-2022 Bell Canada. + * Modifications Copyright (C) 2022 Deutsche Telekom AG * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +27,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import org.onap.cps.api.CpsDataService import org.onap.cps.spi.model.DataNode import org.onap.cps.spi.model.DataNodeBuilder +import org.onap.cps.utils.ContentType import org.onap.cps.utils.DateTimeUtility import org.onap.cps.utils.JsonObjectMapper import org.onap.cps.utils.PrefixResolver @@ -69,10 +71,20 @@ class DataRestControllerSpec extends Specification { def dataspaceName = 'my_dataspace' def anchorName = 'my_anchor' def noTimestamp = null - def requestBody = '{"some-key" : "some-value","categories":[{"books":[{"authors":["Iain M. Banks"]}]}]}' + + @Shared + def requestBodyJson = '{"some-key":"some-value","categories":[{"books":[{"authors":["Iain M. Banks"]}]}]}' + + @Shared def expectedJsonData = '{"some-key":"some-value","categories":[{"books":[{"authors":["Iain M. Banks"]}]}]}' @Shared + def requestBodyXml = '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n<bookstore xmlns="org:onap:ccsdk:sample">\n</bookstore>' + + @Shared + def expectedXmlData = '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n<bookstore xmlns="org:onap:ccsdk:sample">\n</bookstore>' + + @Shared static DataNode dataNodeWithLeavesNoChildren = new DataNodeBuilder().withXpath('/xpath') .withLeaves([leaf: 'value', leafList: ['leaveListElement1', 'leaveListElement2']]).build() @@ -91,18 +103,20 @@ class DataRestControllerSpec extends Specification { def response = mvc.perform( post(endpoint) - .contentType(MediaType.APPLICATION_JSON) + .contentType(contentType) .param('xpath', parentNodeXpath) .content(requestBody) ).andReturn().response then: 'a created response is returned' response.status == HttpStatus.CREATED.value() then: 'the java API was called with the correct parameters' - 1 * mockCpsDataService.saveData(dataspaceName, anchorName, expectedJsonData, noTimestamp) + 1 * mockCpsDataService.saveData(dataspaceName, anchorName, expectedData, noTimestamp, expectedContentType) where: 'following xpath parameters are are used' - scenario | parentNodeXpath - 'no xpath parameter' | '' - 'xpath parameter point root' | '/' + scenario | parentNodeXpath | contentType | expectedContentType | requestBody | expectedData + 'JSON content: no xpath parameter' | '' | MediaType.APPLICATION_JSON | ContentType.JSON | requestBodyJson | expectedJsonData + 'JSON content: xpath parameter point root' | '/' | MediaType.APPLICATION_JSON | ContentType.JSON | requestBodyJson | expectedJsonData + 'XML content: no xpath parameter' | '' | MediaType.APPLICATION_XML | ContentType.XML | requestBodyXml | expectedXmlData + 'XML content: xpath parameter point root' | '/' | MediaType.APPLICATION_XML | ContentType.XML | requestBodyXml | expectedXmlData } def 'Create a node with observed-timestamp'() { @@ -112,30 +126,31 @@ class DataRestControllerSpec extends Specification { def response = mvc.perform( post(endpoint) - .contentType(MediaType.APPLICATION_JSON) + .contentType(contentType) .param('xpath', '') .param('observed-timestamp', observedTimestamp) - .content(requestBody) + .content(content) ).andReturn().response then: 'a created response is returned' response.status == expectedHttpStatus.value() then: 'the java API was called with the correct parameters' - expectedApiCount * mockCpsDataService.saveData(dataspaceName, anchorName, expectedJsonData, - { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) }) + expectedApiCount * mockCpsDataService.saveData(dataspaceName, anchorName, expectedData, + { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) }, expectedContentType) where: - scenario | observedTimestamp || expectedApiCount | expectedHttpStatus - 'with observed-timestamp' | '2021-03-03T23:59:59.999-0400' || 1 | HttpStatus.CREATED - 'with invalid observed-timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST + scenario | observedTimestamp | contentType | content || expectedApiCount | expectedHttpStatus | expectedData | expectedContentType + 'with observed-timestamp JSON' | '2021-03-03T23:59:59.999-0400' | MediaType.APPLICATION_JSON | requestBodyJson || 1 | HttpStatus.CREATED | expectedJsonData | ContentType.JSON + 'with observed-timestamp XML' | '2021-03-03T23:59:59.999-0400' | MediaType.APPLICATION_XML | requestBodyXml || 1 | HttpStatus.CREATED | expectedXmlData | ContentType.XML + 'with invalid observed-timestamp' | 'invalid' | MediaType.APPLICATION_JSON | requestBodyJson || 0 | HttpStatus.BAD_REQUEST | expectedJsonData | ContentType.JSON } - def 'Create a child node'() { + def 'Create a child node #scenario'() { given: 'endpoint to create a node' def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes" and: 'parent node xpath' def parentNodeXpath = 'some xpath' when: 'post is invoked with datanode endpoint and json' def postRequestBuilder = post(endpoint) - .contentType(MediaType.APPLICATION_JSON) + .contentType(contentType) .param('xpath', parentNodeXpath) .content(requestBody) if (observedTimestamp != null) @@ -145,12 +160,14 @@ class DataRestControllerSpec extends Specification { then: 'a created response is returned' response.status == HttpStatus.CREATED.value() then: 'the java API was called with the correct parameters' - 1 * mockCpsDataService.saveData(dataspaceName, anchorName, parentNodeXpath, expectedJsonData, - DateTimeUtility.toOffsetDateTime(observedTimestamp)) + 1 * mockCpsDataService.saveData(dataspaceName, anchorName, parentNodeXpath, expectedData, + DateTimeUtility.toOffsetDateTime(observedTimestamp), expectedContentType) where: - scenario | observedTimestamp - 'with observed-timestamp' | '2021-03-03T23:59:59.999-0400' - 'without observed-timestamp' | null + scenario | observedTimestamp | contentType | requestBody | expectedData | expectedContentType + 'with observed-timestamp JSON' | '2021-03-03T23:59:59.999-0400' | MediaType.APPLICATION_JSON | requestBodyJson | expectedJsonData | ContentType.JSON + 'with observed-timestamp XML' | '2021-03-03T23:59:59.999-0400' | MediaType.APPLICATION_XML | requestBodyXml | expectedXmlData | ContentType.XML + 'without observed-timestamp JSON' | null | MediaType.APPLICATION_JSON | requestBodyJson | expectedJsonData | ContentType.JSON + 'without observed-timestamp XML' | null | MediaType.APPLICATION_XML | requestBodyXml | expectedXmlData | ContentType.XML } def 'Save list elements #scenario.'() { @@ -160,7 +177,7 @@ class DataRestControllerSpec extends Specification { def postRequestBuilder = post("$dataNodeBaseEndpoint/anchors/$anchorName/list-nodes") .contentType(MediaType.APPLICATION_JSON) .param('xpath', parentNodeXpath) - .content(requestBody) + .content(requestBodyJson) if (observedTimestamp != null) postRequestBuilder.param('observed-timestamp', observedTimestamp) def response = mvc.perform(postRequestBuilder).andReturn().response @@ -228,7 +245,7 @@ class DataRestControllerSpec extends Specification { mvc.perform( patch(endpoint) .contentType(MediaType.APPLICATION_JSON) - .content(requestBody) + .content(requestBodyJson) .param('xpath', inputXpath) ).andReturn().response then: 'the service method is invoked with expected parameters' @@ -250,7 +267,7 @@ class DataRestControllerSpec extends Specification { mvc.perform( patch(endpoint) .contentType(MediaType.APPLICATION_JSON) - .content(requestBody) + .content(requestBodyJson) .param('xpath', '/') .param('observed-timestamp', observedTimestamp) ).andReturn().response @@ -273,7 +290,7 @@ class DataRestControllerSpec extends Specification { mvc.perform( put(endpoint) .contentType(MediaType.APPLICATION_JSON) - .content(requestBody) + .content(requestBodyJson) .param('xpath', inputXpath)) .andReturn().response then: 'the service method is invoked with expected parameters' @@ -295,7 +312,7 @@ class DataRestControllerSpec extends Specification { mvc.perform( put(endpoint) .contentType(MediaType.APPLICATION_JSON) - .content(requestBody) + .content(requestBodyJson) .param('xpath', '') .param('observed-timestamp', observedTimestamp)) .andReturn().response @@ -315,7 +332,7 @@ class DataRestControllerSpec extends Specification { def putRequestBuilder = put("$dataNodeBaseEndpoint/anchors/$anchorName/list-nodes") .contentType(MediaType.APPLICATION_JSON) .param('xpath', 'parent xpath') - .content(requestBody) + .content(requestBodyJson) if (observedTimestamp != null) putRequestBuilder.param('observed-timestamp', observedTimestamp) def response = mvc.perform(putRequestBuilder).andReturn().response 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 ece3507f2d..0821b6bebc 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 @@ -4,6 +4,7 @@ * Modifications Copyright (C) 2021-2022 Nordix Foundation * Modifications Copyright (C) 2021 Bell Canada. * Modifications Copyright (C) 2022 TechMahindra Ltd. + * Modifications Copyright (C) 2022 Deutsche Telekom AG * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -167,13 +168,13 @@ class CpsRestExceptionHandlerSpec extends Specification { def 'Post request with #exceptionThrown.class.simpleName returns HTTP Status Bad Request.'() { given: '#exception is thrown the service indicating data is not found' - mockCpsDataService.saveData(_, _, _, _, _) >> { throw exceptionThrown } + mockCpsDataService.saveData(*_) >> { throw exceptionThrown } when: 'data update request is performed' def response = mvc.perform( post("$basePath/v1/dataspaces/dataspace-name/anchors/anchor-name/nodes") .contentType(MediaType.APPLICATION_JSON) .param('xpath', 'parent node xpath') - .content(groovy.json.JsonOutput.toJson('{"some-key" : "some-value"}')) + .content('{"some-key" : "some-value"}') ).andReturn().response then: 'response code indicates bad input parameters' response.status == BAD_REQUEST.value() @@ -181,18 +182,6 @@ class CpsRestExceptionHandlerSpec extends Specification { exceptionThrown << [new DataNodeNotFoundException('', ''), new NotFoundInDataspaceException('', '')] } - def 'Post request with invalid JSON payload returns HTTP Status Bad Request.'() { - when: 'data post request is performed' - def response = mvc.perform( - post("$basePath/v1/dataspaces/dataspace-name/anchors/anchor-name/nodes") - .contentType(MediaType.APPLICATION_JSON) - .param('xpath', 'parent node xpath') - .content('{') - ).andReturn().response - then: 'response code indicates bad input parameters' - response.status == BAD_REQUEST.value() - } - /* * NB. The test uses 'get anchors' endpoint and associated service method invocation * to test the exception handling. The endpoint chosen is not a subject of test. diff --git a/cps-ri/pom.xml b/cps-ri/pom.xml index b6fe284c2e..6cca8b4a5e 100644 --- a/cps-ri/pom.xml +++ b/cps-ri/pom.xml @@ -146,7 +146,7 @@ <artifactId>maven-surefire-plugin</artifactId>
<configuration>
<excludes>
- <exclude>org.onap.cps.spi.performance.CpsToDataNodePerfTest</exclude>
+ <exclude>%regex[.*PerfTest.*]</exclude>
</excludes>
</configuration>
</plugin>
diff --git a/cps-ri/src/main/java/org/onap/cps/spi/entities/FragmentEntityArranger.java b/cps-ri/src/main/java/org/onap/cps/spi/entities/FragmentEntityArranger.java index 27891c525e..6b1162d11b 100644 --- a/cps-ri/src/main/java/org/onap/cps/spi/entities/FragmentEntityArranger.java +++ b/cps-ri/src/main/java/org/onap/cps/spi/entities/FragmentEntityArranger.java @@ -31,14 +31,13 @@ import lombok.NoArgsConstructor; public class FragmentEntityArranger { /** - * Convert a collection of (related) FragmentExtracts into a FragmentEntity (tree) with descendants. - * Multiple top level nodes not yet support. If found only the first top level element is returned + * Convert a collection of (related) FragmentExtracts into FragmentEntities (trees) with descendants. * * @param anchorEntity the anchor(entity) all the fragments belong to * @param fragmentExtracts FragmentExtracts to convert - * @return a FragmentEntity (tree) with descendants, null if none found. + * @return a collection of FragmentEntities (trees) with descendants. */ - public static FragmentEntity toFragmentEntityTree(final AnchorEntity anchorEntity, + public static Collection<FragmentEntity> toFragmentEntityTrees(final AnchorEntity anchorEntity, final Collection<FragmentExtract> fragmentExtracts) { final Map<Long, FragmentEntity> fragmentEntityPerId = new HashMap<>(); for (final FragmentExtract fragmentExtract : fragmentExtracts) { @@ -61,7 +60,8 @@ public class FragmentEntityArranger { return fragmentEntity; } - private static FragmentEntity reuniteChildrenWithTheirParents(final Map<Long, FragmentEntity> fragmentEntityPerId) { + private static Collection<FragmentEntity> reuniteChildrenWithTheirParents( + final Map<Long, FragmentEntity> fragmentEntityPerId) { final Collection<FragmentEntity> fragmentEntitiesWithoutParentInResultSet = new HashSet<>(); for (final FragmentEntity fragmentEntity : fragmentEntityPerId.values()) { final FragmentEntity parentFragmentEntity = fragmentEntityPerId.get(fragmentEntity.getParentId()); @@ -71,7 +71,7 @@ public class FragmentEntityArranger { parentFragmentEntity.getChildFragments().add(fragmentEntity); } } - return fragmentEntitiesWithoutParentInResultSet.stream().findFirst().orElse(null); + return fragmentEntitiesWithoutParentInResultSet; } } 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 c725b4224e..06068e391b 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 @@ -3,6 +3,7 @@ * Copyright (C) 2021-2022 Nordix Foundation * Modifications Copyright (C) 2021 Pantheon.tech * Modifications Copyright (C) 2020-2022 Bell Canada. + * Modifications Copyright (C) 2022 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -60,6 +61,7 @@ import org.onap.cps.spi.model.DataNode; import org.onap.cps.spi.model.DataNodeBuilder; import org.onap.cps.spi.repository.AnchorRepository; import org.onap.cps.spi.repository.DataspaceRepository; +import org.onap.cps.spi.repository.FragmentQueryBuilder; import org.onap.cps.spi.repository.FragmentRepository; import org.onap.cps.spi.utils.SessionManager; import org.onap.cps.utils.JsonObjectMapper; @@ -78,9 +80,6 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService private final SessionManager sessionManager; private static final String REG_EX_FOR_OPTIONAL_LIST_INDEX = "(\\[@[\\s\\S]+?]){0,1})"; - private static final Pattern REG_EX_PATTERN_FOR_LIST_ELEMENT_KEY_PREDICATE = - Pattern.compile("\\[(\\@([^\\/]{0,9999}))\\]$"); - private static final String TOP_LEVEL_MODULE_PREFIX_PROPERTY_NAME = "topLevelModulePrefix"; @Override public void addChildDataNode(final String dataspaceName, final String anchorName, final String parentNodeXpath, @@ -89,6 +88,12 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService } @Override + public void addChildDataNodes(final String dataspaceName, final String anchorName, + final String parentNodeXpath, final Collection<DataNode> dataNodes) { + addChildrenDataNodes(dataspaceName, anchorName, parentNodeXpath, dataNodes); + } + + @Override public void addListElements(final String dataspaceName, final String anchorName, final String parentNodeXpath, final Collection<DataNode> newListElements) { addChildrenDataNodes(dataspaceName, anchorName, parentNodeXpath, newListElements); @@ -167,14 +172,45 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService @Override public void storeDataNode(final String dataspaceName, final String anchorName, final DataNode dataNode) { + storeDataNodes(dataspaceName, anchorName, Collections.singletonList(dataNode)); + } + + @Override + public void storeDataNodes(final String dataspaceName, final String anchorName, + final Collection<DataNode> dataNodes) { final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName); final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName); - final FragmentEntity fragmentEntity = convertToFragmentWithAllDescendants(dataspaceEntity, anchorEntity, - dataNode); + final List<FragmentEntity> fragmentEntities = new ArrayList<>(dataNodes.size()); try { - fragmentRepository.save(fragmentEntity); + for (final DataNode dataNode: dataNodes) { + final FragmentEntity fragmentEntity = convertToFragmentWithAllDescendants(dataspaceEntity, anchorEntity, + dataNode); + fragmentEntities.add(fragmentEntity); + } + fragmentRepository.saveAll(fragmentEntities); } catch (final DataIntegrityViolationException exception) { - throw AlreadyDefinedException.forDataNode(dataNode.getXpath(), anchorName, exception); + log.warn("Exception occurred : {} , While saving : {} data nodes, Retrying saving data nodes individually", + exception, dataNodes.size()); + storeDataNodesIndividually(dataspaceName, anchorName, dataNodes); + } + } + + private void storeDataNodesIndividually(final String dataspaceName, final String anchorName, + final Collection<DataNode> dataNodes) { + final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName); + final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName); + final Collection<String> failedXpaths = new HashSet<>(); + for (final DataNode dataNode: dataNodes) { + try { + final FragmentEntity fragmentEntity = convertToFragmentWithAllDescendants(dataspaceEntity, anchorEntity, + dataNode); + fragmentRepository.save(fragmentEntity); + } catch (final DataIntegrityViolationException e) { + failedXpaths.add(dataNode.getXpath()); + } + } + if (!failedXpaths.isEmpty()) { + throw new AlreadyDefinedExceptionBatch(failedXpaths); } } @@ -230,36 +266,37 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService final String xpath, final FetchDescendantsOption fetchDescendantsOption) { final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName); final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName); + final FragmentEntity fragmentEntity; if (isRootXpath(xpath)) { final List<FragmentExtract> fragmentExtracts = fragmentRepository.getTopLevelFragments(dataspaceEntity, anchorEntity); - return FragmentEntityArranger.toFragmentEntityTree(anchorEntity, - fragmentExtracts); + fragmentEntity = FragmentEntityArranger.toFragmentEntityTrees(anchorEntity, fragmentExtracts) + .stream().findFirst().orElse(null); } else { final String normalizedXpath = getNormalizedXpath(xpath); - final FragmentEntity fragmentEntity; if (FetchDescendantsOption.OMIT_DESCENDANTS.equals(fetchDescendantsOption)) { fragmentEntity = fragmentRepository.getByDataspaceAndAnchorAndXpath(dataspaceEntity, anchorEntity, normalizedXpath); } else { - fragmentEntity = buildFragmentEntityFromFragmentExtracts(anchorEntity, normalizedXpath); + fragmentEntity = buildFragmentEntitiesFromFragmentExtracts(anchorEntity, normalizedXpath) + .stream().findFirst().orElse(null); } - if (fragmentEntity == null) { - throw new DataNodeNotFoundException(dataspaceEntity.getName(), anchorEntity.getName(), xpath); - } - return fragmentEntity; } + if (fragmentEntity == null) { + throw new DataNodeNotFoundException(dataspaceEntity.getName(), anchorEntity.getName(), xpath); + } + return fragmentEntity; + } - private FragmentEntity buildFragmentEntityFromFragmentExtracts(final AnchorEntity anchorEntity, - final String normalizedXpath) { - final FragmentEntity fragmentEntity; + private Collection<FragmentEntity> buildFragmentEntitiesFromFragmentExtracts(final AnchorEntity anchorEntity, + final String normalizedXpath) { final List<FragmentExtract> fragmentExtracts = fragmentRepository.findByAnchorIdAndParentXpath(anchorEntity.getId(), normalizedXpath); log.debug("Fetched {} fragment entities by anchor {} and cps path {}.", fragmentExtracts.size(), anchorEntity.getName(), normalizedXpath); - fragmentEntity = FragmentEntityArranger.toFragmentEntityTree(anchorEntity, fragmentExtracts); - return fragmentEntity; + return FragmentEntityArranger.toFragmentEntityTrees(anchorEntity, fragmentExtracts); + } @Override @@ -273,32 +310,73 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService } catch (final PathParsingException e) { throw new CpsPathException(e.getMessage()); } - List<FragmentEntity> fragmentEntities = - fragmentRepository.findByAnchorAndCpsPath(anchorEntity.getId(), cpsPathQuery); + + Collection<FragmentEntity> fragmentEntities; + if (canUseRegexQuickFind(fetchDescendantsOption, cpsPathQuery)) { + return getDataNodesUsingRegexQuickFind(fetchDescendantsOption, anchorEntity, cpsPathQuery); + } + fragmentEntities = fragmentRepository.findByAnchorAndCpsPath(anchorEntity.getId(), cpsPathQuery); if (cpsPathQuery.hasAncestorAxis()) { - final Set<String> ancestorXpaths = processAncestorXpath(fragmentEntities, cpsPathQuery); - fragmentEntities = ancestorXpaths.isEmpty() ? Collections.emptyList() - : fragmentRepository.findAllByAnchorAndXpathIn(anchorEntity, ancestorXpaths); + fragmentEntities = getAncestorFragmentEntities(anchorEntity, cpsPathQuery, fragmentEntities); } - return createDataNodesFromFragmentEntities(fetchDescendantsOption, anchorEntity, - fragmentEntities); + return createDataNodesFromProxiedFragmentEntities(fetchDescendantsOption, anchorEntity, fragmentEntities); } - private List<DataNode> createDataNodesFromFragmentEntities(final FetchDescendantsOption fetchDescendantsOption, - final AnchorEntity anchorEntity, - final List<FragmentEntity> fragmentEntities) { - final List<DataNode> dataNodes = new ArrayList<>(fragmentEntities.size()); - for (final FragmentEntity proxiedFragmentEntity : fragmentEntities) { - final DataNode dataNode; + private static boolean canUseRegexQuickFind(final FetchDescendantsOption fetchDescendantsOption, + final CpsPathQuery cpsPathQuery) { + return fetchDescendantsOption.equals(FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) + && !cpsPathQuery.hasLeafConditions() + && !cpsPathQuery.hasTextFunctionCondition(); + } + + private List<DataNode> getDataNodesUsingRegexQuickFind(final FetchDescendantsOption fetchDescendantsOption, + final AnchorEntity anchorEntity, + final CpsPathQuery cpsPathQuery) { + Collection<FragmentEntity> fragmentEntities; + final String xpathRegex = FragmentQueryBuilder.getXpathSqlRegex(cpsPathQuery, true); + final List<FragmentExtract> fragmentExtracts = + fragmentRepository.quickFindWithDescendants(anchorEntity.getId(), xpathRegex); + fragmentEntities = FragmentEntityArranger.toFragmentEntityTrees(anchorEntity, fragmentExtracts); + if (cpsPathQuery.hasAncestorAxis()) { + fragmentEntities = getAncestorFragmentEntities(anchorEntity, cpsPathQuery, fragmentEntities); + } + return createDataNodesFromFragmentEntities(fetchDescendantsOption, fragmentEntities); + } + + private Collection<FragmentEntity> getAncestorFragmentEntities(final AnchorEntity anchorEntity, + final CpsPathQuery cpsPathQuery, + Collection<FragmentEntity> fragmentEntities) { + final Set<String> ancestorXpaths = processAncestorXpath(fragmentEntities, cpsPathQuery); + fragmentEntities = ancestorXpaths.isEmpty() ? Collections.emptyList() + : fragmentRepository.findAllByAnchorAndXpathIn(anchorEntity, ancestorXpaths); + return fragmentEntities; + } + + private List<DataNode> createDataNodesFromProxiedFragmentEntities( + final FetchDescendantsOption fetchDescendantsOption, + final AnchorEntity anchorEntity, + final Collection<FragmentEntity> proxiedFragmentEntities) { + final List<DataNode> dataNodes = new ArrayList<>(proxiedFragmentEntities.size()); + for (final FragmentEntity proxiedFragmentEntity : proxiedFragmentEntities) { if (FetchDescendantsOption.OMIT_DESCENDANTS.equals(fetchDescendantsOption)) { - dataNode = toDataNode(proxiedFragmentEntity, fetchDescendantsOption); + dataNodes.add(toDataNode(proxiedFragmentEntity, fetchDescendantsOption)); } else { final String normalizedXpath = getNormalizedXpath(proxiedFragmentEntity.getXpath()); - final FragmentEntity unproxiedFragmentEntity = buildFragmentEntityFromFragmentExtracts(anchorEntity, - normalizedXpath); - dataNode = toDataNode(unproxiedFragmentEntity, fetchDescendantsOption); + final Collection<FragmentEntity> unproxiedFragmentEntities = + buildFragmentEntitiesFromFragmentExtracts(anchorEntity, normalizedXpath); + for (final FragmentEntity unproxiedFragmentEntity : unproxiedFragmentEntities) { + dataNodes.add(toDataNode(unproxiedFragmentEntity, fetchDescendantsOption)); + } } - dataNodes.add(dataNode); + } + return Collections.unmodifiableList(dataNodes); + } + + private List<DataNode> createDataNodesFromFragmentEntities(final FetchDescendantsOption fetchDescendantsOption, + final Collection<FragmentEntity> fragmentEntities) { + final List<DataNode> dataNodes = new ArrayList<>(fragmentEntities.size()); + for (final FragmentEntity fragmentEntity : fragmentEntities) { + dataNodes.add(toDataNode(fragmentEntity, fetchDescendantsOption)); } return Collections.unmodifiableList(dataNodes); } @@ -329,7 +407,7 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService sessionManager.lockAnchor(sessionId, dataspaceName, anchorName, timeoutInMilliseconds); } - private static Set<String> processAncestorXpath(final List<FragmentEntity> fragmentEntities, + private static Set<String> processAncestorXpath(final Collection<FragmentEntity> fragmentEntities, final CpsPathQuery cpsPathQuery) { final Set<String> ancestorXpath = new HashSet<>(); final Pattern pattern = @@ -369,9 +447,11 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService @Override public void updateDataLeaves(final String dataspaceName, final String anchorName, final String xpath, - final Map<String, Serializable> leaves) { + final Map<String, Serializable> updateLeaves) { final FragmentEntity fragmentEntity = getFragmentWithoutDescendantsByXpath(dataspaceName, anchorName, xpath); - fragmentEntity.setAttributes(jsonObjectMapper.asJsonString(leaves)); + final String currentLeavesAsString = fragmentEntity.getAttributes(); + final String mergedLeaves = mergeLeaves(updateLeaves, currentLeavesAsString); + fragmentEntity.setAttributes(mergedLeaves); fragmentRepository.save(fragmentEntity); } @@ -512,13 +592,10 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService if (isRootContainerNodeXpath(targetXpath)) { parentNodeXpath = targetXpath; } else { - parentNodeXpath = targetXpath.substring(0, targetXpath.lastIndexOf('/')); + parentNodeXpath = CpsPathUtil.getNormalizedParentXpath(targetXpath); } parentFragmentEntity = getFragmentWithoutDescendantsByXpath(dataspaceName, anchorName, parentNodeXpath); - final String lastXpathElement = targetXpath.substring(targetXpath.lastIndexOf('/')); - final boolean isListElement = REG_EX_PATTERN_FOR_LIST_ELEMENT_KEY_PREDICATE - .matcher(lastXpathElement).find(); - if (isListElement) { + if (CpsPathUtil.isPathToListElement(targetXpath)) { targetDeleted = deleteDataNode(parentFragmentEntity, targetXpath); } else { targetDeleted = deleteAllListElements(parentFragmentEntity, targetXpath); @@ -619,4 +696,14 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService private static boolean isRootXpath(final String xpath) { return "/".equals(xpath) || "".equals(xpath); } + + private String mergeLeaves(final Map<String, Serializable> updateLeaves, final String currentLeavesAsString) { + final Map<String, Serializable> currentLeavesAsMap = currentLeavesAsString.isEmpty() + ? new HashMap<>() : jsonObjectMapper.convertJsonString(currentLeavesAsString, Map.class); + currentLeavesAsMap.putAll(updateLeaves); + if (currentLeavesAsMap.isEmpty()) { + return ""; + } + return jsonObjectMapper.asJsonString(currentLeavesAsMap); + } } diff --git a/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentQueryBuilder.java b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentQueryBuilder.java new file mode 100644 index 0000000000..f107928ca7 --- /dev/null +++ b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentQueryBuilder.java @@ -0,0 +1,139 @@ +/* + * ============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.HashMap; +import java.util.Map; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.Query; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.onap.cps.cpspath.parser.CpsPathPrefixType; +import org.onap.cps.cpspath.parser.CpsPathQuery; +import org.onap.cps.spi.entities.FragmentEntity; +import org.onap.cps.utils.JsonObjectMapper; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Slf4j +@Component +public class FragmentQueryBuilder { + private static final String REGEX_ABSOLUTE_PATH_PREFIX = ".*\\/"; + private static final String REGEX_OPTIONAL_LIST_INDEX_POSTFIX = "(\\[@(?!.*\\[).*?])?"; + private static final String REGEX_DESCENDANT_PATH_POSTFIX = "(\\/.*)?"; + private static final String REGEX_END_OF_INPUT = "$"; + + @PersistenceContext + private EntityManager entityManager; + + private final JsonObjectMapper jsonObjectMapper; + + /** + * Create a sql query to retrieve by anchor(id) and cps path. + * + * @param anchorId the id of the anchor + * @param cpsPathQuery the cps path query to be transformed into a sql query + * @return a executable query object + */ + public Query getQueryForAnchorAndCpsPath(final int anchorId, final CpsPathQuery cpsPathQuery) { + final StringBuilder sqlStringBuilder = new StringBuilder("SELECT * FROM FRAGMENT WHERE anchor_id = :anchorId"); + final Map<String, Object> queryParameters = new HashMap<>(); + queryParameters.put("anchorId", anchorId); + sqlStringBuilder.append(" AND xpath ~ :xpathRegex"); + final String xpathRegex = getXpathSqlRegex(cpsPathQuery, false); + queryParameters.put("xpathRegex", xpathRegex); + if (cpsPathQuery.hasLeafConditions()) { + sqlStringBuilder.append(" AND attributes @> :leafDataAsJson\\:\\:jsonb"); + queryParameters.put("leafDataAsJson", jsonObjectMapper.asJsonString( + cpsPathQuery.getLeavesData())); + } + + addTextFunctionCondition(cpsPathQuery, sqlStringBuilder, queryParameters); + final Query query = entityManager.createNativeQuery(sqlStringBuilder.toString(), FragmentEntity.class); + setQueryParameters(query, queryParameters); + return query; + } + + /** + * Create a regular expression (string) for xpath based on the given cps path query. + * + * @param cpsPathQuery the cps path query to determine the required regular expression + * @param includeDescendants include descendants yes or no + * @return a string representing the required regular expression + */ + public static String getXpathSqlRegex(final CpsPathQuery cpsPathQuery, final boolean includeDescendants) { + final StringBuilder xpathRegexBuilder = new StringBuilder(); + if (CpsPathPrefixType.ABSOLUTE.equals(cpsPathQuery.getCpsPathPrefixType())) { + xpathRegexBuilder.append(escapeXpath(cpsPathQuery.getXpathPrefix())); + } else { + xpathRegexBuilder.append(REGEX_ABSOLUTE_PATH_PREFIX); + xpathRegexBuilder.append(escapeXpath(cpsPathQuery.getDescendantName())); + } + xpathRegexBuilder.append(REGEX_OPTIONAL_LIST_INDEX_POSTFIX); + if (includeDescendants) { + xpathRegexBuilder.append(REGEX_DESCENDANT_PATH_POSTFIX); + } + xpathRegexBuilder.append(REGEX_END_OF_INPUT); + return xpathRegexBuilder.toString(); + } + + private static String escapeXpath(final String xpath) { + // See https://jira.onap.org/browse/CPS-500 for limitations of this basic escape mechanism + return xpath.replace("[@", "\\[@"); + } + + private static Integer getTextValueAsInt(final CpsPathQuery cpsPathQuery) { + try { + return Integer.parseInt(cpsPathQuery.getTextFunctionConditionValue()); + } catch (final NumberFormatException e) { + return null; + } + } + + private static void addTextFunctionCondition(final CpsPathQuery cpsPathQuery, + final StringBuilder sqlStringBuilder, + final Map<String, Object> queryParameters) { + if (cpsPathQuery.hasTextFunctionCondition()) { + sqlStringBuilder.append(" AND ("); + sqlStringBuilder.append("attributes @> jsonb_build_object(:textLeafName, :textValue)"); + sqlStringBuilder + .append(" OR attributes @> jsonb_build_object(:textLeafName, json_build_array(:textValue))"); + queryParameters.put("textLeafName", cpsPathQuery.getTextFunctionConditionLeafName()); + queryParameters.put("textValue", cpsPathQuery.getTextFunctionConditionValue()); + final Integer textValueAsInt = getTextValueAsInt(cpsPathQuery); + if (textValueAsInt != null) { + sqlStringBuilder.append(" OR attributes @> jsonb_build_object(:textLeafName, :textValueAsInt)"); + sqlStringBuilder + .append(" OR attributes @> jsonb_build_object(:textLeafName, json_build_array(:textValueAsInt))"); + queryParameters.put("textValueAsInt", textValueAsInt); + } + sqlStringBuilder.append(")"); + } + } + + private static void setQueryParameters(final Query query, final Map<String, Object> queryParameters) { + for (final Map.Entry<String, Object> queryParameter : queryParameters.entrySet()) { + query.setParameter(queryParameter.getKey(), queryParameter.getValue()); + } + } + +} 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 2c25a61a7e..c9461bf062 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 @@ -94,4 +94,12 @@ public interface FragmentRepository extends JpaRepository<FragmentEntity, Long>, nativeQuery = true)
List<FragmentExtract> findByAnchorIdAndParentXpath(@Param("anchorId") int anchorId,
@Param("parentXpath") String parentXpath);
+
+ @Query(value = "SELECT id, anchor_id AS anchorId, xpath, parent_id AS parentId,"
+ + " CAST(attributes AS TEXT) AS attributes"
+ + " FROM FRAGMENT WHERE anchor_id = :anchorId"
+ + " AND xpath ~ :xpathRegex",
+ nativeQuery = true)
+ List<FragmentExtract> quickFindWithDescendants(@Param("anchorId") int anchorId,
+ @Param("xpathRegex") String xpathRegex);
}
diff --git a/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryCpsPathQueryImpl.java b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryCpsPathQueryImpl.java index 1d61416cfd..6e8f05f017 100644 --- a/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryCpsPathQueryImpl.java +++ b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryCpsPathQueryImpl.java @@ -20,103 +20,32 @@ package org.onap.cps.spi.repository; -import java.util.HashMap; import java.util.List; -import java.util.Map; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.persistence.Query; import javax.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.onap.cps.cpspath.parser.CpsPathPrefixType; import org.onap.cps.cpspath.parser.CpsPathQuery; import org.onap.cps.spi.entities.FragmentEntity; -import org.onap.cps.utils.JsonObjectMapper; @RequiredArgsConstructor @Slf4j public class FragmentRepositoryCpsPathQueryImpl implements FragmentRepositoryCpsPathQuery { - public static final String REGEX_ABSOLUTE_PATH_PREFIX = ".*\\/"; - public static final String REGEX_OPTIONAL_LIST_INDEX_POSTFIX = "(\\[@(?!.*\\[).*?])?$"; - @PersistenceContext private EntityManager entityManager; - private final JsonObjectMapper jsonObjectMapper; + + private final FragmentQueryBuilder fragmentQueryBuilder; @Override @Transactional public List<FragmentEntity> findByAnchorAndCpsPath(final int anchorId, final CpsPathQuery cpsPathQuery) { - final StringBuilder sqlStringBuilder = new StringBuilder("SELECT * FROM FRAGMENT WHERE anchor_id = :anchorId"); - final Map<String, Object> queryParameters = new HashMap<>(); - queryParameters.put("anchorId", anchorId); - sqlStringBuilder.append(" AND xpath ~ :xpathRegex"); - final String xpathRegex = getXpathSqlRegex(cpsPathQuery); - queryParameters.put("xpathRegex", xpathRegex); - if (cpsPathQuery.hasLeafConditions()) { - sqlStringBuilder.append(" AND attributes @> :leafDataAsJson\\:\\:jsonb"); - queryParameters.put("leafDataAsJson", jsonObjectMapper.asJsonString( - cpsPathQuery.getLeavesData())); - } - - addTextFunctionCondition(cpsPathQuery, sqlStringBuilder, queryParameters); - final Query query = entityManager.createNativeQuery(sqlStringBuilder.toString(), FragmentEntity.class); - setQueryParameters(query, queryParameters); + final Query query = fragmentQueryBuilder.getQueryForAnchorAndCpsPath(anchorId, cpsPathQuery); final List<FragmentEntity> fragmentEntities = query.getResultList(); log.debug("Fetched {} fragment entities by anchor and cps path.", fragmentEntities.size()); return fragmentEntities; } - private static String getXpathSqlRegex(final CpsPathQuery cpsPathQuery) { - final StringBuilder xpathRegexBuilder = new StringBuilder(); - if (CpsPathPrefixType.ABSOLUTE.equals(cpsPathQuery.getCpsPathPrefixType())) { - xpathRegexBuilder.append(escapeXpath(cpsPathQuery.getXpathPrefix())); - } else { - xpathRegexBuilder.append(REGEX_ABSOLUTE_PATH_PREFIX); - xpathRegexBuilder.append(escapeXpath(cpsPathQuery.getDescendantName())); - } - xpathRegexBuilder.append(REGEX_OPTIONAL_LIST_INDEX_POSTFIX); - return xpathRegexBuilder.toString(); - } - - private static String escapeXpath(final String xpath) { - // See https://jira.onap.org/browse/CPS-500 for limitations of this basic escape mechanism - return xpath.replace("[@", "\\[@"); - } - - private static Integer getTextValueAsInt(final CpsPathQuery cpsPathQuery) { - try { - return Integer.parseInt(cpsPathQuery.getTextFunctionConditionValue()); - } catch (final NumberFormatException e) { - return null; - } - } - - private static void addTextFunctionCondition(final CpsPathQuery cpsPathQuery, final StringBuilder sqlStringBuilder, - final Map<String, Object> queryParameters) { - if (cpsPathQuery.hasTextFunctionCondition()) { - sqlStringBuilder.append(" AND ("); - sqlStringBuilder.append("attributes @> jsonb_build_object(:textLeafName, :textValue)"); - sqlStringBuilder - .append(" OR attributes @> jsonb_build_object(:textLeafName, json_build_array(:textValue))"); - queryParameters.put("textLeafName", cpsPathQuery.getTextFunctionConditionLeafName()); - queryParameters.put("textValue", cpsPathQuery.getTextFunctionConditionValue()); - final Integer textValueAsInt = getTextValueAsInt(cpsPathQuery); - if (textValueAsInt != null) { - sqlStringBuilder.append(" OR attributes @> jsonb_build_object(:textLeafName, :textValueAsInt)"); - sqlStringBuilder - .append(" OR attributes @> jsonb_build_object(:textLeafName, json_build_array(:textValueAsInt))"); - queryParameters.put("textValueAsInt", textValueAsInt); - } - sqlStringBuilder.append(")"); - } - } - - private static void setQueryParameters(final Query query, final Map<String, Object> queryParameters) { - for (final Map.Entry<String, Object> queryParameter : queryParameters.entrySet()) { - query.setParameter(queryParameter.getKey(), queryParameter.getValue()); - } - } - } diff --git a/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceQuery.java b/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceQuery.java index 5e4de7fec4..00e53aa00f 100644 --- a/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceQuery.java +++ b/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceQuery.java @@ -28,6 +28,5 @@ import org.onap.cps.spi.model.ModuleReference; */ public interface ModuleReferenceQuery { - Collection<ModuleReference> identifyNewModuleReferences( - final Collection<ModuleReference> moduleReferencesToCheck); + Collection<ModuleReference> identifyNewModuleReferences(final Collection<ModuleReference> moduleReferencesToCheck); } diff --git a/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceRepository.java b/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceRepository.java index f70e218373..ef701bc7dc 100644 --- a/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceRepository.java +++ b/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceRepository.java @@ -20,16 +20,10 @@ package org.onap.cps.spi.repository; -import java.util.Collection; import org.onap.cps.spi.entities.YangResourceEntity; -import org.onap.cps.spi.model.ModuleReference; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository -public interface ModuleReferenceRepository extends JpaRepository<YangResourceEntity, Long>, ModuleReferenceQuery { +public interface ModuleReferenceRepository extends JpaRepository<YangResourceEntity, Long>, ModuleReferenceQuery {} - Collection<ModuleReference> identifyNewModuleReferences( - final Collection<ModuleReference> moduleReferencesToCheck); - -}
\ No newline at end of file diff --git a/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceRepositoryImpl.java b/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceRepositoryImpl.java index 681bbcddec..48982d51ff 100644 --- a/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceRepositoryImpl.java +++ b/cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceRepositoryImpl.java @@ -23,8 +23,8 @@ package org.onap.cps.spi.repository; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.List; -import java.util.UUID; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import lombok.AllArgsConstructor; @@ -41,6 +41,8 @@ public class ModuleReferenceRepositoryImpl implements ModuleReferenceQuery { @PersistenceContext private EntityManager entityManager; + private TempTableCreator tempTableCreator; + @Override @SneakyThrows public Collection<ModuleReference> identifyNewModuleReferences( @@ -50,42 +52,18 @@ public class ModuleReferenceRepositoryImpl implements ModuleReferenceQuery { return Collections.emptyList(); } - final String tempTableName = "moduleReferencesToCheckTemp" - + UUID.randomUUID().toString().replace("-", ""); - - createTemporaryTable(tempTableName); - insertDataIntoTable(tempTableName, moduleReferencesToCheck); - - return identifyNewModuleReferencesForCmHandle(tempTableName); - } - - private void createTemporaryTable(final String tempTableName) { - final StringBuilder sqlStringBuilder = new StringBuilder("CREATE TEMPORARY TABLE " + tempTableName + "("); - sqlStringBuilder.append(" id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,"); - sqlStringBuilder.append(" module_name varchar NOT NULL,"); - sqlStringBuilder.append(" revision varchar NOT NULL"); - sqlStringBuilder.append(");"); - - entityManager.createNativeQuery(sqlStringBuilder.toString()).executeUpdate(); - } - - private void insertDataIntoTable(final String tempTableName, final Collection<ModuleReference> moduleReferences) { - final StringBuilder sqlStringBuilder = new StringBuilder("INSERT INTO " + tempTableName); - sqlStringBuilder.append(" (module_name, revision) "); - sqlStringBuilder.append(" VALUES "); - - for (final ModuleReference moduleReference : moduleReferences) { - sqlStringBuilder.append("('"); - sqlStringBuilder.append(moduleReference.getModuleName()); - sqlStringBuilder.append("', '"); - sqlStringBuilder.append(moduleReference.getRevision()); - sqlStringBuilder.append("'),"); + final Collection<List<String>> sqlData = new HashSet<>(moduleReferencesToCheck.size()); + for (final ModuleReference moduleReference : moduleReferencesToCheck) { + final List<String> row = new ArrayList<>(2); + row.add(moduleReference.getModuleName()); + row.add(moduleReference.getRevision()); + sqlData.add(row); } - // replace last ',' with ';' - sqlStringBuilder.replace(sqlStringBuilder.length() - 1, sqlStringBuilder.length(), ";"); + final String tempTableName = tempTableCreator.createTemporaryTable( + "moduleReferencesToCheckTemp", sqlData, "module_name", "revision"); - entityManager.createNativeQuery(sqlStringBuilder.toString()).executeUpdate(); + return identifyNewModuleReferencesForCmHandle(tempTableName); } private Collection<ModuleReference> identifyNewModuleReferencesForCmHandle(final String tempTableName) { 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 new file mode 100644 index 0000000000..8cad9f5e4c --- /dev/null +++ b/cps-ri/src/main/java/org/onap/cps/spi/repository/TempTableCreator.java @@ -0,0 +1,97 @@ +/*- + * ============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.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.UUID; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Transactional +@AllArgsConstructor +@Component +public class TempTableCreator { + + @PersistenceContext + private EntityManager entityManager; + + /** + * Create a uniquely named temporary table. + * + * @param prefix prefix for the table name (so you can recognize it) + * @param sqlData data to insert (strings only) the inner List present a row of data + * @param columnNames column names (in same order as data in rows in sqlData) + * @return a unique temporary table name with given prefix + */ + public String createTemporaryTable(final String prefix, + final Collection<List<String>> sqlData, + final String... columnNames) { + final String tempTableName = prefix + UUID.randomUUID().toString().replace("-", ""); + final StringBuilder sqlStringBuilder = new StringBuilder("CREATE TEMPORARY TABLE "); + sqlStringBuilder.append(tempTableName); + defineColumns(sqlStringBuilder, columnNames); + insertData(sqlStringBuilder, tempTableName, columnNames, sqlData); + entityManager.createNativeQuery(sqlStringBuilder.toString()).executeUpdate(); + return tempTableName; + } + + private static void defineColumns(final StringBuilder sqlStringBuilder, final String[] columnNames) { + sqlStringBuilder.append('('); + final Iterator<String> it = Arrays.stream(columnNames).iterator(); + while (it.hasNext()) { + final String columnName = it.next(); + sqlStringBuilder.append(" "); + sqlStringBuilder.append(columnName); + sqlStringBuilder.append(" varchar NOT NULL"); + if (it.hasNext()) { + sqlStringBuilder.append(","); + } + } + sqlStringBuilder.append(");"); + } + + private static void insertData(final StringBuilder sqlStringBuilder, + final String tempTableName, + final String[] columnNames, + final Collection<List<String>> sqlData) { + final Collection<String> sqlInserts = new HashSet<>(sqlData.size()); + for (final Collection<String> row : sqlData) { + sqlInserts.add("('" + String.join("','", row) + "')"); + } + sqlStringBuilder.append("INSERT INTO "); + sqlStringBuilder.append(tempTableName); + sqlStringBuilder.append(" ("); + sqlStringBuilder.append(String.join(",", columnNames)); + sqlStringBuilder.append(") VALUES "); + sqlStringBuilder.append(String.join(",", sqlInserts)); + sqlStringBuilder.append(";"); + } + +} 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 fbf414d2ad..cc2369d50e 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 @@ -3,6 +3,7 @@ * Copyright (C) 2021-2022 Nordix Foundation * Modifications Copyright (C) 2021 Pantheon.tech * Modifications Copyright (C) 2021-2022 Bell Canada. + * Modifications Copyright (C) 2022 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +27,6 @@ import com.google.common.collect.ImmutableSet import org.onap.cps.cpspath.parser.PathParsingException import org.onap.cps.spi.CpsDataPersistenceService import org.onap.cps.spi.entities.FragmentEntity -import org.onap.cps.spi.exceptions.AlreadyDefinedException import org.onap.cps.spi.exceptions.AlreadyDefinedExceptionBatch import org.onap.cps.spi.exceptions.AnchorNotFoundException import org.onap.cps.spi.exceptions.CpsAdminException @@ -38,6 +38,7 @@ import org.onap.cps.spi.model.DataNodeBuilder import org.onap.cps.utils.JsonObjectMapper import org.springframework.beans.factory.annotation.Autowired import org.springframework.test.context.jdbc.Sql + import javax.validation.ConstraintViolationException import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS @@ -48,25 +49,29 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase { @Autowired CpsDataPersistenceService objectUnderTest - static final JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(new ObjectMapper()) - static final DataNodeBuilder dataNodeBuilder = new DataNodeBuilder() + static JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(new ObjectMapper()) + static DataNodeBuilder dataNodeBuilder = new DataNodeBuilder() static final String SET_DATA = '/data/fragment.sql' - static final int DATASPACE_1001_ID = 1001L - static final int ANCHOR_3003_ID = 3003L - static final long ID_DATA_NODE_WITH_DESCENDANTS = 4001 - static final String XPATH_DATA_NODE_WITH_DESCENDANTS = '/parent-1' - static final String XPATH_DATA_NODE_WITH_LEAVES = '/parent-207' - static final long DATA_NODE_202_FRAGMENT_ID = 4202L - static final long CHILD_OF_DATA_NODE_202_FRAGMENT_ID = 4203L - static final long LIST_DATA_NODE_PARENT201_FRAGMENT_ID = 4206L - static final long LIST_DATA_NODE_PARENT203_FRAGMENT_ID = 4214L - static final long LIST_DATA_NODE_PARENT202_FRAGMENT_ID = 4211L - static final long PARENT_3_FRAGMENT_ID = 4003L - - static final DataNode newDataNode = new DataNodeBuilder().build() - static DataNode existingDataNode - static DataNode existingChildDataNode + static int DATASPACE_1001_ID = 1001L + static int ANCHOR_3003_ID = 3003L + static long ID_DATA_NODE_WITH_DESCENDANTS = 4001 + static String XPATH_DATA_NODE_WITH_DESCENDANTS = '/parent-1' + static String XPATH_DATA_NODE_WITH_LEAVES = '/parent-207' + static long DATA_NODE_202_FRAGMENT_ID = 4202L + static long CHILD_OF_DATA_NODE_202_FRAGMENT_ID = 4203L + static long LIST_DATA_NODE_PARENT201_FRAGMENT_ID = 4206L + static long LIST_DATA_NODE_PARENT203_FRAGMENT_ID = 4214L + static long LIST_DATA_NODE_PARENT202_FRAGMENT_ID = 4211L + static long PARENT_3_FRAGMENT_ID = 4003L + + static Collection<DataNode> newDataNodes = [new DataNodeBuilder().build()] + static Collection<DataNode> existingDataNodes = [createDataNodeTree(XPATH_DATA_NODE_WITH_DESCENDANTS)] + static Collection<DataNode> existingChildDataNodes = [createDataNodeTree('/parent-1/child-1')] + + def static deleteTestParentXPath = '/parent-200' + def static deleteTestChildXpath = "${deleteTestParentXPath}/child-with-slash[@key='a/b']" + def static deleteTestGrandChildXPath = "${deleteTestChildXpath}/grandChild" def expectedLeavesByXpathMap = [ '/parent-207' : ['parent-leaf': 'parent-leaf value'], @@ -75,11 +80,6 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase { '/parent-207/child-002/grand-child': ['grand-child-leaf': 'grand-child-leaf value'] ] - static { - existingDataNode = createDataNodeTree(XPATH_DATA_NODE_WITH_DESCENDANTS) - existingChildDataNode = createDataNodeTree('/parent-1/child-1') - } - @Sql([CLEAR_DATA, SET_DATA]) def 'Get existing datanode with descendants.'() { when: 'the node is retrieved by its xpath' @@ -93,13 +93,13 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase { } @Sql([CLEAR_DATA, SET_DATA]) - def 'Storing and Retrieving a new DataNode with descendants.'() { + def 'Storing and Retrieving a new DataNodes with descendants.'() { when: 'a fragment with descendants is stored' def parentXpath = '/parent-new' def childXpath = '/parent-new/child-new' def grandChildXpath = '/parent-new/child-new/grandchild-new' - objectUnderTest.storeDataNode(DATASPACE_NAME, ANCHOR_NAME1, - createDataNodeTree(parentXpath, childXpath, grandChildXpath)) + def dataNodes = [createDataNodeTree(parentXpath, childXpath, grandChildXpath)] + objectUnderTest.storeDataNodes(DATASPACE_NAME, ANCHOR_NAME1, dataNodes) then: 'it can be retrieved by its xpath' def dataNode = objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_NAME1, parentXpath, INCLUDE_ALL_DESCENDANTS) assert dataNode.xpath == parentXpath @@ -117,9 +117,9 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase { def 'Store data node for multiple anchors using the same schema.'() { def xpath = '/parent-new' given: 'a fragment is stored for an anchor' - objectUnderTest.storeDataNode(DATASPACE_NAME, ANCHOR_NAME1, createDataNodeTree(xpath)) + objectUnderTest.storeDataNodes(DATASPACE_NAME, ANCHOR_NAME1, [createDataNodeTree(xpath)]) when: 'another fragment is stored for an other anchor, using the same schema set' - objectUnderTest.storeDataNode(DATASPACE_NAME, ANCHOR_NAME3, createDataNodeTree(xpath)) + objectUnderTest.storeDataNodes(DATASPACE_NAME, ANCHOR_NAME3, [createDataNodeTree(xpath)]) then: 'both fragments can be retrieved by their xpath' def fragment1 = getFragmentByXpath(DATASPACE_NAME, ANCHOR_NAME1, xpath) fragment1.anchor.name == ANCHOR_NAME1 @@ -130,45 +130,48 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase { } @Sql([CLEAR_DATA, SET_DATA]) - def 'Store datanode error scenario: #scenario.'() { + def 'Store datanodes error scenario: #scenario.'() { when: 'attempt to store a data node with #scenario' - objectUnderTest.storeDataNode(dataspaceName, anchorName, dataNode) + objectUnderTest.storeDataNodes(dataspaceName, anchorName, dataNodes) then: 'a #expectedException is thrown' thrown(expectedException) where: 'the following data is used' - scenario | dataspaceName | anchorName | dataNode || expectedException - 'dataspace does not exist' | 'unknown' | 'not-relevant' | newDataNode || DataspaceNotFoundException - 'schema set does not exist' | DATASPACE_NAME | 'unknown' | newDataNode || AnchorNotFoundException - 'anchor already exists' | DATASPACE_NAME | ANCHOR_NAME1 | newDataNode || ConstraintViolationException - 'datanode already exists' | DATASPACE_NAME | ANCHOR_NAME1 | existingDataNode || AlreadyDefinedException + scenario | dataspaceName | anchorName | dataNodes || expectedException + 'dataspace does not exist' | 'unknown' | 'not-relevant' | newDataNodes || DataspaceNotFoundException + 'schema set does not exist' | DATASPACE_NAME | 'unknown' | newDataNodes || AnchorNotFoundException + 'anchor already exists' | DATASPACE_NAME | ANCHOR_NAME1 | newDataNodes || ConstraintViolationException + 'datanode already exists' | DATASPACE_NAME | ANCHOR_NAME1 | existingDataNodes || AlreadyDefinedExceptionBatch } @Sql([CLEAR_DATA, SET_DATA]) - def 'Add a child to a Fragment that already has a child.'() { - given: ' a new child node' - def newChild = createDataNodeTree('xpath for new child') + def 'Add children to a Fragment that already has a child.'() { + given: 'collection of new child data nodes' + def newChild1 = createDataNodeTree('/parent-1/child-2') + def newChild2 = createDataNodeTree('/parent-1/child-3') + def newChildrenCollection = [newChild1, newChild2] when: 'the child is added to an existing parent with 1 child' - objectUnderTest.addChildDataNode(DATASPACE_NAME, ANCHOR_NAME1, XPATH_DATA_NODE_WITH_DESCENDANTS, newChild) - then: 'the parent is now has to 2 children' + objectUnderTest.addChildDataNodes(DATASPACE_NAME, ANCHOR_NAME1, XPATH_DATA_NODE_WITH_DESCENDANTS, newChildrenCollection) + then: 'the parent is now has to 3 children' def expectedExistingChildPath = '/parent-1/child-1' def parentFragment = fragmentRepository.findById(ID_DATA_NODE_WITH_DESCENDANTS).orElseThrow() - parentFragment.childFragments.size() == 2 + parentFragment.childFragments.size() == 3 and: 'it still has the old child' parentFragment.childFragments.find({ it.xpath == expectedExistingChildPath }) - and: 'it has the new child' - parentFragment.childFragments.find({ it.xpath == newChild.xpath }) + and: 'it has the new children' + parentFragment.childFragments.find({ it.xpath == newChildrenCollection[0].xpath }) + parentFragment.childFragments.find({ it.xpath == newChildrenCollection[1].xpath }) } @Sql([CLEAR_DATA, SET_DATA]) def 'Add child error scenario: #scenario.'() { when: 'attempt to add a child data node with #scenario' - objectUnderTest.addChildDataNode(DATASPACE_NAME, ANCHOR_NAME1, parentXpath, dataNode) + objectUnderTest.addChildDataNodes(DATASPACE_NAME, ANCHOR_NAME1, parentXpath, dataNodes) then: 'a #expectedException is thrown' thrown(expectedException) where: 'the following data is used' - scenario | parentXpath | dataNode || expectedException - 'parent does not exist' | '/unknown' | newDataNode || DataNodeNotFoundException - 'already existing child' | XPATH_DATA_NODE_WITH_DESCENDANTS | existingChildDataNode || AlreadyDefinedException + scenario | parentXpath | dataNodes || expectedException + 'parent does not exist' | '/unknown' | newDataNodes || DataNodeNotFoundException + 'already existing child' | XPATH_DATA_NODE_WITH_DESCENDANTS | existingChildDataNodes || AlreadyDefinedExceptionBatch } @Sql([CLEAR_DATA, SET_DATA]) @@ -288,7 +291,8 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase { scenario | dataspaceName | anchorName | xpath || expectedException 'non-existing dataspace' | 'NO DATASPACE' | 'not relevant' | '/not relevant' || DataspaceNotFoundException 'non-existing anchor' | DATASPACE_NAME | 'NO ANCHOR' | '/not relevant' || AnchorNotFoundException - 'non-existing xpath' | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | '/NO XPATH' || DataNodeNotFoundException + 'non-existing xpath' | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | '/NO-XPATH' || DataNodeNotFoundException + 'invalid xpath' | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | 'INVALID XPATH' || CpsPathException } @Sql([CLEAR_DATA, SET_DATA]) @@ -318,7 +322,7 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase { scenario | dataspaceName | anchorName | xpath || expectedException 'non-existing dataspace' | 'NO DATASPACE' | 'not relevant' | '/not relevant' || DataspaceNotFoundException 'non-existing anchor' | DATASPACE_NAME | 'NO ANCHOR' | '/not relevant' || AnchorNotFoundException - 'non-existing xpath' | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | '/NON-EXISTING XPATH' || DataNodeNotFoundException + 'non-existing xpath' | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | '/NON-EXISTING-XPATH' || DataNodeNotFoundException } @Sql([CLEAR_DATA, SET_DATA]) @@ -412,7 +416,8 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase { scenario | dataspaceName | anchorName | xpath || expectedException 'non-existing dataspace' | 'NO DATASPACE' | 'not relevant' | '/not relevant' || DataspaceNotFoundException 'non-existing anchor' | DATASPACE_NAME | 'NO ANCHOR' | '/not relevant' || AnchorNotFoundException - 'non-existing xpath' | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | '/NON-EXISTING XPATH' || DataNodeNotFoundException + 'non-existing xpath' | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | '/NON-EXISTING-XPATH' || DataNodeNotFoundException + 'invalid xpath' | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | 'INVALID XPATH' || CpsPathException } @Sql([CLEAR_DATA, SET_DATA]) @@ -525,6 +530,25 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase { } @Sql([CLEAR_DATA, SET_DATA]) + def 'Delete data nodes with "/"-token in list key value: #scenario. (CPS-1409)'() { + given: 'a data nodes with list-element child with "/" in index value (and grandchild)' + def grandChild = new DataNodeBuilder().withXpath(deleteTestGrandChildXPath).build() + def child = new DataNodeBuilder().withXpath(deleteTestChildXpath).withChildDataNodes([grandChild]).build() + objectUnderTest.addChildDataNode(DATASPACE_NAME, ANCHOR_NAME3, deleteTestParentXPath, child) + and: 'number of children before delete is stored' + def numberOfChildrenBeforeDelete = objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_NAME3, pathToParentOfDeletedNode, INCLUDE_ALL_DESCENDANTS).childDataNodes.size() + when: 'target node is deleted' + objectUnderTest.deleteDataNode(DATASPACE_NAME, ANCHOR_NAME3, deleteTarget) + then: 'one child has been deleted' + def numberOfChildrenAfterDelete = objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_NAME3, pathToParentOfDeletedNode, INCLUDE_ALL_DESCENDANTS).childDataNodes.size() + assert numberOfChildrenAfterDelete == numberOfChildrenBeforeDelete - 1 + where: + scenario | deleteTarget | pathToParentOfDeletedNode + 'list element with /' | deleteTestChildXpath | deleteTestParentXPath + 'child of list element' | deleteTestGrandChildXPath | deleteTestChildXpath + } + + @Sql([CLEAR_DATA, SET_DATA]) def 'Delete list error scenario: #scenario.'() { when: 'attempting to delete scenario: #scenario.' objectUnderTest.deleteListDataNode(DATASPACE_NAME, ANCHOR_NAME3, targetXpaths) @@ -541,7 +565,7 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase { } @Sql([CLEAR_DATA, SET_DATA]) - def 'Confirm deletion of #scenario.'() { + def 'Delete data node by xpath #scenario.'() { given: 'a valid data node' def dataNode and: 'data nodes are deleted' @@ -566,7 +590,7 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase { } @Sql([CLEAR_DATA, SET_DATA]) - def 'Delete data node with #scenario.'() { + def 'Delete data node error scenario: #scenario.'() { when: 'data node is deleted' objectUnderTest.deleteDataNode(DATASPACE_NAME, ANCHOR_NAME3, datanodeXpath) then: 'a #expectedException is thrown' 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 e69cbee471..8234d32a8e 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 @@ -2,6 +2,7 @@ * ============LICENSE_START======================================================= * Copyright (c) 2021 Bell Canada. * Modifications Copyright (C) 2021-2022 Nordix Foundation + * Modifications Copyright (C) 2022 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,6 +35,7 @@ import org.onap.cps.spi.repository.DataspaceRepository import org.onap.cps.spi.repository.FragmentRepository import org.onap.cps.spi.utils.SessionManager import org.onap.cps.utils.JsonObjectMapper +import org.springframework.dao.DataIntegrityViolationException import spock.lang.Specification class CpsDataPersistenceServiceSpec extends Specification { @@ -44,7 +46,28 @@ class CpsDataPersistenceServiceSpec extends Specification { def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper()) def mockSessionManager = Mock(SessionManager) - def objectUnderTest = new CpsDataPersistenceServiceImpl(mockDataspaceRepository, mockAnchorRepository, mockFragmentRepository, jsonObjectMapper, mockSessionManager) + def objectUnderTest = Spy(new CpsDataPersistenceServiceImpl(mockDataspaceRepository, mockAnchorRepository, mockFragmentRepository, jsonObjectMapper, mockSessionManager)) + + def 'Storing data nodes individually when batch operation fails'(){ + given: 'two data nodes and supporting repository mock behavior' + def dataNode1 = createDataNodeAndMockRepositoryMethodSupportingIt('xpath1','OK') + def dataNode2 = createDataNodeAndMockRepositoryMethodSupportingIt('xpath2','OK') + and: 'the batch store operation will fail' + mockFragmentRepository.saveAll(*_) >> { throw new DataIntegrityViolationException("Exception occurred") } + when: 'trying to store data nodes' + objectUnderTest.storeDataNodes('dataSpaceName', 'anchorName', [dataNode1, dataNode2]) + then: 'the two data nodes are saved individually' + 2 * mockFragmentRepository.save(_); + } + + def 'Store single data node.'() { + given: 'a data node' + def dataNode = new DataNode() + when: 'storing a single data node' + objectUnderTest.storeDataNode('dataspace1', 'anchor1', dataNode) + then: 'the call is redirected to storing a collection of data nodes with just the given data node' + 1 * objectUnderTest.storeDataNodes('dataspace1', 'anchor1', [dataNode]) + } def 'Handling of StaleStateException (caused by concurrent updates) during update data node and descendants.'() { given: 'the fragment repository returns a fragment entity' @@ -66,10 +89,10 @@ class CpsDataPersistenceServiceSpec extends Specification { def 'Handling of StaleStateException (caused by concurrent updates) during update data nodes and descendants.'() { given: 'the system contains and can update one datanode' - def dataNode1 = mockDataNodeAndFragmentEntity('/node1', 'OK') + def dataNode1 = createDataNodeAndMockRepositoryMethodSupportingIt('/node1', 'OK') and: 'the system contains two more datanodes that throw an exception while updating' - def dataNode2 = mockDataNodeAndFragmentEntity('/node2', 'EXCEPTION') - def dataNode3 = mockDataNodeAndFragmentEntity('/node3', 'EXCEPTION') + def dataNode2 = createDataNodeAndMockRepositoryMethodSupportingIt('/node2', 'EXCEPTION') + def dataNode3 = createDataNodeAndMockRepositoryMethodSupportingIt('/node3', 'EXCEPTION') and: 'the batch update will therefore also fail' mockFragmentRepository.saveAll(*_) >> { throw new StaleStateException("concurrent updates") } when: 'attempt batch update data nodes' @@ -142,6 +165,25 @@ class CpsDataPersistenceServiceSpec extends Specification { 1 * mockSessionManager.lockAnchor('mySessionId', 'myDataspaceName', 'myAnchorName', 123L) } + def 'update data node leaves: #scenario'(){ + given: 'A node exists for the given xpath' + mockFragmentRepository.getByDataspaceAndAnchorAndXpath(_, _, '/some/xpath') >> new FragmentEntity(xpath: '/some/xpath', attributes: existingAttributes) + when: 'the node leaves are updated' + objectUnderTest.updateDataLeaves('some-dataspace', 'some-anchor', '/some/xpath', newAttributes as Map<String, Serializable>) + then: 'the fragment entity saved has the original and new attributes' + 1 * mockFragmentRepository.save({fragmentEntity -> { + assert fragmentEntity.getXpath() == '/some/xpath' + assert fragmentEntity.getAttributes() == mergedAttributes + }}) + where: 'the following attributes combinations are used' + scenario | existingAttributes | newAttributes | mergedAttributes + 'add new leaf' | '{"existing":"value"}' | ["new":"value"] | '{"existing":"value","new":"value"}' + 'update existing leaf' | '{"existing":"value"}' | ["existing":"value2"] | '{"existing":"value2"}' + 'update nothing with nothing' | '' | [] | '' + 'update with nothing' | '{"existing":"value"}' | [] | '{"existing":"value"}' + 'update with same value' | '{"existing":"value"}' | ["existing":"value"] | '{"existing":"value"}' + } + def 'update data node and descendants: #scenario'(){ given: 'mocked responses' mockFragmentRepository.getByDataspaceAndAnchorAndXpath(_, _, '/test/xpath') >> new FragmentEntity(xpath: '/test/xpath', childFragments: []) @@ -174,7 +216,7 @@ class CpsDataPersistenceServiceSpec extends Specification { }}) } - def mockDataNodeAndFragmentEntity(xpath, scenario) { + def createDataNodeAndMockRepositoryMethodSupportingIt(xpath, scenario) { def dataNode = new DataNodeBuilder().withXpath(xpath).build() def fragmentEntity = new FragmentEntity(xpath: xpath, childFragments: []) mockFragmentRepository.getByDataspaceAndAnchorAndXpath(_, _, xpath) >> fragmentEntity diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsModulePersistenceServiceIntegrationSpec.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsModulePersistenceServiceIntegrationSpec.groovy index bcb080726a..4c67f7e972 100644 --- a/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsModulePersistenceServiceIntegrationSpec.groovy +++ b/cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsModulePersistenceServiceIntegrationSpec.groovy @@ -230,8 +230,9 @@ class CpsModulePersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase def 'Identifying new module references where #scenario'() { when: 'identifyNewModuleReferences is called' def result = objectUnderTest.identifyNewModuleReferences(moduleReferences) - then: 'the correct module reference collection is returned' - assert result == expectedResult + then: 'the correct module references are returned' + assert result.size() == expectedResult.size() + assert result.containsAll(expectedResult) where: 'the following data is used' scenario | moduleReferences || expectedResult 'new module references exist' | toModuleReference([['some module 1' : 'some revision 1'], ['some module 2' : 'some revision 2']]) || toModuleReference([['some module 1' : 'some revision 1'], ['some module 2' : 'some revision 2']]) @@ -304,7 +305,7 @@ class CpsModulePersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase def moduleReferences = [].withDefault { [:] } moduleReferenceAsMap.forEach(property -> property.forEach((moduleName, revision) -> { - moduleReferences.add(new ModuleReference('moduleName' : moduleName, 'revision' : revision)) + moduleReferences.add(new ModuleReference(moduleName, revision)) })) return moduleReferences } diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/performance/CpsToDataNodePerfTest.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/performance/CpsDataPersistenceServicePerfTest.groovy index b26cef4de7..910d8a4601 100644 --- a/cps-ri/src/test/groovy/org/onap/cps/spi/performance/CpsToDataNodePerfTest.groovy +++ b/cps-ri/src/test/groovy/org/onap/cps/spi/performance/CpsDataPersistenceServicePerfTest.groovy @@ -20,7 +20,7 @@ package org.onap.cps.spi.performance -import org.apache.commons.lang3.time.StopWatch +import org.springframework.util.StopWatch import org.onap.cps.spi.CpsDataPersistenceService import org.onap.cps.spi.impl.CpsPersistenceSpecBase import org.onap.cps.spi.model.DataNode @@ -33,7 +33,7 @@ import java.util.concurrent.TimeUnit import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS -class CpsToDataNodePerfTest extends CpsPersistenceSpecBase { +class CpsDataPersistenceServicePerfTest extends CpsPersistenceSpecBase { static final String PERF_TEST_DATA = '/data/perf-test.sql' @@ -47,26 +47,25 @@ class CpsToDataNodePerfTest extends CpsPersistenceSpecBase { static def ALLOWED_SETUP_TIME_MS = TimeUnit.SECONDS.toMillis(10) static def ALLOWED_READ_TIME_AL_NODES_MS = 500 - def readStopWatch = new StopWatch() + def stopWatch = new StopWatch() @Sql([CLEAR_DATA, PERF_TEST_DATA]) def 'Create a node with many descendants (please note, subsequent tests depend on this running first).'() { given: 'a node with a large number of descendants is created' - def setupStopWatch = new StopWatch() - setupStopWatch.start() + stopWatch.start() createLineage() - setupStopWatch.stop() - def setupDurationInMillis = setupStopWatch.getTime() + stopWatch.stop() + def setupDurationInMillis = stopWatch.getTotalTimeMillis() and: 'setup duration is under #ALLOWED_SETUP_TIME_MS milliseconds' assert setupDurationInMillis < ALLOWED_SETUP_TIME_MS } def 'Get data node with many descendants by xpath #scenario'() { when: 'get parent is executed with all descendants' - readStopWatch.start() + stopWatch.start() def result = objectUnderTest.getDataNode('PERF-DATASPACE', 'PERF-ANCHOR', xpath, INCLUDE_ALL_DESCENDANTS) - readStopWatch.stop() - def readDurationInMillis = readStopWatch.getTime() + stopWatch.stop() + def readDurationInMillis = stopWatch.getTotalTimeMillis() then: 'read duration is under 500 milliseconds' assert readDurationInMillis < ALLOWED_READ_TIME_AL_NODES_MS and: 'data node is returned with all the descendants populated' @@ -79,11 +78,10 @@ class CpsToDataNodePerfTest extends CpsPersistenceSpecBase { def 'Query parent data node with many descendants by cps-path'() { when: 'query is executed with all descendants' - readStopWatch.reset() - readStopWatch.start() + stopWatch.start() def result = objectUnderTest.queryDataNodes('PERF-DATASPACE', 'PERF-ANCHOR', '//perf-parent-1' , INCLUDE_ALL_DESCENDANTS) - readStopWatch.stop() - def readDurationInMillis = readStopWatch.getTime() + stopWatch.stop() + def readDurationInMillis = stopWatch.getTotalTimeMillis() then: 'read duration is under 500 milliseconds' assert readDurationInMillis < ALLOWED_READ_TIME_AL_NODES_MS and: 'data node is returned with all the descendants populated' @@ -92,11 +90,10 @@ class CpsToDataNodePerfTest extends CpsPersistenceSpecBase { def 'Query many descendants by cps-path with #scenario'() { when: 'query is executed with all descendants' - readStopWatch.reset() - readStopWatch.start() + stopWatch.start() def result = objectUnderTest.queryDataNodes('PERF-DATASPACE', 'PERF-ANCHOR', '//perf-test-grand-child-1', descendantsOption) - readStopWatch.stop() - def readDurationInMillis = readStopWatch.getTime() + stopWatch.stop() + def readDurationInMillis = stopWatch.getTotalTimeMillis() then: 'read duration is under 500 milliseconds' assert readDurationInMillis < alowedDuration and: 'data node is returned with all the descendants populated' @@ -104,24 +101,60 @@ class CpsToDataNodePerfTest extends CpsPersistenceSpecBase { where: 'the following options are used' scenario | descendantsOption || alowedDuration 'omit descendants ' | OMIT_DESCENDANTS || 150 - 'include descendants (although there are none)' | INCLUDE_ALL_DESCENDANTS || 1500 + 'include descendants (although there are none)' | INCLUDE_ALL_DESCENDANTS || 150 + } + + def 'Delete 50 grandchildren (that have no descendants)'() { + when: 'target nodes are deleted' + stopWatch.start() + (1..50).each { + def grandchildPath = "${PERF_TEST_PARENT}/perf-test-child-1/perf-test-grand-child-${it}".toString(); + objectUnderTest.deleteDataNode('PERF-DATASPACE', 'PERF-ANCHOR', grandchildPath) + } + stopWatch.stop() + def deleteDurationInMillis = stopWatch.getTotalTimeMillis() + then: 'delete duration is under 1000 milliseconds' + assert deleteDurationInMillis < 1000 + } + + def 'Delete 5 children with grandchildren'() { + when: 'child nodes are deleted' + stopWatch.start() + (1..5).each { + def childPath = "${PERF_TEST_PARENT}/perf-test-child-${it}".toString(); + objectUnderTest.deleteDataNode('PERF-DATASPACE', 'PERF-ANCHOR', childPath) + } + stopWatch.stop() + def deleteDurationInMillis = stopWatch.getTotalTimeMillis() + then: 'delete duration is under 10000 milliseconds' + assert deleteDurationInMillis < 10000 + } + + def 'Delete 1 large data node with many descendants'() { + when: 'parent node is deleted' + stopWatch.start() + objectUnderTest.deleteDataNode('PERF-DATASPACE', 'PERF-ANCHOR', PERF_TEST_PARENT) + stopWatch.stop() + def deleteDurationInMillis = stopWatch.getTotalTimeMillis() + then: 'delete duration is under 5000 milliseconds' + assert deleteDurationInMillis < 5000 } def createLineage() { (1..NUMBER_OF_CHILDREN).each { def childName = "perf-test-child-${it}".toString() - def newChild = goForthAndMultiply(PERF_TEST_PARENT, childName) - objectUnderTest.addChildDataNode('PERF-DATASPACE', 'PERF-ANCHOR', PERF_TEST_PARENT, newChild) + def child = goForthAndMultiply(PERF_TEST_PARENT, childName) + objectUnderTest.addChildDataNode('PERF-DATASPACE', 'PERF-ANCHOR', PERF_TEST_PARENT, child) } } def goForthAndMultiply(parentXpath, childName) { - def children = [] + def grandChildren = [] (1..NUMBER_OF_GRAND_CHILDREN).each { - def child = new DataNodeBuilder().withXpath("${parentXpath}/${childName}/perf-test-grand-child-${it}").build() - children.add(child) + def grandChild = new DataNodeBuilder().withXpath("${parentXpath}/${childName}/perf-test-grand-child-${it}").build() + grandChildren.add(grandChild) } - return new DataNodeBuilder().withXpath("${parentXpath}/${childName}").withChildDataNodes(children).build() + return new DataNodeBuilder().withXpath("${parentXpath}/${childName}").withChildDataNodes(grandChildren).build() } def countDataNodes(dataNodes) { diff --git a/cps-ri/src/test/groovy/org/onap/cps/spi/performance/CpsModuleReferenceRepositoryPerfTest.groovy b/cps-ri/src/test/groovy/org/onap/cps/spi/performance/CpsModuleReferenceRepositoryPerfTest.groovy new file mode 100644 index 0000000000..9b722cddae --- /dev/null +++ b/cps-ri/src/test/groovy/org/onap/cps/spi/performance/CpsModuleReferenceRepositoryPerfTest.groovy @@ -0,0 +1,103 @@ +/* + * ============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.performance + +import org.onap.cps.spi.CpsModulePersistenceService +import org.onap.cps.spi.entities.SchemaSetEntity +import org.onap.cps.spi.impl.CpsPersistenceSpecBase +import org.onap.cps.spi.model.ModuleReference +import org.onap.cps.spi.repository.DataspaceRepository +import org.onap.cps.spi.repository.ModuleReferenceRepository +import org.onap.cps.spi.repository.SchemaSetRepository +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.test.context.jdbc.Sql +import org.springframework.util.StopWatch + +import java.util.concurrent.ThreadLocalRandom + +class CpsModuleReferenceRepositoryPerfTest extends CpsPersistenceSpecBase { + + static final String PERF_TEST_DATA = '/data/perf-test.sql' + + def NEW_RESOURCE_CONTENT = 'module stores {\n' + + ' yang-version 1.1;\n' + + ' namespace "org:onap:ccsdk:sample";\n' + + '\n' + + ' prefix book-store;\n' + + '\n' + + ' revision "2020-09-15" {\n' + + ' description\n' + + ' "Sample Model";\n' + + ' }' + + '}' + + @Autowired + CpsModulePersistenceService objectUnderTest + + @Autowired + DataspaceRepository dataspaceRepository + + @Autowired + SchemaSetRepository schemaSetRepository + + @Autowired + ModuleReferenceRepository moduleReferenceRepository + + @Sql([CLEAR_DATA, PERF_TEST_DATA]) + def 'Store new schema set with many modules'() { + when: 'a new schema set with 200 modules is stored' + def newYangResourcesNameToContentMap = [:] + (1..200).each { + def year = 2000 + it + def resourceName = "module${it}".toString() + def moduleName = "stores${it}" + def content = NEW_RESOURCE_CONTENT.replace('2020',String.valueOf(year)).replace('stores',moduleName) + newYangResourcesNameToContentMap.put(resourceName, content) + } + objectUnderTest.storeSchemaSet('PERF-DATASPACE', 'perfSchemaSet', newYangResourcesNameToContentMap) + then: 'the schema set is persisted correctly' + def dataspaceEntity = dataspaceRepository.getByName('PERF-DATASPACE') + SchemaSetEntity result = schemaSetRepository.getByDataspaceAndName(dataspaceEntity, 'perfSchemaSet') + result.yangResources.size() == 200 + and: 'identification of new module resources is fast enough (1,000 executions less then 6,000 milliseconds)' + def stopWatch = new StopWatch() + 1000.times() { + def moduleReferencesToCheck = createModuleReferencesWithRandomMatchingExistingModuleReferences() + stopWatch.start() + def newModuleReferences = moduleReferenceRepository.identifyNewModuleReferences(moduleReferencesToCheck) + stopWatch.stop() + assert newModuleReferences.size() > 0 && newModuleReferences.size() < 300 + } + assert stopWatch.getTotalTimeMillis() < 6000 + } + + def createModuleReferencesWithRandomMatchingExistingModuleReferences() { + def moduleReferences = [] + (1..250).each { + def randomNumber = ThreadLocalRandom.current().nextInt(1, 300) + def year = 2000 + randomNumber + def moduleName = "stores${randomNumber}" + moduleReferences.add(new ModuleReference(moduleName, "${year}-09-15")) + } + return moduleReferences + } + +} diff --git a/cps-service/pom.xml b/cps-service/pom.xml index 77f262c32e..70fa4479a4 100644 --- a/cps-service/pom.xml +++ b/cps-service/pom.xml @@ -4,6 +4,7 @@ Copyright (C) 2021-2022 Nordix Foundation Modifications Copyright (C) 2021 Bell Canada. Modifications Copyright (C) 2021 Pantheon.tech + Modifications Copyright (C) 2022 Deutsche Telekom AG ================================================================================ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -65,6 +66,10 @@ <artifactId>yang-data-codec-gson</artifactId> </dependency> <dependency> + <groupId>org.opendaylight.yangtools</groupId> + <artifactId>yang-data-codec-xml</artifactId> + </dependency> + <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> @@ -111,6 +116,10 @@ <artifactId>janino</artifactId> </dependency> <dependency> + <groupId>org.onap.cps</groupId> + <artifactId>cps-path-parser</artifactId> + </dependency> + <dependency> <!-- Hazelcast provide Distributed Caches --> <groupId>com.hazelcast</groupId> <artifactId>hazelcast-spring</artifactId> diff --git a/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java b/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java index b2e8c5ba42..012d7f8259 100644 --- a/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java +++ b/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java @@ -3,6 +3,7 @@ * Copyright (C) 2020-2022 Nordix Foundation * Modifications Copyright (C) 2021 Pantheon.tech * Modifications Copyright (C) 2021-2022 Bell Canada + * Modifications Copyright (C) 2022 Deutsche Telekom AG * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +28,7 @@ import java.util.Collection; import java.util.Map; import org.onap.cps.spi.FetchDescendantsOption; import org.onap.cps.spi.model.DataNode; +import org.onap.cps.utils.ContentType; /* * Datastore interface for handling CPS data. @@ -38,10 +40,22 @@ public interface CpsDataService { * * @param dataspaceName dataspace name * @param anchorName anchor name - * @param jsonData json data + * @param nodeData node data * @param observedTimestamp observedTimestamp */ - void saveData(String dataspaceName, String anchorName, String jsonData, OffsetDateTime observedTimestamp); + void saveData(String dataspaceName, String anchorName, String nodeData, OffsetDateTime observedTimestamp); + + /** + * Persists data for the given anchor and dataspace. + * + * @param dataspaceName dataspace name + * @param anchorName anchor name + * @param nodeData node data + * @param observedTimestamp observedTimestamp + * @param contentType node data content type + */ + void saveData(String dataspaceName, String anchorName, String nodeData, OffsetDateTime observedTimestamp, + ContentType contentType); /** * Persists child data fragment under existing data node for the given anchor and dataspace. @@ -49,11 +63,25 @@ public interface CpsDataService { * @param dataspaceName dataspace name * @param anchorName anchor name * @param parentNodeXpath parent node xpath - * @param jsonData json data + * @param nodeData node data * @param observedTimestamp observedTimestamp */ - void saveData(String dataspaceName, String anchorName, String parentNodeXpath, String jsonData, - OffsetDateTime observedTimestamp); + void saveData(String dataspaceName, String anchorName, String parentNodeXpath, String nodeData, + OffsetDateTime observedTimestamp); + + /** + * Persists child data fragment under existing data node for the given anchor, dataspace and content type. + * + * @param dataspaceName dataspace name + * @param anchorName anchor name + * @param parentNodeXpath parent node xpath + * @param nodeData node data + * @param observedTimestamp observedTimestamp + * @param contentType node data content type + * + */ + void saveData(String dataspaceName, String anchorName, String parentNodeXpath, String nodeData, + OffsetDateTime observedTimestamp, ContentType contentType); /** * Persists child data fragment representing one or more list elements under existing data node for the 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 b08d8c1eba..65dfa7f5c6 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 @@ -3,6 +3,8 @@ * Copyright (C) 2021-2022 Nordix Foundation * Modifications Copyright (C) 2020-2022 Bell Canada. * Modifications Copyright (C) 2021 Pantheon.tech + * Modifications Copyright (C) 2022 TechMahindra Ltd. + * Modifications Copyright (C) 2022 Deutsche Telekom AG * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +29,7 @@ import static org.onap.cps.notification.Operation.DELETE; import static org.onap.cps.notification.Operation.UPDATE; import java.time.OffsetDateTime; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; @@ -44,8 +47,9 @@ import org.onap.cps.spi.model.Anchor; import org.onap.cps.spi.model.DataNode; import org.onap.cps.spi.model.DataNodeBuilder; import org.onap.cps.spi.utils.CpsValidator; +import org.onap.cps.utils.ContentType; import org.onap.cps.utils.YangUtils; -import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; +import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode; import org.opendaylight.yangtools.yang.model.api.SchemaContext; import org.springframework.stereotype.Service; @@ -64,20 +68,35 @@ public class CpsDataServiceImpl implements CpsDataService { private final CpsValidator cpsValidator; @Override - public void saveData(final String dataspaceName, final String anchorName, final String jsonData, + public void saveData(final String dataspaceName, final String anchorName, final String nodeData, final OffsetDateTime observedTimestamp) { + saveData(dataspaceName, anchorName, nodeData, observedTimestamp, ContentType.JSON); + } + + @Override + public void saveData(final String dataspaceName, final String anchorName, final String nodeData, + final OffsetDateTime observedTimestamp, final ContentType contentType) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); - final DataNode dataNode = buildDataNode(dataspaceName, anchorName, ROOT_NODE_XPATH, jsonData); - cpsDataPersistenceService.storeDataNode(dataspaceName, anchorName, dataNode); + final Collection<DataNode> dataNodes = + buildDataNodes(dataspaceName, anchorName, ROOT_NODE_XPATH, nodeData, contentType); + cpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName, dataNodes); processDataUpdatedEventAsync(dataspaceName, anchorName, ROOT_NODE_XPATH, CREATE, observedTimestamp); } @Override public void saveData(final String dataspaceName, final String anchorName, final String parentNodeXpath, - final String jsonData, final OffsetDateTime observedTimestamp) { + final String nodeData, final OffsetDateTime observedTimestamp) { + saveData(dataspaceName, anchorName, parentNodeXpath, nodeData, observedTimestamp, ContentType.JSON); + } + + @Override + public void saveData(final String dataspaceName, final String anchorName, final String parentNodeXpath, + final String nodeData, final OffsetDateTime observedTimestamp, + final ContentType contentType) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); - final DataNode dataNode = buildDataNode(dataspaceName, anchorName, parentNodeXpath, jsonData); - cpsDataPersistenceService.addChildDataNode(dataspaceName, anchorName, parentNodeXpath, dataNode); + final Collection<DataNode> dataNodes = + buildDataNodes(dataspaceName, anchorName, parentNodeXpath, nodeData, contentType); + cpsDataPersistenceService.addChildDataNodes(dataspaceName, anchorName, parentNodeXpath, dataNodes); processDataUpdatedEventAsync(dataspaceName, anchorName, parentNodeXpath, CREATE, observedTimestamp); } @@ -86,7 +105,7 @@ public class CpsDataServiceImpl implements CpsDataService { final String parentNodeXpath, final String jsonData, final OffsetDateTime observedTimestamp) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); final Collection<DataNode> listElementDataNodeCollection = - buildDataNodes(dataspaceName, anchorName, parentNodeXpath, jsonData); + buildDataNodes(dataspaceName, anchorName, parentNodeXpath, jsonData, ContentType.JSON); cpsDataPersistenceService.addListElements(dataspaceName, anchorName, parentNodeXpath, listElementDataNodeCollection); processDataUpdatedEventAsync(dataspaceName, anchorName, parentNodeXpath, UPDATE, observedTimestamp); @@ -97,7 +116,7 @@ public class CpsDataServiceImpl implements CpsDataService { final Collection<String> jsonDataList, final OffsetDateTime observedTimestamp) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); final Collection<Collection<DataNode>> listElementDataNodeCollections = - buildDataNodes(dataspaceName, anchorName, parentNodeXpath, jsonDataList); + buildDataNodes(dataspaceName, anchorName, parentNodeXpath, jsonDataList, ContentType.JSON); cpsDataPersistenceService.addMultipleLists(dataspaceName, anchorName, parentNodeXpath, listElementDataNodeCollections); processDataUpdatedEventAsync(dataspaceName, anchorName, parentNodeXpath, UPDATE, observedTimestamp); @@ -114,7 +133,7 @@ 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); + final DataNode dataNode = buildDataNode(dataspaceName, anchorName, parentNodeXpath, jsonData, ContentType.JSON); cpsDataPersistenceService .updateDataLeaves(dataspaceName, anchorName, dataNode.getXpath(), dataNode.getLeaves()); processDataUpdatedEventAsync(dataspaceName, anchorName, parentNodeXpath, UPDATE, observedTimestamp); @@ -128,7 +147,7 @@ public class CpsDataServiceImpl implements CpsDataService { cpsValidator.validateNameCharacters(dataspaceName, anchorName); final Collection<DataNode> dataNodeUpdates = buildDataNodes(dataspaceName, anchorName, - parentNodeXpath, dataNodeUpdatesAsJson); + parentNodeXpath, dataNodeUpdatesAsJson, ContentType.JSON); for (final DataNode dataNodeUpdate : dataNodeUpdates) { processDataNodeUpdate(dataspaceName, anchorName, dataNodeUpdate); } @@ -161,8 +180,10 @@ public class CpsDataServiceImpl implements CpsDataService { final String parentNodeXpath, final String jsonData, final OffsetDateTime observedTimestamp) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); - final DataNode dataNode = buildDataNode(dataspaceName, anchorName, parentNodeXpath, jsonData); - cpsDataPersistenceService.updateDataNodeAndDescendants(dataspaceName, anchorName, dataNode); + 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); } @@ -183,7 +204,7 @@ public class CpsDataServiceImpl implements CpsDataService { final String jsonData, final OffsetDateTime observedTimestamp) { cpsValidator.validateNameCharacters(dataspaceName, anchorName); final Collection<DataNode> newListElements = - buildDataNodes(dataspaceName, anchorName, parentNodeXpath, jsonData); + buildDataNodes(dataspaceName, anchorName, parentNodeXpath, jsonData, ContentType.JSON); replaceListContent(dataspaceName, anchorName, parentNodeXpath, newListElements, observedTimestamp); } @@ -220,42 +241,54 @@ public class CpsDataServiceImpl implements CpsDataService { } private DataNode buildDataNode(final String dataspaceName, final String anchorName, - final String parentNodeXpath, final String jsonData) { + final String parentNodeXpath, final String nodeData, + final ContentType contentType) { final Anchor anchor = cpsAdminService.getAnchor(dataspaceName, anchorName); final SchemaContext schemaContext = getSchemaContext(dataspaceName, anchor.getSchemaSetName()); if (ROOT_NODE_XPATH.equals(parentNodeXpath)) { - final NormalizedNode normalizedNode = YangUtils.parseJsonData(jsonData, schemaContext); - return new DataNodeBuilder().withNormalizedNodeTree(normalizedNode).build(); + final ContainerNode containerNode = YangUtils.parseData(contentType, nodeData, schemaContext); + return new DataNodeBuilder().withContainerNode(containerNode).build(); } - final NormalizedNode normalizedNode = YangUtils.parseJsonData(jsonData, schemaContext, parentNodeXpath); + final ContainerNode containerNode = YangUtils.parseData(contentType, nodeData, schemaContext, parentNodeXpath); + return new DataNodeBuilder() - .withParentNodeXpath(parentNodeXpath) - .withNormalizedNodeTree(normalizedNode) - .build(); + .withParentNodeXpath(parentNodeXpath) + .withContainerNode(containerNode) + .build(); } private List<DataNode> buildDataNodes(final String dataspaceName, final String anchorName, final Map<String, String> nodesJsonData) { return nodesJsonData.entrySet().stream().map(nodeJsonData -> buildDataNode(dataspaceName, anchorName, nodeJsonData.getKey(), - nodeJsonData.getValue())).collect(Collectors.toList()); + nodeJsonData.getValue(), ContentType.JSON)).collect(Collectors.toList()); } private Collection<DataNode> buildDataNodes(final String dataspaceName, final String anchorName, final String parentNodeXpath, - final String jsonData) { + final String nodeData, + final ContentType contentType) { final Anchor anchor = cpsAdminService.getAnchor(dataspaceName, anchorName); final SchemaContext schemaContext = getSchemaContext(dataspaceName, anchor.getSchemaSetName()); - - final NormalizedNode normalizedNode = YangUtils.parseJsonData(jsonData, schemaContext, parentNodeXpath); + if (ROOT_NODE_XPATH.equals(parentNodeXpath)) { + final ContainerNode containerNode = YangUtils.parseData(contentType, nodeData, schemaContext); + final Collection<DataNode> dataNodes = new DataNodeBuilder() + .withContainerNode(containerNode) + .buildCollection(); + if (dataNodes.isEmpty()) { + throw new DataValidationException("Invalid data.", "No data nodes provided"); + } + return dataNodes; + } + final ContainerNode containerNode = YangUtils.parseData(contentType, nodeData, schemaContext, parentNodeXpath); final Collection<DataNode> dataNodes = new DataNodeBuilder() .withParentNodeXpath(parentNodeXpath) - .withNormalizedNodeTree(normalizedNode) + .withContainerNode(containerNode) .buildCollection(); if (dataNodes.isEmpty()) { throw new DataValidationException("Invalid data.", "No data nodes provided"); @@ -265,9 +298,9 @@ public class CpsDataServiceImpl implements CpsDataService { } private Collection<Collection<DataNode>> buildDataNodes(final String dataspaceName, final String anchorName, - final String parentNodeXpath, final Collection<String> jsonDataList) { - return jsonDataList.stream() - .map(jsonData -> buildDataNodes(dataspaceName, anchorName, parentNodeXpath, jsonData)) + final String parentNodeXpath, final Collection<String> nodeDataList, final ContentType contentType) { + return nodeDataList.stream() + .map(nodeData -> buildDataNodes(dataspaceName, anchorName, parentNodeXpath, nodeData, contentType)) .collect(Collectors.toList()); } 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 28b18b3b5c..b9da4af025 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 @@ -3,6 +3,7 @@ * Copyright (C) 2020-2022 Nordix Foundation. * Modifications Copyright (C) 2021 Pantheon.tech * Modifications Copyright (C) 2022 Bell Canada + * Modifications Copyright (C) 2022 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,16 +36,27 @@ import org.onap.cps.spi.model.DataNode; */ public interface CpsDataPersistenceService { + /** * Store a datanode. * * @param dataspaceName dataspace name * @param anchorName anchor name * @param dataNode data node + * @deprecated Please use {@link #storeDataNodes(String, String, Collection)} as it supports multiple data nodes. */ + @Deprecated void storeDataNode(String dataspaceName, String anchorName, DataNode dataNode); /** + * Store multiple datanodes at once. + * @param dataspaceName dataspace name + * @param anchorName anchor name + * @param dataNodes data nodes + */ + void storeDataNodes(String dataspaceName, String anchorName, Collection<DataNode> dataNodes); + + /** * Add a child to a Fragment. * * @param dataspaceName dataspace name @@ -55,6 +67,16 @@ public interface CpsDataPersistenceService { void addChildDataNode(String dataspaceName, String anchorName, String parentXpath, DataNode dataNode); /** + * Add multiple children to a Fragment. + * + * @param dataspaceName dataspace name + * @param anchorName anchor name + * @param parentXpath parent xpath + * @param dataNodes collection of dataNodes + */ + void addChildDataNodes(String dataspaceName, String anchorName, String parentXpath, Collection<DataNode> dataNodes); + + /** * Adds list child elements to a Fragment. * * @param dataspaceName dataspace name @@ -62,7 +84,6 @@ public interface CpsDataPersistenceService { * @param parentNodeXpath parent node xpath * @param listElementsCollection collection of data nodes representing list elements */ - void addListElements(String dataspaceName, String anchorName, String parentNodeXpath, Collection<DataNode> listElementsCollection); diff --git a/cps-service/src/main/java/org/onap/cps/spi/model/DataNodeBuilder.java b/cps-service/src/main/java/org/onap/cps/spi/model/DataNodeBuilder.java index 1d8bac0dde..b23cdfc8d1 100644 --- a/cps-service/src/main/java/org/onap/cps/spi/model/DataNodeBuilder.java +++ b/cps-service/src/main/java/org/onap/cps/spi/model/DataNodeBuilder.java @@ -3,6 +3,7 @@ * Copyright (C) 2021 Bell Canada. All rights reserved. * Modifications Copyright (C) 2021 Pantheon.tech * Modifications Copyright (C) 2022 Nordix Foundation. + * Modifications Copyright (C) 2022 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,6 +36,7 @@ import org.onap.cps.spi.exceptions.DataValidationException; import org.onap.cps.utils.YangUtils; import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; import org.opendaylight.yangtools.yang.data.api.schema.ChoiceNode; +import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode; import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild; import org.opendaylight.yangtools.yang.data.api.schema.DataContainerNode; import org.opendaylight.yangtools.yang.data.api.schema.LeafSetNode; @@ -46,7 +48,7 @@ import org.opendaylight.yangtools.yang.data.api.schema.ValueNode; @Slf4j public class DataNodeBuilder { - private NormalizedNode normalizedNodeTree; + private ContainerNode containerNode; private String xpath; private String moduleNamePrefix; private String parentNodeXpath = ""; @@ -64,15 +66,14 @@ public class DataNodeBuilder { return this; } - /** - * To use {@link NormalizedNode} for creating {@link DataNode}. + * To use {@link Collection} of Normalized Nodes for creating {@link DataNode}. * - * @param normalizedNodeTree used for creating the Data Node + * @param containerNode used for creating the Data Node * @return this {@link DataNodeBuilder} object */ - public DataNodeBuilder withNormalizedNodeTree(final NormalizedNode normalizedNodeTree) { - this.normalizedNodeTree = normalizedNodeTree; + public DataNodeBuilder withContainerNode(final ContainerNode containerNode) { + this.containerNode = containerNode; return this; } @@ -128,11 +129,10 @@ public class DataNodeBuilder { * @return {@link DataNode} */ public DataNode build() { - if (normalizedNodeTree != null) { - return buildFromNormalizedNodeTree(); - } else { - return buildFromAttributes(); + if (containerNode != null) { + return buildFromContainerNode(); } + return buildFromAttributes(); } /** @@ -141,11 +141,10 @@ public class DataNodeBuilder { * @return {@link DataNode} {@link Collection} */ public Collection<DataNode> buildCollection() { - if (normalizedNodeTree != null) { - return buildCollectionFromNormalizedNodeTree(); - } else { - return Set.of(buildFromAttributes()); + if (containerNode != null) { + return buildCollectionFromContainerNode(); } + return Collections.emptySet(); } private DataNode buildFromAttributes() { @@ -157,8 +156,8 @@ public class DataNodeBuilder { return dataNode; } - private DataNode buildFromNormalizedNodeTree() { - final Collection<DataNode> dataNodeCollection = buildCollectionFromNormalizedNodeTree(); + private DataNode buildFromContainerNode() { + final Collection<DataNode> dataNodeCollection = buildCollectionFromContainerNode(); if (!dataNodeCollection.iterator().hasNext()) { throw new DataValidationException( "Unsupported xpath: ", "Unsupported xpath as it is referring to one element"); @@ -166,9 +165,13 @@ public class DataNodeBuilder { return dataNodeCollection.iterator().next(); } - private Collection<DataNode> buildCollectionFromNormalizedNodeTree() { + private Collection<DataNode> buildCollectionFromContainerNode() { final var parentDataNode = new DataNodeBuilder().withXpath(parentNodeXpath).build(); - addDataNodeFromNormalizedNode(parentDataNode, normalizedNodeTree); + if (containerNode.body() != null) { + for (final NormalizedNode normalizedNode: containerNode.body()) { + addDataNodeFromNormalizedNode(parentDataNode, normalizedNode); + } + } return parentDataNode.getChildDataNodes(); } diff --git a/cps-service/src/main/java/org/onap/cps/spi/model/ModuleReference.java b/cps-service/src/main/java/org/onap/cps/spi/model/ModuleReference.java index 569f0a06ed..18d55d5230 100644 --- a/cps-service/src/main/java/org/onap/cps/spi/model/ModuleReference.java +++ b/cps-service/src/main/java/org/onap/cps/spi/model/ModuleReference.java @@ -49,4 +49,5 @@ public class ModuleReference implements Serializable { this.revision = revision; this.namespace = ""; } + } diff --git a/cps-service/src/main/java/org/onap/cps/utils/ContentType.java b/cps-service/src/main/java/org/onap/cps/utils/ContentType.java new file mode 100644 index 0000000000..f888504843 --- /dev/null +++ b/cps-service/src/main/java/org/onap/cps/utils/ContentType.java @@ -0,0 +1,26 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022 Deutsche Telekom AG + * ================================================================================ + * 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.utils; + +public enum ContentType { + JSON, + XML +} diff --git a/cps-service/src/main/java/org/onap/cps/utils/XmlFileUtils.java b/cps-service/src/main/java/org/onap/cps/utils/XmlFileUtils.java new file mode 100644 index 0000000000..09f2e16c6a --- /dev/null +++ b/cps-service/src/main/java/org/onap/cps/utils/XmlFileUtils.java @@ -0,0 +1,185 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022 Deutsche Telekom AG + * ================================================================================ + * 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.utils; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.onap.cps.spi.exceptions.DataValidationException; +import org.opendaylight.yangtools.yang.model.api.DataSchemaNode; +import org.opendaylight.yangtools.yang.model.api.SchemaContext; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.SAXException; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class XmlFileUtils { + + private static final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + private static boolean isNewDocumentBuilderFactoryInstance = true; + private static final TransformerFactory transformerFactory = TransformerFactory.newInstance(); + private static boolean isNewTransformerFactoryInstance = true; + private static final Pattern XPATH_PROPERTY_REGEX = + Pattern.compile("\\[@(\\S{1,100})=['\\\"](\\S{1,100})['\\\"]\\]"); + + /** + * Prepare XML content. + * + * @param xmlContent XML content sent to store + * @param schemaContext schema context + * + * @return XML content wrapped by root node (if needed) + */ + public static String prepareXmlContent(final String xmlContent, final SchemaContext schemaContext) + throws IOException, ParserConfigurationException, TransformerException, SAXException { + return addRootNodeToXmlContent(xmlContent, schemaContext.getModules().iterator().next().getName(), + YangUtils.DATA_ROOT_NODE_NAMESPACE); + } + + /** + * Prepare XML content. + * + * @param xmlContent XML content sent to store + * @param parentSchemaNode Parent schema node + * @param xpath Parent xpath + * + * @return XML content wrapped by root node (if needed) + */ + public static String prepareXmlContent(final String xmlContent, + final DataSchemaNode parentSchemaNode, + final String xpath) + throws IOException, ParserConfigurationException, TransformerException, SAXException { + final String namespace = parentSchemaNode.getQName().getNamespace().toString(); + final String parentXpathPart = xpath.substring(xpath.lastIndexOf('/') + 1); + final Matcher regexMatcher = XPATH_PROPERTY_REGEX.matcher(parentXpathPart); + if (regexMatcher.find()) { + final HashMap<String, String> rootNodePropertyMap = new HashMap<>(); + rootNodePropertyMap.put(regexMatcher.group(1), regexMatcher.group(2)); + return addRootNodeToXmlContent(xmlContent, parentSchemaNode.getQName().getLocalName(), namespace, + rootNodePropertyMap); + } + + return addRootNodeToXmlContent(xmlContent, parentSchemaNode.getQName().getLocalName(), namespace); + } + + private static String addRootNodeToXmlContent(final String xmlContent, + final String rootNodeTagName, + final String namespace, + final Map<String, String> rootNodeProperty) + throws IOException, SAXException, ParserConfigurationException, TransformerException { + final DocumentBuilder documentBuilder = getDocumentBuilderFactory().newDocumentBuilder(); + final StringBuilder xmlStringBuilder = new StringBuilder(); + xmlStringBuilder.append(xmlContent); + final Document document = documentBuilder.parse( + new ByteArrayInputStream(xmlStringBuilder.toString().getBytes(StandardCharsets.UTF_8))); + final Element root = document.getDocumentElement(); + if (!root.getTagName().equals(rootNodeTagName) + && !root.getTagName().equals(YangUtils.DATA_ROOT_NODE_TAG_NAME)) { + final Document documentWithRootNode = addDataRootNode(root, rootNodeTagName, namespace, rootNodeProperty); + documentWithRootNode.setXmlStandalone(true); + final Transformer transformer = getTransformerFactory().newTransformer(); + final StringWriter stringWriter = new StringWriter(); + transformer.transform(new DOMSource(documentWithRootNode), new StreamResult(stringWriter)); + return stringWriter.toString(); + } + return xmlContent; + } + + /** + * Add root node to XML content. + * + * @param xmlContent XML content to add root node into + * @param rootNodeTagName Root node tag name + * @return XML content with root node tag added (if needed) + */ + public static String addRootNodeToXmlContent(final String xmlContent, + final String rootNodeTagName, + final String namespace) + throws IOException, ParserConfigurationException, TransformerException, SAXException { + return addRootNodeToXmlContent(xmlContent, rootNodeTagName, namespace, new HashMap<>()); + } + + /** + * Add root node into DOM element. + * + * @param node DOM element to add root node into + * @param tagName Root tag name to add + * @return DOM element with a root node + */ + static Document addDataRootNode(final Element node, + final String tagName, + final String namespace, + final Map<String, String> rootNodeProperty) { + try { + final DocumentBuilder documentBuilder = getDocumentBuilderFactory().newDocumentBuilder(); + final Document document = documentBuilder.newDocument(); + final Element rootElement = document.createElementNS(namespace, tagName); + for (final Map.Entry<String, String> entry : rootNodeProperty.entrySet()) { + final Element propertyElement = document.createElement(entry.getKey()); + propertyElement.setTextContent(entry.getValue()); + rootElement.appendChild(propertyElement); + } + rootElement.appendChild(document.adoptNode(node)); + document.appendChild(rootElement); + return document; + } catch (final ParserConfigurationException exception) { + throw new DataValidationException("Can't parse XML", "XML can't be parsed", exception); + } + } + + private static DocumentBuilderFactory getDocumentBuilderFactory() { + + if (isNewDocumentBuilderFactoryInstance) { + documentBuilderFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + documentBuilderFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); + isNewDocumentBuilderFactoryInstance = false; + } + + return documentBuilderFactory; + } + + private static TransformerFactory getTransformerFactory() { + + if (isNewTransformerFactoryInstance) { + transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, ""); + isNewTransformerFactoryInstance = false; + } + + return transformerFactory; + } +} diff --git a/cps-service/src/main/java/org/onap/cps/utils/YangUtils.java b/cps-service/src/main/java/org/onap/cps/utils/YangUtils.java index 48241ed392..c0dfe5205a 100644 --- a/cps-service/src/main/java/org/onap/cps/utils/YangUtils.java +++ b/cps-service/src/main/java/org/onap/cps/utils/YangUtils.java @@ -3,6 +3,8 @@ * Copyright (C) 2020-2022 Nordix Foundation * Modifications Copyright (C) 2021 Bell Canada. * Modifications Copyright (C) 2021 Pantheon.tech + * Modifications Copyright (C) 2022 TechMahindra Ltd. + * Modifications Copyright (C) 2022 Deutsche Telekom AG * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,24 +28,40 @@ import com.google.gson.JsonSyntaxException; import com.google.gson.stream.JsonReader; import java.io.IOException; import java.io.StringReader; +import java.net.URISyntaxException; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; +import javax.xml.transform.TransformerException; import lombok.AccessLevel; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.onap.cps.cpspath.parser.CpsPathUtil; +import org.onap.cps.cpspath.parser.PathParsingException; import org.onap.cps.spi.exceptions.DataValidationException; import org.opendaylight.yangtools.yang.common.QName; import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; +import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode; +import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild; +import org.opendaylight.yangtools.yang.data.api.schema.LeafNode; import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; +import org.opendaylight.yangtools.yang.data.api.schema.builder.DataContainerNodeBuilder; import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter; import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactory; import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactorySupplier; import org.opendaylight.yangtools.yang.data.codec.gson.JsonParserStream; +import org.opendaylight.yangtools.yang.data.codec.xml.XmlParserStream; +import org.opendaylight.yangtools.yang.data.impl.schema.Builders; import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNormalizedNodeStreamWriter; import org.opendaylight.yangtools.yang.data.impl.schema.NormalizedNodeResult; import org.opendaylight.yangtools.yang.model.api.DataNodeContainer; @@ -53,101 +71,191 @@ import org.opendaylight.yangtools.yang.model.api.EffectiveStatementInference; import org.opendaylight.yangtools.yang.model.api.SchemaContext; import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier; import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack; +import org.xml.sax.SAXException; @Slf4j @NoArgsConstructor(access = AccessLevel.PRIVATE) public class YangUtils { - private static final String XPATH_DELIMITER_REGEX = "\\/"; - private static final String XPATH_NODE_KEY_ATTRIBUTES_REGEX = "\\[.*?\\]"; + public static final String DATA_ROOT_NODE_NAMESPACE = "urn:ietf:params:xml:ns:netconf:base:1.0"; + public static final String DATA_ROOT_NODE_TAG_NAME = "data"; /** - * Parses jsonData into NormalizedNode according to given schema context. + * Parses data into Collection of NormalizedNode according to given schema context. * - * @param jsonData json data as string + * @param nodeData data string + * @param schemaContext schema context describing associated data model + * @return the NormalizedNode object + */ + public static ContainerNode parseData(final ContentType contentType, + final String nodeData, + final SchemaContext schemaContext) { + if (contentType == ContentType.JSON) { + return parseJsonDataWithOptionalParent(nodeData, schemaContext, Optional.empty()); + } + return parseXmlDataWithOptionalParent(nodeData, schemaContext, Optional.empty()); + } + + /** + * Parses data into NormalizedNode according to given schema context. + * + * @param nodeData data string * @param schemaContext schema context describing associated data model * @return the NormalizedNode object */ - public static NormalizedNode parseJsonData(final String jsonData, final SchemaContext schemaContext) { - return parseJsonData(jsonData, schemaContext, Optional.empty()); + public static ContainerNode parseData(final ContentType contentType, + final String nodeData, + final SchemaContext schemaContext, + final String parentNodeXpath) { + if (contentType == ContentType.JSON) { + return parseJsonDataWithOptionalParent(nodeData, schemaContext, Optional.of(parentNodeXpath)); + } + return parseXmlDataWithOptionalParent(nodeData, schemaContext, Optional.of(parentNodeXpath)); } /** - * Parses jsonData into NormalizedNode according to given schema context. + * Parses data into Collection of NormalizedNode according to given schema context. + * + * @param jsonData json data as string + * @param schemaContext schema context describing associated data model + * @return the Collection of NormalizedNode object + */ + public static ContainerNode parseJsonData(final String jsonData, final SchemaContext schemaContext) { + return parseJsonDataWithOptionalParent(jsonData, schemaContext, Optional.empty()); + } + + /** + * Parses jsonData into Collection of NormalizedNode according to given schema context. * * @param jsonData json data fragment as string * @param schemaContext schema context describing associated data model * @param parentNodeXpath the xpath referencing the parent node current data fragment belong to * @return the NormalizedNode object */ - public static NormalizedNode parseJsonData(final String jsonData, final SchemaContext schemaContext, - final String parentNodeXpath) { - final Collection<QName> dataSchemaNodeIdentifiers = - getDataSchemaNodeIdentifiersByXpath(parentNodeXpath, schemaContext); - return parseJsonData(jsonData, schemaContext, Optional.of(dataSchemaNodeIdentifiers)); + public static ContainerNode parseJsonData(final String jsonData, + final SchemaContext schemaContext, + final String parentNodeXpath) { + return parseJsonDataWithOptionalParent(jsonData, schemaContext, Optional.of(parentNodeXpath)); + } + + /** + * Create an xpath form a Yang Tools NodeIdentifier (i.e. PathArgument). + * + * @param nodeIdentifier the NodeIdentifier + * @return a xpath + */ + public static String buildXpath(final YangInstanceIdentifier.PathArgument nodeIdentifier) { + final StringBuilder xpathBuilder = new StringBuilder(); + xpathBuilder.append("/").append(nodeIdentifier.getNodeType().getLocalName()); + + if (nodeIdentifier instanceof YangInstanceIdentifier.NodeIdentifierWithPredicates) { + xpathBuilder.append(getKeyAttributesStatement( + (YangInstanceIdentifier.NodeIdentifierWithPredicates) nodeIdentifier)); + } + return xpathBuilder.toString(); } - private static NormalizedNode parseJsonData(final String jsonData, final SchemaContext schemaContext, - final Optional<Collection<QName>> dataSchemaNodeIdentifiers) { + private static ContainerNode parseJsonDataWithOptionalParent(final String jsonData, + final SchemaContext schemaContext, + final Optional<String> parentNodeXpath) { final JSONCodecFactory jsonCodecFactory = JSONCodecFactorySupplier.DRAFT_LHOTKA_NETMOD_YANG_JSON_02 .getShared((EffectiveModelContext) schemaContext); - final NormalizedNodeResult normalizedNodeResult = new NormalizedNodeResult(); + final DataContainerNodeBuilder<YangInstanceIdentifier.NodeIdentifier, ContainerNode> dataContainerNodeBuilder = + Builders.containerBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier( + QName.create(DATA_ROOT_NODE_NAMESPACE, DATA_ROOT_NODE_TAG_NAME) + )); final NormalizedNodeStreamWriter normalizedNodeStreamWriter = ImmutableNormalizedNodeStreamWriter - .from(normalizedNodeResult); + .from(dataContainerNodeBuilder); final JsonReader jsonReader = new JsonReader(new StringReader(jsonData)); final JsonParserStream jsonParserStream; - if (dataSchemaNodeIdentifiers.isPresent()) { + if (parentNodeXpath.isPresent()) { + final Collection<QName> dataSchemaNodeIdentifiers + = getDataSchemaNodeIdentifiers(schemaContext, parentNodeXpath.get()); final EffectiveModelContext effectiveModelContext = ((EffectiveModelContext) schemaContext); final EffectiveStatementInference effectiveStatementInference = SchemaInferenceStack.of(effectiveModelContext, - SchemaNodeIdentifier.Absolute.of(dataSchemaNodeIdentifiers.get())).toInference(); + SchemaNodeIdentifier.Absolute.of(dataSchemaNodeIdentifiers)).toInference(); jsonParserStream = JsonParserStream.create(normalizedNodeStreamWriter, jsonCodecFactory, effectiveStatementInference); } else { jsonParserStream = JsonParserStream.create(normalizedNodeStreamWriter, jsonCodecFactory); } - try { + try (jsonParserStream) { jsonParserStream.parse(jsonReader); - jsonParserStream.close(); - } catch (final JsonSyntaxException exception) { + } catch (final IOException | JsonSyntaxException exception) { throw new DataValidationException( - "Failed to parse json data: " + jsonData, exception.getMessage(), exception); - } catch (final IOException | IllegalStateException illegalStateException) { + "Failed to parse json data: " + jsonData, exception.getMessage(), exception); + } catch (final IllegalStateException | IllegalArgumentException exception) { throw new DataValidationException( - "Failed to parse json data. Unsupported xpath or json data:" + jsonData, illegalStateException - .getMessage(), illegalStateException); + "Failed to parse json data. Unsupported xpath or json data:" + jsonData, exception + .getMessage(), exception); } - return normalizedNodeResult.getResult(); + return dataContainerNodeBuilder.build(); } - /** - * Create an xpath form a Yang Tools NodeIdentifier (i.e. PathArgument). - * - * @param nodeIdentifier the NodeIdentifier - * @return an xpath - */ - public static String buildXpath(final YangInstanceIdentifier.PathArgument nodeIdentifier) { - final StringBuilder xpathBuilder = new StringBuilder(); - xpathBuilder.append("/").append(nodeIdentifier.getNodeType().getLocalName()); + private static ContainerNode parseXmlDataWithOptionalParent(final String xmlData, + final SchemaContext schemaContext, + final Optional<String> parentNodeXpath) { + final XMLInputFactory factory = XMLInputFactory.newInstance(); + factory.setProperty(XMLInputFactory.SUPPORT_DTD, false); + final NormalizedNodeResult normalizedNodeResult = new NormalizedNodeResult(); + final NormalizedNodeStreamWriter normalizedNodeStreamWriter = ImmutableNormalizedNodeStreamWriter + .from(normalizedNodeResult); - if (nodeIdentifier instanceof YangInstanceIdentifier.NodeIdentifierWithPredicates) { - xpathBuilder.append(getKeyAttributesStatement( - (YangInstanceIdentifier.NodeIdentifierWithPredicates) nodeIdentifier)); + final EffectiveModelContext effectiveModelContext = (EffectiveModelContext) schemaContext; + final XmlParserStream xmlParserStream; + final String preparedXmlContent; + try { + if (parentNodeXpath.isPresent()) { + final DataSchemaNode parentSchemaNode = + (DataSchemaNode) getDataSchemaNodeAndIdentifiersByXpath(parentNodeXpath.get(), schemaContext) + .get("dataSchemaNode"); + final Collection<QName> dataSchemaNodeIdentifiers = + getDataSchemaNodeIdentifiers(schemaContext, parentNodeXpath.get()); + final EffectiveStatementInference effectiveStatementInference = + SchemaInferenceStack.of(effectiveModelContext, + SchemaNodeIdentifier.Absolute.of(dataSchemaNodeIdentifiers)).toInference(); + preparedXmlContent = XmlFileUtils.prepareXmlContent(xmlData, parentSchemaNode, parentNodeXpath.get()); + xmlParserStream = XmlParserStream.create(normalizedNodeStreamWriter, effectiveStatementInference); + } else { + preparedXmlContent = XmlFileUtils.prepareXmlContent(xmlData, schemaContext); + xmlParserStream = XmlParserStream.create(normalizedNodeStreamWriter, effectiveModelContext); + } + + try (xmlParserStream; + StringReader stringReader = new StringReader(preparedXmlContent)) { + final XMLStreamReader xmlStreamReader = factory.createXMLStreamReader(stringReader); + xmlParserStream.parse(xmlStreamReader); + } + } catch (final XMLStreamException | URISyntaxException | IOException | SAXException | NullPointerException + | ParserConfigurationException | TransformerException exception) { + throw new DataValidationException( + "Failed to parse xml data: " + xmlData, exception.getMessage(), exception); } - return xpathBuilder.toString(); + final DataContainerChild dataContainerChild = + (DataContainerChild) getFirstChildXmlRoot(normalizedNodeResult.getResult()); + final YangInstanceIdentifier.NodeIdentifier nodeIdentifier = + new YangInstanceIdentifier.NodeIdentifier(dataContainerChild.getIdentifier().getNodeType()); + return Builders.containerBuilder().withChild(dataContainerChild).withNodeIdentifier(nodeIdentifier).build(); } + private static Collection<QName> getDataSchemaNodeIdentifiers(final SchemaContext schemaContext, + final String parentNodeXpath) { + return (Collection<QName>) getDataSchemaNodeAndIdentifiersByXpath(parentNodeXpath, schemaContext) + .get("dataSchemaNodeIdentifiers"); + } private static String getKeyAttributesStatement( - final YangInstanceIdentifier.NodeIdentifierWithPredicates nodeIdentifier) { + final YangInstanceIdentifier.NodeIdentifierWithPredicates nodeIdentifier) { final List<String> keyAttributes = nodeIdentifier.entrySet().stream().map( - entry -> { - final String name = entry.getKey().getLocalName(); - final String value = String.valueOf(entry.getValue()).replace("'", "\\'"); - return String.format("@%s='%s'", name, value); - } + entry -> { + final String name = entry.getKey().getLocalName(); + final String value = String.valueOf(entry.getValue()).replace("'", "\\'"); + return String.format("@%s='%s'", name, value); + } ).collect(Collectors.toList()); if (keyAttributes.isEmpty()) { @@ -158,26 +266,23 @@ public class YangUtils { } } - private static Collection<QName> getDataSchemaNodeIdentifiersByXpath(final String parentNodeXpath, - final SchemaContext schemaContext) { + private static Map<String, Object> getDataSchemaNodeAndIdentifiersByXpath(final String parentNodeXpath, + final SchemaContext schemaContext) { final String[] xpathNodeIdSequence = xpathToNodeIdSequence(parentNodeXpath); - return findDataSchemaNodeIdentifiersByXpathNodeIdSequence(xpathNodeIdSequence, schemaContext.getChildNodes(), + return findDataSchemaNodeAndIdentifiersByXpathNodeIdSequence(xpathNodeIdSequence, schemaContext.getChildNodes(), new ArrayList<>()); } private static String[] xpathToNodeIdSequence(final String xpath) { - final String[] xpathNodeIdSequence = Arrays.stream(xpath - .replaceAll(XPATH_NODE_KEY_ATTRIBUTES_REGEX, "") - .split(XPATH_DELIMITER_REGEX)) - .filter(identifier -> !identifier.isEmpty()) - .toArray(String[]::new); - if (xpathNodeIdSequence.length < 1) { - throw new DataValidationException("Invalid xpath.", "Xpath contains no node identifiers."); + try { + return CpsPathUtil.getXpathNodeIdSequence(xpath); + } catch (final PathParsingException pathParsingException) { + throw new DataValidationException(pathParsingException.getMessage(), pathParsingException.getDetails(), + pathParsingException); } - return xpathNodeIdSequence; } - private static Collection<QName> findDataSchemaNodeIdentifiersByXpathNodeIdSequence( + private static Map<String, Object> findDataSchemaNodeAndIdentifiersByXpathNodeIdSequence( final String[] xpathNodeIdSequence, final Collection<? extends DataSchemaNode> dataSchemaNodes, final Collection<QName> dataSchemaNodeIdentifiers) { @@ -187,11 +292,15 @@ public class YangUtils { .findFirst().orElseThrow(() -> schemaNodeNotFoundException(currentXpathNodeId)); dataSchemaNodeIdentifiers.add(currentDataSchemaNode.getQName()); if (xpathNodeIdSequence.length <= 1) { - return dataSchemaNodeIdentifiers; + final Map<String, Object> dataSchemaNodeAndIdentifiers = + new HashMap<>(); + dataSchemaNodeAndIdentifiers.put("dataSchemaNode", currentDataSchemaNode); + dataSchemaNodeAndIdentifiers.put("dataSchemaNodeIdentifiers", dataSchemaNodeIdentifiers); + return dataSchemaNodeAndIdentifiers; } if (currentDataSchemaNode instanceof DataNodeContainer) { - return findDataSchemaNodeIdentifiersByXpathNodeIdSequence( - getNextLevelXpathNodeIdSequence(xpathNodeIdSequence), + return findDataSchemaNodeAndIdentifiersByXpathNodeIdSequence( + getNextLevelXpathNodeIdSequence(xpathNodeIdSequence), ((DataNodeContainer) currentDataSchemaNode).getChildNodes(), dataSchemaNodeIdentifiers); } @@ -208,4 +317,19 @@ public class YangUtils { return new DataValidationException("Invalid xpath.", String.format("No schema node was found for xpath identifier '%s'.", schemaNodeIdentifier)); } + + private static NormalizedNode getFirstChildXmlRoot(final NormalizedNode parent) { + final String rootNodeType = parent.getIdentifier().getNodeType().getLocalName(); + final Collection<DataContainerChild> children = (Collection<DataContainerChild>) parent.body(); + final Iterator<DataContainerChild> iterator = children.iterator(); + NormalizedNode child = null; + while (iterator.hasNext()) { + child = iterator.next(); + if (!child.getIdentifier().getNodeType().getLocalName().equals(rootNodeType) + && !(child instanceof LeafNode)) { + return child; + } + } + return getFirstChildXmlRoot(child); + } } 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 b60e7e86e0..c81a50ea74 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 @@ -3,7 +3,8 @@ * Copyright (C) 2021-2022 Nordix Foundation * Modifications Copyright (C) 2021 Pantheon.tech * Modifications Copyright (C) 2021-2022 Bell Canada. - * ================================================================================ + * Modifications Copyright (C) 2022 TechMahindra Ltd. + * Modifications Copyright (C) 2022 Deutsche Telekom AG * 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 @@ -32,6 +33,7 @@ import org.onap.cps.spi.exceptions.DataValidationException import org.onap.cps.spi.model.Anchor import org.onap.cps.spi.model.DataNode import org.onap.cps.spi.model.DataNodeBuilder +import org.onap.cps.utils.ContentType import org.onap.cps.yang.YangTextSchemaSourceSet import org.onap.cps.yang.YangTextSchemaSourceSetBuilder import spock.lang.Specification @@ -60,21 +62,59 @@ class CpsDataServiceImplSpec extends Specification { def anchor = Anchor.builder().name(anchorName).schemaSetName(schemaSetName).build() def observedTimestamp = OffsetDateTime.now() - def 'Saving json data.'() { + def 'Saving multicontainer json data.'() { given: 'schema set for given anchor and dataspace references test-tree model' - setupSchemaSetMocks('test-tree.yang') + setupSchemaSetMocks('multipleDataTree.yang') when: 'save data method is invoked with test-tree json data' - def jsonData = TestUtils.getResourceFileContent('test-tree.json') + def jsonData = TestUtils.getResourceFileContent('multiple-object-data.json') objectUnderTest.saveData(dataspaceName, anchorName, jsonData, observedTimestamp) then: 'the persistence service method is invoked with correct parameters' - 1 * mockCpsDataPersistenceService.storeDataNode(dataspaceName, anchorName, - { dataNode -> dataNode.xpath == '/test-tree' }) + 1 * mockCpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName, + { dataNode -> dataNode.xpath[index] == xpath }) + 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) + where: + index | xpath + 0 | '/first-container' + 1 | '/last-container' + + } + + def 'Saving #scenario data.'() { + given: 'schema set for given anchor and dataspace references test-tree model' + setupSchemaSetMocks('test-tree.yang') + when: 'save data method is invoked with test-tree #scenario data' + def data = TestUtils.getResourceFileContent(dataFile) + objectUnderTest.saveData(dataspaceName, anchorName, data, observedTimestamp, contentType) + then: 'the persistence service method is invoked with correct parameters' + 1 * mockCpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName, + { dataNode -> dataNode.xpath[0] == '/test-tree' }) 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) + where: 'given parameters' + scenario | dataFile | contentType + 'json' | 'test-tree.json' | ContentType.JSON + 'xml' | 'test-tree.xml' | ContentType.XML } + def 'Saving #scenarioDesired data with invalid data.'() { + given: 'schema set for given anchor and dataspace references test-tree model' + setupSchemaSetMocks('test-tree.yang') + when: 'save data method is invoked with test-tree json data' + objectUnderTest.saveData(dataspaceName, anchorName, invalidData, observedTimestamp, contentType) + then: 'a data validation exception is thrown' + thrown(DataValidationException) + where: 'given parameters' + scenarioDesired | invalidData | contentType + 'json' | '{invalid json' | ContentType.XML + 'xml' | '<invalid xml' | ContentType.JSON + } + + def 'Saving child data fragment under existing node.'() { given: 'schema set for given anchor and dataspace references test-tree model' setupSchemaSetMocks('test-tree.yang') @@ -82,8 +122,8 @@ class CpsDataServiceImplSpec extends Specification { def jsonData = '{"branch": [{"name": "New"}]}' objectUnderTest.saveData(dataspaceName, anchorName, '/test-tree', jsonData, observedTimestamp) then: 'the persistence service method is invoked with correct parameters' - 1 * mockCpsDataPersistenceService.addChildDataNode(dataspaceName, anchorName, '/test-tree', - { dataNode -> dataNode.xpath == '/test-tree/branch[@name=\'New\']' }) + 1 * mockCpsDataPersistenceService.addChildDataNodes(dataspaceName, anchorName, '/test-tree', + { dataNode -> dataNode.xpath[0] == '/test-tree/branch[@name=\'New\']' }) and: 'the CpsValidator is called on the dataspaceName and AnchorName' 1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName) and: 'data updated event is sent to notification service' @@ -207,8 +247,8 @@ class CpsDataServiceImplSpec extends Specification { when: 'replace data method is invoked with json data #jsonData and parent node xpath #parentNodeXpath' objectUnderTest.updateDataNodeAndDescendants(dataspaceName, anchorName, parentNodeXpath, jsonData, observedTimestamp) then: 'the persistence service method is invoked with correct parameters' - 1 * mockCpsDataPersistenceService.updateDataNodeAndDescendants(dataspaceName, anchorName, - { dataNode -> dataNode.xpath == expectedNodeXpath }) + 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) and: 'the CpsValidator is called on the dataspaceName and AnchorName' 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 2fc85aa5a4..ccfb23b449 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 @@ -3,6 +3,7 @@ * Copyright (C) 2021-2022 Nordix Foundation.
* Modifications Copyright (C) 2021-2022 Bell Canada.
* Modifications Copyright (C) 2021 Pantheon.tech
+ * Modifications Copyright (C) 2022 TechMahindra Ltd.
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -90,9 +91,9 @@ class E2ENetworkSliceSpec extends Specification { when: 'saveData method is invoked'
cpsDataServiceImpl.saveData(dataspaceName, anchorName, jsonData, noTimestamp)
then: 'Parameters are validated and processing is delegated to persistence service'
- 1 * mockDataStoreService.storeDataNode('someDataspace', 'someAnchor', _) >>
+ 1 * mockDataStoreService.storeDataNodes('someDataspace', 'someAnchor', _) >>
{ args -> dataNodeStored = args[2]}
- def child = dataNodeStored.childDataNodes[0]
+ def child = dataNodeStored[0].childDataNodes[0]
assert child.childDataNodes.size() == 1
and: 'list of Tracking Area for a Coverage Area are stored with correct xpath and child nodes '
def listOfTAForCoverageArea = child.childDataNodes[0]
@@ -122,10 +123,10 @@ class E2ENetworkSliceSpec extends Specification { when: 'saveData method is invoked'
cpsDataServiceImpl.saveData('someDataspace', 'someAnchor', jsonData, noTimestamp)
then: 'parameters are validated and processing is delegated to persistence service'
- 1 * mockDataStoreService.storeDataNode('someDataspace', 'someAnchor', _) >>
+ 1 * mockDataStoreService.storeDataNodes('someDataspace', 'someAnchor', _) >>
{ args -> dataNodeStored = args[2]}
and: 'the size of the tree is correct'
- def cpsRanInventory = TestUtils.getFlattenMapByXpath(dataNodeStored)
+ def cpsRanInventory = TestUtils.getFlattenMapByXpath(dataNodeStored[0])
assert cpsRanInventory.size() == 4
and: 'ran-inventory contains the correct child node'
def ranInventory = cpsRanInventory.get('/ran-inventory')
diff --git a/cps-service/src/test/groovy/org/onap/cps/spi/model/DataNodeBuilderSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/spi/model/DataNodeBuilderSpec.groovy index e46147c04d..1559783e97 100644 --- a/cps-service/src/test/groovy/org/onap/cps/spi/model/DataNodeBuilderSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/spi/model/DataNodeBuilderSpec.groovy @@ -2,6 +2,7 @@ * ============LICENSE_START======================================================= * Copyright (C) 2021 Pantheon.tech * Modifications Copyright (C) 2021-2022 Nordix Foundation. + * Modifications Copyright (C) 2022 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,13 +22,10 @@ package org.onap.cps.spi.model import org.onap.cps.TestUtils -import org.onap.cps.spi.model.DataNodeBuilder import org.onap.cps.utils.DataMapUtils import org.onap.cps.utils.YangUtils import org.onap.cps.yang.YangTextSchemaSourceSetBuilder -import org.opendaylight.yangtools.yang.common.QName -import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier -import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode +import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode import spock.lang.Specification class DataNodeBuilderSpec extends Specification { @@ -50,17 +48,17 @@ class DataNodeBuilderSpec extends Specification { 'ietf/ietf-inet-types@2013-07-15.yang' ] - def 'Converting NormalizedNode (tree) to a DataNode (tree).'() { + def 'Converting ContainerNode (tree) to a DataNode (tree).'() { given: 'the schema context for expected model' def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('test-tree.yang') def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent) getSchemaContext() - and: 'the json data parsed into normalized node object' + and: 'the json data parsed into container node object' def jsonData = TestUtils.getResourceFileContent('test-tree.json') - def normalizedNode = YangUtils.parseJsonData(jsonData, schemaContext) - when: 'the normalized node is converted to a data node' - def result = new DataNodeBuilder().withNormalizedNodeTree(normalizedNode).build() + def containerNode = YangUtils.parseJsonData(jsonData, schemaContext) + when: 'the container node is converted to a data node' + def result = new DataNodeBuilder().withContainerNode(containerNode).build() def mappedResult = TestUtils.getFlattenMapByXpath(result) - then: '5 DataNode objects with unique xpath were created in total' + then: '6 DataNode objects with unique xpath were created in total' mappedResult.size() == 6 and: 'all expected xpaths were built' mappedResult.keySet().containsAll(expectedLeavesByXpathMap.keySet()) @@ -70,16 +68,16 @@ class DataNodeBuilderSpec extends Specification { } } - def 'Converting NormalizedNode (tree) to a DataNode (tree) for known parent node.'() { + def 'Converting ContainerNode (tree) to a DataNode (tree) for known parent node.'() { given: 'a schema context for expected model' def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('test-tree.yang') def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent) getSchemaContext() - and: 'the json data parsed into normalized node object' + and: 'the json data parsed into container node object' def jsonData = '{ "branch": [{ "name": "Branch", "nest": { "name": "Nest", "birds": ["bird"] } }] }' - def normalizedNode = YangUtils.parseJsonData(jsonData, schemaContext, "/test-tree") - when: 'the normalized node is converted to a data node with parent node xpath defined' + def containerNode = YangUtils.parseJsonData(jsonData, schemaContext, "/test-tree") + when: 'the container node is converted to a data node with parent node xpath defined' def result = new DataNodeBuilder() - .withNormalizedNodeTree(normalizedNode) + .withContainerNode(containerNode) .withParentNodeXpath("/test-tree") .build() def mappedResult = TestUtils.getFlattenMapByXpath(result) @@ -90,15 +88,15 @@ class DataNodeBuilderSpec extends Specification { .containsAll(['/test-tree/branch[@name=\'Branch\']', '/test-tree/branch[@name=\'Branch\']/nest']) } - def 'Converting NormalizedNode (tree) to a DataNode (tree) -- augmentation case.'() { + def 'Converting ContainerNode (tree) to a DataNode (tree) -- augmentation case.'() { given: 'a schema context for expected model' def yangResourceNameToContent = TestUtils.getYangResourcesAsMap(networkTopologyModelRfc8345) def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent) getSchemaContext() - and: 'the json data parsed into normalized node object' + and: 'the json data parsed into container node object' def jsonData = TestUtils.getResourceFileContent('ietf/data/ietf-network-topology-sample-rfc8345.json') - def normalizedNode = YangUtils.parseJsonData(jsonData, schemaContext) - when: 'the normalized node is converted to a data node ' - def result = new DataNodeBuilder().withNormalizedNodeTree(normalizedNode).build() + def containerNode = YangUtils.parseJsonData(jsonData, schemaContext) + when: 'the container node is converted to a data node ' + def result = new DataNodeBuilder().withContainerNode(containerNode).build() def mappedResult = TestUtils.getFlattenMapByXpath(result) then: 'all expected data nodes are populated' mappedResult.size() == 32 @@ -122,17 +120,17 @@ class DataNodeBuilderSpec extends Specification { ]) } - def 'Converting NormalizedNode (tree) to a DataNode (tree) for known parent node -- augmentation case.'() { + def 'Converting ContainerNode (tree) to a DataNode (tree) for known parent node -- augmentation case.'() { given: 'a schema context for expected model' def yangResourceNameToContent = TestUtils.getYangResourcesAsMap(networkTopologyModelRfc8345) def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent) getSchemaContext() and: 'parent node xpath referencing augmentation node within a model' def parentNodeXpath = "/networks/network[@network-id='otn-hc']/link[@link-id='D1,1-2-1,D2,2-1-1']" - and: 'the json data fragment parsed into normalized node object for given parent node xpath' + and: 'the json data fragment parsed into container node object for given parent node xpath' def jsonData = '{"source": {"source-node": "D1", "source-tp": "1-2-1"}}' - def normalizedNode = YangUtils.parseJsonData(jsonData, schemaContext, parentNodeXpath) - when: 'the normalized node is converted to a data node with given parent node xpath' - def result = new DataNodeBuilder().withNormalizedNodeTree(normalizedNode) + def containerNode = YangUtils.parseJsonData(jsonData, schemaContext, parentNodeXpath) + when: 'the container node is converted to a data node with given parent node xpath' + def result = new DataNodeBuilder().withContainerNode(containerNode) .withParentNodeXpath(parentNodeXpath).build() then: 'the resulting data node represents a child of augmentation node' assert result.xpath == "/networks/network[@network-id='otn-hc']/link[@link-id='D1,1-2-1,D2,2-1-1']/source" @@ -140,15 +138,15 @@ class DataNodeBuilderSpec extends Specification { assert result.leaves['source-tp'] == '1-2-1' } - def 'Converting NormalizedNode (tree) to a DataNode (tree) -- with ChoiceNode.'() { + def 'Converting ContainerNode (tree) to a DataNode (tree) -- with ChoiceNode.'() { given: 'a schema context for expected model' def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('yang-with-choice-node.yang') def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent) getSchemaContext() - and: 'the json data fragment parsed into normalized node object' + and: 'the json data fragment parsed into container node object' def jsonData = TestUtils.getResourceFileContent('data-with-choice-node.json') - def normalizedNode = YangUtils.parseJsonData(jsonData, schemaContext) - when: 'the normalized node is converted to a data node' - def result = new DataNodeBuilder().withNormalizedNodeTree(normalizedNode).build() + def containerNode = YangUtils.parseJsonData(jsonData, schemaContext) + when: 'the container node is converted to a data node' + def result = new DataNodeBuilder().withContainerNode(containerNode).build() def mappedResult = TestUtils.getFlattenMapByXpath(result) then: 'the resulting data node contains only one xpath with 3 leaves' mappedResult.keySet().containsAll([ @@ -159,16 +157,16 @@ class DataNodeBuilderSpec extends Specification { assert result.leaves['choice-case1-leaf-b'] == "test" } - def 'Converting NormalizedNode into DataNode collection: #scenario.'() { + def 'Converting ContainerNode into DataNode collection: #scenario.'() { given: 'a schema context for expected model' def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('test-tree.yang') def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent) getSchemaContext() and: 'parent node xpath referencing parent of list element' def parentNodeXpath = "/test-tree" - and: 'the json data fragment (list element) parsed into normalized node object' - def normalizedNode = YangUtils.parseJsonData(jsonData, schemaContext, parentNodeXpath) - when: 'the normalized node is converted to a data node collection' - def result = new DataNodeBuilder().withNormalizedNodeTree(normalizedNode) + and: 'the json data fragment (list element) parsed into container node object' + def containerNode = YangUtils.parseJsonData(jsonData, schemaContext, parentNodeXpath) + when: 'the container node is converted to a data node collection' + def result = new DataNodeBuilder().withContainerNode(containerNode) .withParentNodeXpath(parentNodeXpath).buildCollection() def resultXpaths = result.collect { it.getXpath() } then: 'the resulting collection contains data nodes for expected list elements' @@ -180,16 +178,15 @@ class DataNodeBuilderSpec extends Specification { 'multiple entries' | '{"branch": [{"name": "One"}, {"name": "Two"}]}' | 2 | ['/test-tree/branch[@name=\'One\']', '/test-tree/branch[@name=\'Two\']'] } - def 'Converting NormalizedNode to a DataNode collection -- edge cases: #scenario.'() { - when: 'the normalized node is #node' - def result = new DataNodeBuilder().withNormalizedNodeTree(normalizedNode).buildCollection() + def 'Converting ContainerNode to a DataNode collection -- edge cases: #scenario.'() { + when: 'the container node is #node' + def result = new DataNodeBuilder().withContainerNode(containerNode).buildCollection() then: 'the resulting collection contains data nodes for expected list elements' - assert result.size() == expectedSize - assert result.containsAll(expectedNodes) + assert result.isEmpty() where: 'following parameters are used' - scenario | node | normalizedNode | expectedSize | expectedNodes - 'NormalizedNode is null' | 'null' | null | 1 | [ new DataNode() ] - 'NormalizedNode is an unsupported type' | 'not supported' | Mock(NormalizedNode) | 0 | [ ] + scenario | containerNode + 'ContainerNode is null' | null + 'ContainerNode is an unsupported type' | Mock(ContainerNode) } def 'Use of adding the module name prefix attribute of data node.'() { diff --git a/cps-service/src/test/groovy/org/onap/cps/utils/JsonObjectMapperSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/utils/JsonObjectMapperSpec.groovy index e205a19eed..b70c437953 100644 --- a/cps-service/src/test/groovy/org/onap/cps/utils/JsonObjectMapperSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/utils/JsonObjectMapperSpec.groovy @@ -41,7 +41,7 @@ class JsonObjectMapperSpec extends Specification { then: 'the result is a valid json string (can be parsed)' def contentMap = new JsonSlurper().parseText(content) and: 'the parsed content is as expected' - assert contentMap.'test:bookstore'.'bookstore-name' == 'Chapters' + assert contentMap.'test:bookstore'.'bookstore-name' == 'Chapters/Easons' } def 'Map a structured object to json String error.'() { diff --git a/cps-service/src/test/groovy/org/onap/cps/utils/JsonParserStreamSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/utils/JsonParserStreamSpec.groovy index 40f0e0a2ae..2eede23913 100644 --- a/cps-service/src/test/groovy/org/onap/cps/utils/JsonParserStreamSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/utils/JsonParserStreamSpec.groovy @@ -1,3 +1,24 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022 Nordix Foundation + * Modifications Copyright (C) 2022 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.utils import com.google.gson.stream.JsonReader @@ -26,10 +47,10 @@ class JsonParserStreamSpec extends Specification{ def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourcesMap).getSchemaContext() and: 'variable to store the result of parsing' DataContainerNodeBuilder<YangInstanceIdentifier.NodeIdentifier, ContainerNode> builder = - Builders.containerBuilder().withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(schemaContext.getQName())); - def normalizedNodeStreamWriter = ImmutableNormalizedNodeStreamWriter.from(builder); + Builders.containerBuilder().withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(schemaContext.getQName())) + def normalizedNodeStreamWriter = ImmutableNormalizedNodeStreamWriter.from(builder) def jsonCodecFactory = JSONCodecFactorySupplier.DRAFT_LHOTKA_NETMOD_YANG_JSON_02 - .getShared((EffectiveModelContext) schemaContext); + .getShared((EffectiveModelContext) schemaContext) and: 'JSON parser stream' def jsonParserStream = JsonParserStream.create(normalizedNodeStreamWriter, jsonCodecFactory) when: 'parsing is invoked with the given JSON reader' diff --git a/cps-service/src/test/groovy/org/onap/cps/utils/XmlFileUtilsSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/utils/XmlFileUtilsSpec.groovy new file mode 100644 index 0000000000..b044e2e727 --- /dev/null +++ b/cps-service/src/test/groovy/org/onap/cps/utils/XmlFileUtilsSpec.groovy @@ -0,0 +1,61 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2022 Deutsche Telekom AG + * ================================================================================ + * 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.utils + +import org.onap.cps.TestUtils +import org.onap.cps.yang.YangTextSchemaSourceSetBuilder +import spock.lang.Specification + +class XmlFileUtilsSpec extends Specification { + def 'Parse a valid xml content #scenario'(){ + given: 'YANG model schema context' + def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('bookstore.yang') + def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext() + when: 'the XML data is parsed' + def parsedXmlContent = XmlFileUtils.prepareXmlContent(xmlData, schemaContext) + then: 'the result XML is wrapped by root node defined in YANG schema' + assert parsedXmlContent == expectedOutput + where: + scenario | xmlData || expectedOutput + 'without root data node' | '<?xml version="1.0" encoding="UTF-8"?><class> </class>' || '<?xml version="1.0" encoding="UTF-8"?><stores xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"><class> </class></stores>' + 'with root data node' | '<?xml version="1.0" encoding="UTF-8"?><stores><class> </class></stores>' || '<?xml version="1.0" encoding="UTF-8"?><stores><class> </class></stores>' + 'no xml header' | '<stores><class> </class></stores>' || '<stores><class> </class></stores>' + } + + def 'Parse a xml content with XPath container #scenario'() { + given: 'YANG model schema context' + def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('test-tree.yang') + def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext() + and: 'Parent schema node by xPath' + def parentSchemaNode = YangUtils.getDataSchemaNodeAndIdentifiersByXpath(xPath, schemaContext) + .get("dataSchemaNode") + when: 'the XML data is parsed' + def parsedXmlContent = XmlFileUtils.prepareXmlContent(xmlData, parentSchemaNode, xPath) + then: 'the result XML is wrapped by xPath defined parent root node' + assert parsedXmlContent == expectedOutput + where: + scenario | xmlData | xPath || expectedOutput + 'XML element test tree' | '<?xml version="1.0" encoding="UTF-8"?><test-tree xmlns="org:onap:cps:test:test-tree"><branch><name>Left</name><nest><name>Small</name><birds>Sparrow</birds></nest></branch></test-tree>' | '/test-tree' || '<?xml version="1.0" encoding="UTF-8"?><test-tree xmlns="org:onap:cps:test:test-tree"><branch><name>Left</name><nest><name>Small</name><birds>Sparrow</birds></nest></branch></test-tree>' + 'without root data node' | '<?xml version="1.0" encoding="UTF-8"?><nest xmlns="org:onap:cps:test:test-tree"><name>Small</name><birds>Sparrow</birds></nest>' | '/test-tree/branch[@name=\'Branch\']' || '<?xml version="1.0" encoding="UTF-8"?><branch xmlns="org:onap:cps:test:test-tree"><name>Branch</name><nest><name>Small</name><birds>Sparrow</birds></nest></branch>' + + + } + +} diff --git a/cps-service/src/test/groovy/org/onap/cps/utils/YangUtilsSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/utils/YangUtilsSpec.groovy index 65aa3af7d8..bf6e134a65 100644 --- a/cps-service/src/test/groovy/org/onap/cps/utils/YangUtilsSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/utils/YangUtilsSpec.groovy @@ -2,6 +2,8 @@ * ============LICENSE_START======================================================= * Copyright (C) 2020-2022 Nordix Foundation * Modifications Copyright (C) 2021 Pantheon.tech + * Modifications Copyright (C) 2022 TechMahindra Ltd. + * Modifications Copyright (C) 2022 Deutsche Telekom AG * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,16 +31,42 @@ import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode import spock.lang.Specification class YangUtilsSpec extends Specification { - def 'Parsing a valid Json String.'() { + def 'Parsing a valid multicontainer Json String.'() { given: 'a yang model (file)' - def jsonData = org.onap.cps.TestUtils.getResourceFileContent('bookstore.json') + def jsonData = org.onap.cps.TestUtils.getResourceFileContent('multiple-object-data.json') and: 'a model for that data' - def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('bookstore.yang') + def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('multipleDataTree.yang') def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext() when: 'the json data is parsed' - NormalizedNode result = YangUtils.parseJsonData(jsonData, schemaContext) + def result = YangUtils.parseJsonData(jsonData, schemaContext) + then: 'a ContainerNode holding collection of normalized nodes is returned' + result.body().getAt(index) instanceof NormalizedNode == true + then: 'qualified name of children created is as expected' + result.body().getAt(index).getIdentifier().nodeType == QName.create('org:onap:ccsdk:multiDataTree', '2020-09-15', nodeName) + where: + index | nodeName + 0 | 'first-container' + 1 | 'last-container' + } + + def 'Parsing a valid #scenario String.'() { + given: 'a yang model (file)' + def fileData = org.onap.cps.TestUtils.getResourceFileContent(contentFile) + and: 'a model for that data' + def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('bookstore.yang') + def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext() + when: 'the data is parsed' + NormalizedNode result = YangUtils.parseData(contentType, fileData, schemaContext) then: 'the result is a normalized node of the correct type' - result.getIdentifier().nodeType == QName.create('org:onap:ccsdk:sample', '2020-09-15', 'bookstore') + if (revision) { + result.identifier.nodeType == QName.create(namespace, revision, localName) + } else { + result.identifier.nodeType == QName.create(namespace, localName) + } + where: + scenario | contentFile | contentType | namespace | revision | localName + 'JSON' | 'bookstore.json' | ContentType.JSON | 'org:onap:ccsdk:sample' | '2020-09-15' | 'bookstore' + 'XML' | 'bookstore.xml' | ContentType.XML | 'urn:ietf:params:xml:ns:netconf:base:1.0' | '' | 'bookstore' } def 'Parsing invalid data: #description.'() { @@ -46,29 +74,37 @@ class YangUtilsSpec extends Specification { def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('bookstore.yang') def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext() when: 'invalid data is parsed' - YangUtils.parseJsonData(invalidJson, schemaContext) + YangUtils.parseData(contentType, invalidData, schemaContext) then: 'an exception is thrown' thrown(DataValidationException) - where: 'the following invalid json is provided' - invalidJson | description - '{incomplete json' | 'incomplete json' - '{"test:bookstore": {"address": "Parnell st." }}' | 'json with un-modelled data' - '{" }' | 'json with syntax exception' + where: 'the following invalid data is provided' + invalidData | contentType | description + '{incomplete json' | ContentType.JSON | 'incomplete json' + '{"test:bookstore": {"address": "Parnell st." }}' | ContentType.JSON | 'json with un-modelled data' + '{" }' | ContentType.JSON | 'json with syntax exception' + '<data>' | ContentType.XML | 'incomplete xml' + '<data><bookstore><bookstore-anything>blabla</bookstore-anything></bookstore</data>' | ContentType.XML | 'xml with invalid model' + '' | ContentType.XML | 'empty xml' } - def 'Parsing json data fragment by xpath for #scenario.'() { + def 'Parsing data fragment by xpath for #scenario.'() { given: 'schema context' def yangResourcesMap = TestUtils.getYangResourcesAsMap('test-tree.yang') def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourcesMap).getSchemaContext() when: 'json string is parsed' - def result = YangUtils.parseJsonData(jsonData, schemaContext, parentNodeXpath) + def result = YangUtils.parseData(contentType, nodeData, schemaContext, parentNodeXpath) + then: 'a ContainerNode holding collection of normalized nodes is returned' + result.body().getAt(0) instanceof NormalizedNode == true then: 'result represents a node of expected type' - result.getIdentifier().nodeType == QName.create('org:onap:cps:test:test-tree', '2020-02-02', nodeName) + result.body().getAt(0).getIdentifier().nodeType == QName.create('org:onap:cps:test:test-tree', '2020-02-02', nodeName) where: - scenario | jsonData | parentNodeXpath || nodeName - 'list element as container' | '{ "branch": { "name": "B", "nest": { "name": "N", "birds": ["bird"] } } }' | '/test-tree' || 'branch' - 'list element within list' | '{ "branch": [{ "name": "B", "nest": { "name": "N", "birds": ["bird"] } }] }' | '/test-tree' || 'branch' - 'container element' | '{ "nest": { "name": "N", "birds": ["bird"] } }' | '/test-tree/branch[@name=\'Branch\']' || 'nest' + scenario | contentType | nodeData | parentNodeXpath || nodeName + 'JSON list element as container' | ContentType.JSON | '{ "branch": { "name": "B", "nest": { "name": "N", "birds": ["bird"] } } }' | '/test-tree' || 'branch' + 'JSON list element within list' | ContentType.JSON | '{ "branch": [{ "name": "B", "nest": { "name": "N", "birds": ["bird"] } }] }' | '/test-tree' || 'branch' + 'JSON container element' | ContentType.JSON | '{ "nest": { "name": "N", "birds": ["bird"] } }' | '/test-tree/branch[@name=\'Branch\']' || 'nest' + 'XML element test tree' | ContentType.XML | '<?xml version=\'1.0\' encoding=\'UTF-8\'?><branch xmlns="org:onap:cps:test:test-tree"><name>Left</name><nest><name>Small</name><birds>Sparrow</birds></nest></branch>' | '/test-tree' || 'branch' + 'XML element branch xpath' | ContentType.XML | '<?xml version=\'1.0\' encoding=\'UTF-8\'?><branch xmlns="org:onap:cps:test:test-tree"><name>Left</name><nest><name>Small</name><birds>Sparrow</birds><birds>Robin</birds></nest></branch>' | '/test-tree' || 'branch' + 'XML container element' | ContentType.XML | '<?xml version=\'1.0\' encoding=\'UTF-8\'?><nest xmlns="org:onap:cps:test:test-tree"><name>Small</name><birds>Sparrow</birds></nest>' | '/test-tree/branch[@name=\'Branch\']' || 'nest' } def 'Parsing json data fragment by xpath error scenario: #scenario.'() { @@ -126,5 +162,4 @@ class YangUtilsSpec extends Specification { 'xpath contains list attribute' | '/test-tree/branch[@name=\'Branch\']' || ['test-tree','branch'] 'xpath contains list attributes with /' | '/test-tree/branch[@name=\'/Branch\']/categories[@id=\'/broken\']' || ['test-tree','branch','categories'] } - } diff --git a/cps-service/src/test/groovy/org/onap/cps/yang/YangTextSchemaSourceSetBuilderSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/yang/YangTextSchemaSourceSetBuilderSpec.groovy index 236221aca7..6d570d6432 100644 --- a/cps-service/src/test/groovy/org/onap/cps/yang/YangTextSchemaSourceSetBuilderSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/yang/YangTextSchemaSourceSetBuilderSpec.groovy @@ -3,6 +3,7 @@ * Copyright (C) 2020-2021 Pantheon.tech * Modifications Copyright (C) 2020-2022 Nordix Foundation * Modifications Copyright (C) 2021 Bell Canada. + * Modifications Copyright (C) 2022 TechMahindra Ltd. * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,11 +21,12 @@ * ============LICENSE_END========================================================= */ -package org.onap.cps.yang +package org.onap.cps.utils.yang import org.onap.cps.TestUtils import org.onap.cps.spi.exceptions.ModelValidationException +import org.onap.cps.yang.YangTextSchemaSourceSetBuilder import org.opendaylight.yangtools.yang.common.Revision import spock.lang.Specification diff --git a/cps-service/src/test/resources/bookstore.json b/cps-service/src/test/resources/bookstore.json index d1b8d6882d..459908bd63 100644 --- a/cps-service/src/test/resources/bookstore.json +++ b/cps-service/src/test/resources/bookstore.json @@ -1,19 +1,19 @@ { "test:bookstore":{ - "bookstore-name": "Chapters", + "bookstore-name": "Chapters/Easons", "categories": [ { - "code": "01", + "code": "01/1", "name": "SciFi", "books": [ { "authors": [ "Iain M. Banks" ], - "lang": "en", + "lang": "en/it", "price": "895", "pub_year": "1994", - "title": "Feersum Endjinn" + "title": "Feersum Endjinn/Endjinn Feersum" }, { "authors": [ diff --git a/cps-service/src/test/resources/bookstore.xml b/cps-service/src/test/resources/bookstore.xml new file mode 100644 index 0000000000..dd45e16896 --- /dev/null +++ b/cps-service/src/test/resources/bookstore.xml @@ -0,0 +1,19 @@ +<?xml version='1.0' encoding='UTF-8'?> +<stores xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"> +<bookstore xmlns="org:onap:ccsdk:sample"> + <bookstore-name>Chapters</bookstore-name> + <categories> + <code>1</code> + <name>SciFi</name> + <books> + <title>2001: A Space Odyssey</title> + <lang>en</lang> + <authors> + Iain M. Banks + </authors> + <pub_year>1994</pub_year> + <price>895</price> + </books> + </categories> +</bookstore> +</stores>
\ No newline at end of file diff --git a/cps-service/src/test/resources/bookstore_xpath.xml b/cps-service/src/test/resources/bookstore_xpath.xml new file mode 100644 index 0000000000..e206901d6d --- /dev/null +++ b/cps-service/src/test/resources/bookstore_xpath.xml @@ -0,0 +1,17 @@ +<?xml version='1.0' encoding='UTF-8'?> +<bookstore xmlns="org:onap:ccsdk:sample"> + <bookstore-name>Chapters</bookstore-name> + <categories> + <code>1</code> + <name>SciFi</name> + <books> + <title>2001: A Space Odyssey</title> + <lang>en</lang> + <authors> + Iain M. Banks + </authors> + <pub_year>1994</pub_year> + <price>895</price> + </books> + </categories> +</bookstore>
\ No newline at end of file diff --git a/cps-service/src/test/resources/test-tree.xml b/cps-service/src/test/resources/test-tree.xml new file mode 100644 index 0000000000..3daa814cf6 --- /dev/null +++ b/cps-service/src/test/resources/test-tree.xml @@ -0,0 +1,27 @@ +<?xml version='1.0' encoding='UTF-8'?> +<data xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"> + <test-tree xmlns="org:onap:cps:test:test-tree"> + <branch> + <name>Left</name> + <nest> + <name>Small</name> + <birds>Sparrow</birds> + <birds>Robin</birds> + <birds>Finch</birds> + </nest> + </branch> + <branch> + <name>Right</name> + <nest> + <name>Big</name> + <birds>Owl</birds> + <birds>Raven</birds> + <birds>Crow</birds> + </nest> + </branch> + <fruit> + <name>Apple</name> + <color>Green</color> + </fruit> + </test-tree> +</data> diff --git a/csit/data/test-tree.json b/csit/data/test-tree.json index 89d6784275..8f4b522799 100644 --- a/csit/data/test-tree.json +++ b/csit/data/test-tree.json @@ -2,11 +2,11 @@ "test-tree": { "branch": [ { - "name": "Left", + "name": "LEFT/left", "nest": { - "name": "Small", + "name": "SMALL/small", "birds": [ - "Sparrow", + "SPARROW/sparrow", "Robin", "Finch" ] diff --git a/csit/prepare-csit.sh b/csit/prepare-csit.sh index 67412f3cf3..78f0fbde22 100755 --- a/csit/prepare-csit.sh +++ b/csit/prepare-csit.sh @@ -28,6 +28,21 @@ fi TESTPLANDIR=${WORKSPACE}/${TESTPLAN} +# Version should match those used to setup robot-framework in other jobs/stages +# Use pyenv for selecting the python version +if [[ -d "/opt/pyenv" ]]; then + echo "Setup pyenv:" + export PYENV_ROOT="/opt/pyenv" + export PATH="$PYENV_ROOT/bin:$PATH" + pyenv versions + if command -v pyenv 1>/dev/null 2>&1; then + eval "$(pyenv init - --no-rehash)" + # Choose the latest numeric Python version from installed list + version=$(pyenv versions --bare | sed '/^[^0-9]/d' | sort -V | tail -n 1) + pyenv local "${version}" + fi +fi + # Assume that if ROBOT3_VENV is set and virtualenv with system site packages can be activated, # ci-management/jjb/integration/include-raw-integration-install-robotframework.sh has already # been executed @@ -42,13 +57,17 @@ else rm -f ${WORKSPACE}/env.properties cd /tmp git clone "https://gerrit.onap.org/r/ci-management" - source /tmp/ci-management/jjb/integration/include-raw-integration-install-robotframework-py3.sh +# source /tmp/ci-management/jjb/integration/include-raw-integration-install-robotframework-py3.sh + source ${WORKSPACE}/install-robotframework.sh fi # install eteutils mkdir -p ${ROBOT3_VENV}/src/onap rm -rf ${ROBOT3_VENV}/src/onap/testsuite -python3 -m pip install --upgrade --extra-index-url="https://nexus3.onap.org/repository/PyPi.staging/simple" 'robotframework-onap==0.5.1.*' --pre -pip freeze +python3 -m pip install --upgrade --extra-index-url="https://nexus3.onap.org/repository/PyPi.staging/simple" 'robotframework-onap==11.0.0.dev17' --pre +echo "Versioning information:" +python3 --version +pip freeze +python3 -m robot.run --version || :
\ No newline at end of file diff --git a/csit/pylibs.txt b/csit/pylibs.txt index 4952616540..9fee634156 100644 --- a/csit/pylibs.txt +++ b/csit/pylibs.txt @@ -6,7 +6,7 @@ pyhocon requests robotframework-httplibrary robotframework-requests==0.9.3 -robotframework-selenium2library +robotframework-selenium2library==3.0.0 robotframework-extendedselenium2library robotframework-sshlibrary scapy diff --git a/csit/run-csit.sh b/csit/run-csit.sh index 6703160a37..9a344c1ffd 100755 --- a/csit/run-csit.sh +++ b/csit/run-csit.sh @@ -20,14 +20,29 @@ # Branched from ccsdk/distribution to this repository Feb 23, 2021 +echo "---> run-csit.sh" + WORKDIR=$(mktemp -d --suffix=-robot-workdir) +# Version should match those used to setup robot-framework in other jobs/stages +# Use pyenv for selecting the python version +if [[ -d "/opt/pyenv" ]]; then + echo "Setup pyenv:" + export PYENV_ROOT="/opt/pyenv" + export PATH="$PYENV_ROOT/bin:$PATH" + pyenv versions + if command -v pyenv 1>/dev/null 2>&1; then + eval "$(pyenv init - --no-rehash)" + # Choose the latest numeric Python version from installed list + version=$(pyenv versions --bare | sed '/^[^0-9]/d' | sort -V | tail -n 1) + pyenv local "${version}" + fi +fi + # # functions # -echo "---> run-csit.sh" - # wrapper for sourcing a file function source_safely() { [ -z "$1" ] && return 1 @@ -192,6 +207,12 @@ SUITES=$( xargs -a testplan.txt ) echo ROBOT_VARIABLES="${ROBOT_VARIABLES}" echo "Starting Robot test suites ${SUITES} ..." relax_set + +echo "Versioning information:" +python3 --version +pip freeze +python3 -m robot.run --version || : + python3 -m robot.run -N ${TESTPLAN} -v WORKSPACE:/tmp ${ROBOT_VARIABLES} ${TESTOPTIONS} ${SUITES} RESULT=$? load_set diff --git a/csit/tests/cps-data/cps-data.robot b/csit/tests/cps-data/cps-data.robot index 2da2b73414..096bd07b79 100644 --- a/csit/tests/cps-data/cps-data.robot +++ b/csit/tests/cps-data/cps-data.robot @@ -44,10 +44,10 @@ Create Data Node Get Data Node by XPath ${uri}= Set Variable ${basePath}/v1/dataspaces/${dataspaceName}/anchors/${anchorName}/node - ${params}= Create Dictionary xpath=/test-tree/branch[@name='Left']/nest + ${params}= Create Dictionary xpath=/test-tree/branch[@name='LEFT/left']/nest ${headers}= Create Dictionary Authorization=${auth} ${response}= Get On Session CPS_URL ${uri} params=${params} headers=${headers} expected_status=200 ${responseJson}= Set Variable ${response.json()['tree:nest']} - Should Be Equal As Strings ${responseJson['name']} Small + Should Be Equal As Strings ${responseJson['name']} SMALL/small diff --git a/docs/api/swagger/cps/openapi.yaml b/docs/api/swagger/cps/openapi.yaml index 09ccbe14a0..ec7d29524e 100644 --- a/docs/api/swagger/cps/openapi.yaml +++ b/docs/api/swagger/cps/openapi.yaml @@ -29,6 +29,7 @@ paths: summary: Create a dataspace description: Create a new dataspace operationId: createDataspace + deprecated: true parameters: - name: dataspace-name in: query @@ -95,6 +96,75 @@ paths: status: 500 message: Internal Server Error details: Internal Server Error occurred + /v2/dataspaces: + post: + tags: + - cps-admin + summary: Create a dataspace + description: Create a new dataspace + operationId: createDataspaceV2 + parameters: + - name: dataspace-name + in: query + description: dataspace-name + required: true + schema: + type: string + example: my-dataspace + responses: + "201": + description: Created + "400": + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + example: + status: 400 + message: Bad Request + details: The provided request is not valid + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + example: + status: 401 + message: Unauthorized request + details: This request is unauthorized + "403": + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + example: + status: 403 + message: Request Forbidden + details: This request is forbidden + "409": + description: Conflict + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + example: + status: 409 + message: Conflicting request + details: The request cannot be processed as the resource is in use. + "500": + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + example: + status: 500 + message: Internal Server Error + details: Internal Server Error occurred + /{apiVersion}/dataspaces: delete: tags: - cps-admin @@ -102,6 +172,7 @@ paths: description: Delete a dataspace operationId: deleteDataspace parameters: + - $ref: '#/components/parameters/apiVersionInPath' - name: dataspace-name in: query description: dataspace-name @@ -163,13 +234,15 @@ paths: status: 500 message: Internal Server Error details: Internal Server Error occurred - /v1/admin/dataspaces: + /{apiVersion}/admin/dataspaces: get: tags: - cps-admin summary: Get dataspaces description: "Read all dataspaces" operationId: getAllDataspaces + parameters: + - $ref: '#/components/parameters/apiVersionInPath' responses: "200": description: OK @@ -219,7 +292,7 @@ paths: status: 500 message: Internal Server Error details: Internal Server Error occurred - /v1/admin/dataspaces/{dataspace-name}: + /{apiVersion}/admin/dataspaces/{dataspace-name}: get: tags: - cps-admin @@ -227,6 +300,7 @@ paths: description: Read an dataspace given a dataspace name operationId: getDataspace parameters: + - $ref: '#/components/parameters/apiVersionInPath' - name: dataspace-name in: path description: dataspace-name @@ -281,7 +355,7 @@ paths: status: 500 message: Internal Server Error details: Internal Server Error occurred - /v1/dataspaces/{dataspace-name}/anchors: + /{apiVersion}/dataspaces/{dataspace-name}/anchors: get: tags: - cps-admin @@ -289,6 +363,7 @@ paths: description: "Read all anchors, given a dataspace" operationId: getAnchors parameters: + - $ref: '#/components/parameters/apiVersionInPath' - name: dataspace-name in: path description: dataspace-name @@ -345,10 +420,12 @@ paths: status: 500 message: Internal Server Error details: Internal Server Error occurred + /v1/dataspaces/{dataspace-name}/anchors: post: tags: - cps-admin summary: Create an anchor + deprecated: true description: Create a new anchor in the given dataspace operationId: createAnchor parameters: @@ -431,7 +508,89 @@ paths: status: 500 message: Internal Server Error details: Internal Server Error occurred - /v1/dataspaces/{dataspace-name}/anchors/{anchor-name}: + /v2/dataspaces/{dataspace-name}/anchors: + post: + tags: + - cps-admin + summary: Create an anchor + description: Create a new anchor in the given dataspace + operationId: createAnchorV2 + parameters: + - name: dataspace-name + in: path + description: dataspace-name + required: true + schema: + type: string + example: my-dataspace + - name: schema-set-name + in: query + description: schema-set-name + required: true + schema: + type: string + example: my-schema-set + - name: anchor-name + in: query + description: anchor-name + required: true + schema: + type: string + example: my-anchor + responses: + "201": + description: Created + "400": + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + example: + status: 400 + message: Bad Request + details: The provided request is not valid + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + example: + status: 401 + message: Unauthorized request + details: This request is unauthorized + "403": + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + example: + status: 403 + message: Request Forbidden + details: This request is forbidden + "409": + description: Conflict + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + example: + status: 409 + message: Conflicting request + details: The request cannot be processed as the resource is in use. + "500": + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + example: + status: 500 + message: Internal Server Error + details: Internal Server Error occurred + /{apiVersion}/dataspaces/{dataspace-name}/anchors/{anchor-name}: get: tags: - cps-admin @@ -439,6 +598,7 @@ paths: description: Read an anchor given an anchor name and a dataspace operationId: getAnchor parameters: + - $ref: '#/components/parameters/apiVersionInPath' - name: dataspace-name in: path description: dataspace-name @@ -507,6 +667,7 @@ paths: description: Delete an anchor given an anchor name and a dataspace operationId: deleteAnchor parameters: + - $ref: '#/components/parameters/apiVersionInPath' - name: dataspace-name in: path description: dataspace-name @@ -651,6 +812,88 @@ paths: status: 500 message: Internal Server Error details: Internal Server Error occurred + /v2/dataspaces/{dataspace-name}/schema-sets: + post: + tags: + - cps-admin + summary: Create a schema set + description: Create a new schema set in the given dataspace + operationId: createSchemaSetV2 + parameters: + - name: dataspace-name + in: path + description: dataspace-name + required: true + schema: + type: string + example: my-dataspace + - name: schema-set-name + in: query + description: schema-set-name + required: true + schema: + type: string + example: my-schema-set + requestBody: + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/MultipartFile' + required: true + responses: + "201": + description: Created + "400": + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + example: + status: 400 + message: Bad Request + details: The provided request is not valid + "401": + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + example: + status: 401 + message: Unauthorized request + details: This request is unauthorized + "403": + description: Forbidden + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + example: + status: 403 + message: Request Forbidden + details: This request is forbidden + "409": + description: Conflict + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + example: + status: 409 + message: Conflicting request + details: The request cannot be processed as the resource is in use. + "500": + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorMessage' + example: + status: 500 + message: Internal Server Error + details: Internal Server Error occurred + /{apiVersion}/dataspaces/{dataspace-name}/schema-sets: get: tags: - cps-admin @@ -658,13 +901,14 @@ paths: description: "Read schema sets for a given dataspace" operationId: getSchemaSets parameters: - - name: dataspace-name - in: path - description: dataspace-name - required: true - schema: - type: string - example: my-dataspace + - $ref: '#/components/parameters/apiVersionInPath' + - name: dataspace-name + in: path + description: dataspace-name + required: true + schema: + type: string + example: my-dataspace responses: "200": description: OK @@ -714,7 +958,7 @@ paths: status: 500 message: Internal Server Error details: Internal Server Error occurred - /v1/dataspaces/{dataspace-name}/schema-sets/{schema-set-name}: + /{apiVersion}/dataspaces/{dataspace-name}/schema-sets/{schema-set-name}: get: tags: - cps-admin @@ -722,6 +966,7 @@ paths: description: Read a schema set given a schema set name and a dataspace operationId: getSchemaSet parameters: + - $ref: '#/components/parameters/apiVersionInPath' - name: dataspace-name in: path description: dataspace-name @@ -790,6 +1035,7 @@ paths: description: Delete a schema set given a schema set name and a dataspace operationId: deleteSchemaSet parameters: + - $ref: '#/components/parameters/apiVersionInPath' - name: dataspace-name in: path description: dataspace-name @@ -858,7 +1104,7 @@ paths: status: 500 message: Internal Server Error details: Internal Server Error occurred - /v1/dataspaces/{dataspace-name}/anchors/{anchor-name}/node: + /{apiVersion}/dataspaces/{dataspace-name}/anchors/{anchor-name}/node: get: tags: - cps-data @@ -867,6 +1113,7 @@ paths: anchor and dataspace operationId: getNodeByDataspaceAndAnchor parameters: + - $ref: '#/components/parameters/apiVersionInPath' - name: dataspace-name in: path description: dataspace-name @@ -952,7 +1199,7 @@ paths: message: Internal Server Error details: Internal Server Error occurred x-codegen-request-body-name: xpath - /v1/dataspaces/{dataspace-name}/anchors/{anchor-name}/nodes: + /{apiVersion}/dataspaces/{dataspace-name}/anchors/{anchor-name}/nodes: put: tags: - cps-data @@ -961,6 +1208,7 @@ paths: \ and a parent node xpath" operationId: replaceNode parameters: + - $ref: '#/components/parameters/apiVersionInPath' - name: dataspace-name in: path description: dataspace-name @@ -1060,6 +1308,7 @@ paths: description: Create a node for a given anchor and dataspace operationId: createNode parameters: + - $ref: '#/components/parameters/apiVersionInPath' - name: dataspace-name in: path description: dataspace-name @@ -1168,6 +1417,7 @@ paths: xpath. operationId: deleteDataNode parameters: + - $ref: '#/components/parameters/apiVersionInPath' - name: dataspace-name in: path description: dataspace-name @@ -1253,6 +1503,7 @@ paths: a parent node xpath operationId: updateNodeLeaves parameters: + - $ref: '#/components/parameters/apiVersionInPath' - name: dataspace-name in: path description: dataspace-name @@ -1345,7 +1596,7 @@ paths: status: 500 message: Internal Server Error details: Internal Server Error occurred - /v1/dataspaces/{dataspace-name}/anchors/{anchor-name}/list-nodes: + /{apiVersion}/dataspaces/{dataspace-name}/anchors/{anchor-name}/list-nodes: put: tags: - cps-data @@ -1353,6 +1604,7 @@ paths: description: "Replace list content under a given parent, anchor and dataspace" operationId: replaceListContent parameters: + - $ref: '#/components/parameters/apiVersionInPath' - name: dataspace-name in: path description: dataspace-name @@ -1451,6 +1703,7 @@ paths: description: Add list element(s) to a list for a given anchor and dataspace operationId: addListElements parameters: + - $ref: '#/components/parameters/apiVersionInPath' - name: dataspace-name in: path description: dataspace-name @@ -1547,6 +1800,7 @@ paths: description: Delete one or all list element(s) for a given anchor and dataspace operationId: deleteListOrListElement parameters: + - $ref: '#/components/parameters/apiVersionInPath' - name: dataspace-name in: path description: dataspace-name @@ -1624,7 +1878,7 @@ paths: message: Internal Server Error details: Internal Server Error occurred deprecated: true - /v1/dataspaces/{dataspace-name}/anchors/{anchor-name}/nodes/query: + /{apiVersion}/dataspaces/{dataspace-name}/anchors/{anchor-name}/nodes/query: get: tags: - cps-query @@ -1632,6 +1886,7 @@ paths: description: Query data nodes for the given dataspace and anchor using CPS path operationId: getNodesByDataspaceAndAnchorAndCpsPath parameters: + - $ref: '#/components/parameters/apiVersionInPath' - name: dataspace-name in: path description: dataspace-name @@ -1718,6 +1973,16 @@ paths: details: Internal Server Error occurred x-codegen-request-body-name: xpath components: + parameters: + apiVersionInPath: + name: apiVersion + in: path + description: apiVersion + required: true + schema: + type: string + enum: [v1, v2] + default: v2 securitySchemes: basicAuth: type: http diff --git a/docs/release-notes.rst b/docs/release-notes.rst index 76e167f060..30715aff35 100755 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -39,16 +39,24 @@ Features -------- 3.2.1 - `CPS-1236 <https://jira.onap.org/browse/CPS-1236>`_ DMI audit support for NCMP: Filter on any properties of CM Handles + - `CPS-1185 <https://jira.onap.org/browse/CPS-1185>`_ Get all dataspaces. + - `CPS-1186 <https://jira.onap.org/browse/CPS-1186>`_ Get single dataspace. - `CPS-1187 <https://jira.onap.org/browse/CPS-1187>`_ Added API to get all schema sets for a given dataspace. -3.2.0 - - `CPS-1185 <https://jira.onap.org/browse/CPS-1185>`_ Get all dataspaces - - `CPS-1187 <https://jira.onap.org/browse/CPS-1187>`_ Get single dataspace + - `CPS-1171 <https://jira.onap.org/browse/CPS-1171>`_ Optimized retrieval of data nodes with many descendants. + - `CPS-341 <https://jira.onap.org/browse/CPS-341>`_ Added support for multiple data tree instances under 1 anchor. + - `CPS-1257 <https://jira.onap.org/browse/CPS-1257>`_ Added support for application/xml Content-Type (write only). + - `CPS-1421 <https://jira.onap.org/browse/CPS-1421>`_ Optimized query for large number of hits with descendants. Bug Fixes --------- +3.2.1 + - `CPS-1352 <https://jira.onap.org/browse/CPS-1352>`_ Handle YangChoiceNode in right format. + - `CPS-1350 <https://jira.onap.org/browse/CPS-1350>`_ Add Basic Auth to CPS/NCMP OpenAPI Definitions. + - `CPS-1433 <https://jira.onap.org/browse/CPS-1433>`_ Fix to allow posting data with '/' key fields. + - `CPS-1409 <https://jira.onap.org/browse/CPS-1409>`_ Fix Delete uses case with '/' in path. + 3.2.0 - - `CPS-1312 <https://jira.onap.org/browse/CPS-1312>`_ CPS(/NCMP) does not have version control - - `CPS-1350 <https://jira.onap.org/browse/CPS-1350>`_ [CPS/NCMP] Add Basic Auth to CPS/NCMP OpenAPI Definitions + - `CPS-1312 <https://jira.onap.org/browse/CPS-1312>`_ CPS(/NCMP) does not have version control. Known Limitations, Issues and Workarounds ----------------------------------------- |