summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-xcps-application/pom.xml5
-rw-r--r--cps-application/src/main/resources/application.yml2
-rw-r--r--cps-ncmp-events/src/main/resources/schemas/avc-subscription-event-v1.json101
-rw-r--r--cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandlerQueryServiceImpl.java12
-rw-r--r--cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/event/avc/SubscriptionEventConsumer.java53
-rw-r--r--cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/InventoryPersistence.java14
-rw-r--r--cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/InventoryPersistenceImpl.java19
-rw-r--r--cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandlerQueryServiceSpec.groovy30
-rw-r--r--cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/event/avc/SubscriptionEventConsumerSpec.groovy52
-rw-r--r--cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/InventoryPersistenceImplSpec.groovy33
-rw-r--r--cps-ncmp-service/src/test/resources/application.yml5
-rw-r--r--cps-ncmp-service/src/test/resources/avcSubscriptionCreationEvent.json23
-rw-r--r--cps-path-parser/pom.xml25
-rw-r--r--cps-path-parser/src/main/antlr4/org/onap/cps/cpspath/parser/antlr4/CpsPath.g44
-rw-r--r--cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathBuilder.java16
-rw-r--r--cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathQuery.java3
-rw-r--r--cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathUtil.java36
-rw-r--r--cps-path-parser/src/test/groovy/org/onap/cps/cpspath/parser/CpsPathUtilSpec.groovy90
-rw-r--r--cps-path-parser/src/test/groovy/org/onap/cps/cpspath/parser/performance/CpsPathUtilPerfTest.groovy43
-rw-r--r--cps-rest/docs/openapi/components.yml31
-rw-r--r--cps-rest/docs/openapi/cpsAdmin.yml82
-rw-r--r--cps-rest/docs/openapi/cpsAdminV1Deprecated.yml98
-rw-r--r--cps-rest/docs/openapi/cpsAdminV2.yml95
-rw-r--r--cps-rest/docs/openapi/cpsData.yml44
-rw-r--r--cps-rest/docs/openapi/cpsDataV1Deprecated.yml42
-rw-r--r--cps-rest/docs/openapi/cpsQuery.yml2
-rw-r--r--cps-rest/docs/openapi/openapi.yml35
-rwxr-xr-xcps-rest/src/main/java/org/onap/cps/rest/controller/AdminRestController.java71
-rwxr-xr-xcps-rest/src/main/java/org/onap/cps/rest/controller/DataRestController.java37
-rw-r--r--cps-rest/src/main/java/org/onap/cps/rest/controller/QueryRestController.java5
-rwxr-xr-xcps-rest/src/test/groovy/org/onap/cps/rest/controller/AdminRestControllerSpec.groovy70
-rwxr-xr-xcps-rest/src/test/groovy/org/onap/cps/rest/controller/DataRestControllerSpec.groovy69
-rw-r--r--cps-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy17
-rw-r--r--cps-ri/pom.xml2
-rw-r--r--cps-ri/src/main/java/org/onap/cps/spi/entities/FragmentEntityArranger.java12
-rw-r--r--cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java179
-rw-r--r--cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentQueryBuilder.java139
-rwxr-xr-xcps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepository.java8
-rw-r--r--cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryCpsPathQueryImpl.java77
-rw-r--r--cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceQuery.java3
-rw-r--r--cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceRepository.java8
-rw-r--r--cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceRepositoryImpl.java46
-rw-r--r--cps-ri/src/main/java/org/onap/cps/spi/repository/TempTableCreator.java97
-rwxr-xr-xcps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceIntegrationSpec.groovy128
-rw-r--r--cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy52
-rw-r--r--cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsModulePersistenceServiceIntegrationSpec.groovy7
-rw-r--r--cps-ri/src/test/groovy/org/onap/cps/spi/performance/CpsDataPersistenceServicePerfTest.groovy (renamed from cps-ri/src/test/groovy/org/onap/cps/spi/performance/CpsToDataNodePerfTest.groovy)83
-rw-r--r--cps-ri/src/test/groovy/org/onap/cps/spi/performance/CpsModuleReferenceRepositoryPerfTest.groovy103
-rw-r--r--cps-service/pom.xml9
-rw-r--r--cps-service/src/main/java/org/onap/cps/api/CpsDataService.java38
-rwxr-xr-xcps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java91
-rw-r--r--cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java23
-rw-r--r--cps-service/src/main/java/org/onap/cps/spi/model/DataNodeBuilder.java39
-rw-r--r--cps-service/src/main/java/org/onap/cps/spi/model/ModuleReference.java1
-rw-r--r--cps-service/src/main/java/org/onap/cps/utils/ContentType.java26
-rw-r--r--cps-service/src/main/java/org/onap/cps/utils/XmlFileUtils.java185
-rw-r--r--cps-service/src/main/java/org/onap/cps/utils/YangUtils.java246
-rw-r--r--cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy60
-rwxr-xr-xcps-service/src/test/groovy/org/onap/cps/api/impl/E2ENetworkSliceSpec.groovy9
-rw-r--r--cps-service/src/test/groovy/org/onap/cps/spi/model/DataNodeBuilderSpec.groovy83
-rw-r--r--cps-service/src/test/groovy/org/onap/cps/utils/JsonObjectMapperSpec.groovy2
-rw-r--r--cps-service/src/test/groovy/org/onap/cps/utils/JsonParserStreamSpec.groovy27
-rw-r--r--cps-service/src/test/groovy/org/onap/cps/utils/XmlFileUtilsSpec.groovy61
-rw-r--r--cps-service/src/test/groovy/org/onap/cps/utils/YangUtilsSpec.groovy73
-rw-r--r--cps-service/src/test/groovy/org/onap/cps/yang/YangTextSchemaSourceSetBuilderSpec.groovy4
-rw-r--r--cps-service/src/test/resources/bookstore.json8
-rw-r--r--cps-service/src/test/resources/bookstore.xml19
-rw-r--r--cps-service/src/test/resources/bookstore_xpath.xml17
-rw-r--r--cps-service/src/test/resources/test-tree.xml27
-rw-r--r--csit/data/test-tree.json6
-rwxr-xr-xcsit/prepare-csit.sh25
-rw-r--r--csit/pylibs.txt2
-rwxr-xr-xcsit/run-csit.sh25
-rw-r--r--csit/tests/cps-data/cps-data.robot4
-rw-r--r--docs/api/swagger/cps/openapi.yaml297
-rwxr-xr-xdocs/release-notes.rst18
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
-----------------------------------------