Merge "Updating CmHandleStates using batch operation"
authorToine Siebelink <toine.siebelink@est.tech>
Thu, 12 Jan 2023 16:31:07 +0000 (16:31 +0000)
committerGerrit Code Review <gerrit@onap.org>
Thu, 12 Jan 2023 16:31:07 +0000 (16:31 +0000)
76 files changed:
cps-application/pom.xml
cps-application/src/main/resources/application.yml
cps-ncmp-events/src/main/resources/schemas/avc-subscription-event-v1.json [new file with mode: 0644]
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandlerQueryServiceImpl.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/event/avc/SubscriptionEventConsumer.java [new file with mode: 0644]
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/InventoryPersistence.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/inventory/InventoryPersistenceImpl.java
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandlerQueryServiceSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/event/avc/SubscriptionEventConsumerSpec.groovy [new file with mode: 0644]
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/InventoryPersistenceImplSpec.groovy
cps-ncmp-service/src/test/resources/application.yml
cps-ncmp-service/src/test/resources/avcSubscriptionCreationEvent.json [new file with mode: 0644]
cps-path-parser/pom.xml
cps-path-parser/src/main/antlr4/org/onap/cps/cpspath/parser/antlr4/CpsPath.g4
cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathBuilder.java
cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathQuery.java
cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathUtil.java
cps-path-parser/src/test/groovy/org/onap/cps/cpspath/parser/CpsPathUtilSpec.groovy [new file with mode: 0644]
cps-path-parser/src/test/groovy/org/onap/cps/cpspath/parser/performance/CpsPathUtilPerfTest.groovy [new file with mode: 0644]
cps-rest/docs/openapi/components.yml
cps-rest/docs/openapi/cpsAdmin.yml
cps-rest/docs/openapi/cpsAdminV1Deprecated.yml [new file with mode: 0644]
cps-rest/docs/openapi/cpsAdminV2.yml [new file with mode: 0644]
cps-rest/docs/openapi/cpsData.yml
cps-rest/docs/openapi/cpsDataV1Deprecated.yml [new file with mode: 0644]
cps-rest/docs/openapi/cpsQuery.yml
cps-rest/docs/openapi/openapi.yml
cps-rest/src/main/java/org/onap/cps/rest/controller/AdminRestController.java
cps-rest/src/main/java/org/onap/cps/rest/controller/DataRestController.java
cps-rest/src/main/java/org/onap/cps/rest/controller/QueryRestController.java
cps-rest/src/test/groovy/org/onap/cps/rest/controller/AdminRestControllerSpec.groovy
cps-rest/src/test/groovy/org/onap/cps/rest/controller/DataRestControllerSpec.groovy
cps-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy
cps-ri/pom.xml
cps-ri/src/main/java/org/onap/cps/spi/entities/FragmentEntityArranger.java
cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java
cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentQueryBuilder.java [new file with mode: 0644]
cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepository.java
cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepositoryCpsPathQueryImpl.java
cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceQuery.java
cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceRepository.java
cps-ri/src/main/java/org/onap/cps/spi/repository/ModuleReferenceRepositoryImpl.java
cps-ri/src/main/java/org/onap/cps/spi/repository/TempTableCreator.java [new file with mode: 0644]
cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceIntegrationSpec.groovy
cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy
cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsModulePersistenceServiceIntegrationSpec.groovy
cps-ri/src/test/groovy/org/onap/cps/spi/performance/CpsDataPersistenceServicePerfTest.groovy [moved from cps-ri/src/test/groovy/org/onap/cps/spi/performance/CpsToDataNodePerfTest.groovy with 65% similarity]
cps-ri/src/test/groovy/org/onap/cps/spi/performance/CpsModuleReferenceRepositoryPerfTest.groovy [new file with mode: 0644]
cps-service/pom.xml
cps-service/src/main/java/org/onap/cps/api/CpsDataService.java
cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java
cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java
cps-service/src/main/java/org/onap/cps/spi/model/DataNodeBuilder.java
cps-service/src/main/java/org/onap/cps/spi/model/ModuleReference.java
cps-service/src/main/java/org/onap/cps/utils/ContentType.java [new file with mode: 0644]
cps-service/src/main/java/org/onap/cps/utils/XmlFileUtils.java [new file with mode: 0644]
cps-service/src/main/java/org/onap/cps/utils/YangUtils.java
cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy
cps-service/src/test/groovy/org/onap/cps/api/impl/E2ENetworkSliceSpec.groovy
cps-service/src/test/groovy/org/onap/cps/spi/model/DataNodeBuilderSpec.groovy
cps-service/src/test/groovy/org/onap/cps/utils/JsonObjectMapperSpec.groovy
cps-service/src/test/groovy/org/onap/cps/utils/JsonParserStreamSpec.groovy
cps-service/src/test/groovy/org/onap/cps/utils/XmlFileUtilsSpec.groovy [new file with mode: 0644]
cps-service/src/test/groovy/org/onap/cps/utils/YangUtilsSpec.groovy
cps-service/src/test/groovy/org/onap/cps/yang/YangTextSchemaSourceSetBuilderSpec.groovy
cps-service/src/test/resources/bookstore.json
cps-service/src/test/resources/bookstore.xml [new file with mode: 0644]
cps-service/src/test/resources/bookstore_xpath.xml [new file with mode: 0644]
cps-service/src/test/resources/test-tree.xml [new file with mode: 0644]
csit/data/test-tree.json
csit/prepare-csit.sh
csit/pylibs.txt
csit/run-csit.sh
csit/tests/cps-data/cps-data.robot
docs/api/swagger/cps/openapi.yaml
docs/release-notes.rst

index 9990cdd..c689c75 100755 (executable)
@@ -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.
             <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>
index e3ffd04..b5b10b0 100644 (file)
@@ -98,6 +98,8 @@ app:
     ncmp:\r
         async-m2m:\r
             topic: ${NCMP_ASYNC_M2M_TOPIC:ncmp-async-m2m}\r
+        avc:\r
+            subscription-topic: ${NCMP_CM_AVC_SUBSCRIPTION:cm-avc-subscription}\r
     lcm:\r
         events:\r
             topic: ${LCM_EVENTS_TOPIC:ncmp-events}\r
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 (file)
index 0000000..5ab446c
--- /dev/null
@@ -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
index a8fc6d7..b67ae0c 100644 (file)
@@ -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 (file)
index 0000000..1f03246
--- /dev/null
@@ -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");
+        }
+    }
+}
index b29825e..6d006d9 100644 (file)
@@ -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.
index adba198..5b0b5ea 100644 (file)
@@ -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
index 201f6af..05856d0 100644 (file)
@@ -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 (file)
index 0000000..20d60e3
--- /dev/null
@@ -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' ]
+    }
+}
index c713aad..355487f 100644 (file)
 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'() {
index 8d8bfaf..4009e56 100644 (file)
 #  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 (file)
index 0000000..1d84c3a
--- /dev/null
@@ -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
index d9c1508..d0da105 100644 (file)
         </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>
index 40ad410..db09b3c 100644 (file)
@@ -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 ;
 
index 21f5173..3a9d70e 100644 (file)
@@ -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());
@@ -60,6 +64,11 @@ public class CpsPathBuilder extends CpsPathBaseListener {
         cpsPathQuery.setXpathPrefix(normalizedXpathBuilder.toString());
     }
 
+    @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);
         }
     }
 
index 53490f3..c9df8df 100644 (file)
@@ -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;
index 97d7d1d..60f0e2e 100644 (file)
@@ -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 (file)
index 0000000..d62f337
--- /dev/null
@@ -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 (file)
index 0000000..2ba20c1
--- /dev/null
@@ -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
+    }
+
+}
index fb0947e..e700da6 100644 (file)
@@ -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:
index 595f6d7..f60a9be 100644 (file)
 # ============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 (file)
index 0000000..56f7f1b
--- /dev/null
@@ -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 (file)
index 0000000..14e2cfe
--- /dev/null
@@ -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'
index 265ee23..0dc3887 100644 (file)
@@ -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 (file)
index 0000000..194ca3e
--- /dev/null
@@ -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'
index dc0402d..45fc70c 100644 (file)
@@ -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'
index e02d6a6..0918b56 100644 (file)
@@ -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:
index 285a15c..b8ba089 100755 (executable)
@@ -68,6 +68,18 @@ public class AdminRestController implements CpsAdminApi {
         return new ResponseEntity<>(dataspaceName, HttpStatus.CREATED);
     }
 
+    /**
+     * 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.
      *
@@ -75,7 +87,7 @@ public class AdminRestController implements CpsAdminApi {
      * @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);
     }
@@ -95,15 +107,32 @@ public class AdminRestController implements CpsAdminApi {
         return new ResponseEntity<>(schemaSetName, HttpStatus.CREATED);
     }
 
+    /**
+     * 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);
     }
@@ -151,15 +183,32 @@ public class AdminRestController implements CpsAdminApi {
         return new ResponseEntity<>(anchorName, HttpStatus.CREATED);
     }
 
+    /**
+     * 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);
index fdce9be..30bed12 100755 (executable)
@@ -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));
index 577ad9c..3e162ae 100644 (file)
@@ -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 =
index 7120ce4..f81efd6 100755 (executable)
@@ -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.'() {
index 53da3e6..94f62f8 100755 (executable)
@@ -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,9 +71,19 @@ 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
index ece3507..0821b6b 100644 (file)
@@ -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.
index b6fe284..6cca8b4 100644 (file)
                         <artifactId>maven-surefire-plugin</artifactId>\r
                         <configuration>\r
                             <excludes>\r
-                                <exclude>org.onap.cps.spi.performance.CpsToDataNodePerfTest</exclude>\r
+                                <exclude>%regex[.*PerfTest.*]</exclude>\r
                             </excludes>\r
                         </configuration>\r
                     </plugin>\r
index 27891c5..6b1162d 100644 (file)
@@ -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;
     }
 
 }
index c725b42..06068e3 100644 (file)
@@ -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,
@@ -88,6 +87,12 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
         addNewChildDataNode(dataspaceName, anchorName, parentNodeXpath, newChildDataNode);
     }
 
+    @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) {
@@ -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 (file)
index 0000000..f107928
--- /dev/null
@@ -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());
+        }
+    }
+
+}
index 2c25a61..c9461bf 100755 (executable)
@@ -94,4 +94,12 @@ public interface FragmentRepository extends JpaRepository<FragmentEntity, Long>,
            nativeQuery = true)\r
     List<FragmentExtract> findByAnchorIdAndParentXpath(@Param("anchorId") int anchorId,\r
                                                        @Param("parentXpath") String parentXpath);\r
+\r
+    @Query(value = "SELECT id, anchor_id AS anchorId, xpath, parent_id AS parentId,"\r
+        + " CAST(attributes AS TEXT) AS attributes"\r
+        + " FROM FRAGMENT WHERE anchor_id = :anchorId"\r
+        + " AND xpath ~ :xpathRegex",\r
+        nativeQuery = true)\r
+    List<FragmentExtract> quickFindWithDescendants(@Param("anchorId") int anchorId,\r
+                                                   @Param("xpathRegex") String xpathRegex);\r
 }\r
index 1d61416..6e8f05f 100644 (file)
 
 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());
-        }
-    }
-
 }
index 5e4de7f..00e53aa 100644 (file)
@@ -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);
 }
index f70e218..ef701bc 100644 (file)
 
 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
index 681bbcd..48982d5 100644 (file)
@@ -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 (file)
index 0000000..8cad9f5
--- /dev/null
@@ -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(";");
+    }
+
+}
index fbf414d..cc2369d 100755 (executable)
@@ -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     | 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   | 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                      | dataNode              || expectedException
+            'parent does not exist'  | '/unknown'                       | newDataNode           || 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])
@@ -524,6 +529,25 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
             'list element under list element' | '/parent-203/child-204[@key="B"]/grand-child-204[@key2="Y"]' | LIST_DATA_NODE_PARENT203_FRAGMENT_ID || ["/parent-203/child-203", "/parent-203/child-204[@key='A']", "/parent-203/child-204[@key='B']"]
     }
 
+    @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.'
@@ -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'
index e69cbee..8234d32 100644 (file)
@@ -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
index bcb0807..4c67f7e 100644 (file)
@@ -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
     }
@@ -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 (file)
index 0000000..9b722cd
--- /dev/null
@@ -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
+    }
+
+}
index 77f262c..70fa447 100644 (file)
@@ -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.
       <groupId>org.opendaylight.yangtools</groupId>
       <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>
       <groupId>org.codehaus.janino</groupId>
       <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>
index b2e8c5b..012d7f8 100644 (file)
@@ -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
index b08d8c1..65dfa7f 100755 (executable)
@@ -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());
     }
 
index 28b18b3..b9da4af 100644 (file)
@@ -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,15 +36,26 @@ 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.
      *
@@ -54,6 +66,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.
      *
@@ -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);
 
index 1d8bac0..b23cdfc 100644 (file)
@@ -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();
     }
 
index 569f0a0..18d55d5 100644 (file)
@@ -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 (file)
index 0000000..f888504
--- /dev/null
@@ -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 (file)
index 0000000..09f2e16
--- /dev/null
@@ -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;
+    }
+}
index 48241ed..c0dfe52 100644 (file)
@@ -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);
+    }
 }
index b60e7e8..c81a50e 100644 (file)
@@ -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'
index 2fc85aa..ccfb23b 100755 (executable)
@@ -3,6 +3,7 @@
  * Copyright (C) 2021-2022 Nordix Foundation.\r
  * Modifications Copyright (C) 2021-2022 Bell Canada.\r
  * Modifications Copyright (C) 2021 Pantheon.tech\r
+ * Modifications Copyright (C) 2022 TechMahindra Ltd.\r
  * ================================================================================\r
  * Licensed under the Apache License, Version 2.0 (the "License");\r
  * you may not use this file except in compliance with the License.\r
@@ -90,9 +91,9 @@ class E2ENetworkSliceSpec extends Specification {
         when: 'saveData method is invoked'\r
             cpsDataServiceImpl.saveData(dataspaceName, anchorName, jsonData, noTimestamp)\r
         then: 'Parameters are validated and processing is delegated to persistence service'\r
-            1 * mockDataStoreService.storeDataNode('someDataspace', 'someAnchor', _) >>\r
+            1 * mockDataStoreService.storeDataNodes('someDataspace', 'someAnchor', _) >>\r
                     { args -> dataNodeStored = args[2]}\r
-            def child = dataNodeStored.childDataNodes[0]\r
+            def child = dataNodeStored[0].childDataNodes[0]\r
             assert child.childDataNodes.size() == 1\r
         and: 'list of Tracking Area for a Coverage Area are stored with correct xpath and child nodes '\r
             def listOfTAForCoverageArea = child.childDataNodes[0]\r
@@ -122,10 +123,10 @@ class E2ENetworkSliceSpec extends Specification {
         when: 'saveData method is invoked'\r
             cpsDataServiceImpl.saveData('someDataspace', 'someAnchor', jsonData, noTimestamp)\r
         then: 'parameters are validated and processing is delegated to persistence service'\r
-            1 * mockDataStoreService.storeDataNode('someDataspace', 'someAnchor', _) >>\r
+            1 * mockDataStoreService.storeDataNodes('someDataspace', 'someAnchor', _) >>\r
                     { args -> dataNodeStored = args[2]}\r
         and: 'the size of the tree is correct'\r
-            def cpsRanInventory = TestUtils.getFlattenMapByXpath(dataNodeStored)\r
+            def cpsRanInventory = TestUtils.getFlattenMapByXpath(dataNodeStored[0])\r
             assert  cpsRanInventory.size() == 4\r
         and: 'ran-inventory contains the correct child node'\r
             def ranInventory = cpsRanInventory.get('/ran-inventory')\r
index e46147c..1559783 100644 (file)
@@ -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.
 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.'() {
index e205a19..b70c437 100644 (file)
@@ -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.'() {
index 40f0e0a..2eede23 100644 (file)
@@ -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 (file)
index 0000000..b044e2e
--- /dev/null
@@ -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>'
+
+
+    }
+
+}
index 65aa3af..bf6e134 100644 (file)
@@ -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']
     }
-
 }
index 236221a..6d570d6 100644 (file)
@@ -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.
  *  ============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
 
index d1b8d68..459908b 100644 (file)
@@ -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 (file)
index 0000000..dd45e16
--- /dev/null
@@ -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 (file)
index 0000000..e206901
--- /dev/null
@@ -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 (file)
index 0000000..3daa814
--- /dev/null
@@ -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>
index 89d6784..8f4b522 100644 (file)
@@ -2,11 +2,11 @@
   "test-tree": {
     "branch": [
       {
-        "name": "Left",
+        "name": "LEFT/left",
         "nest": {
-          "name": "Small",
+          "name": "SMALL/small",
           "birds": [
-            "Sparrow",
+            "SPARROW/sparrow",
             "Robin",
             "Finch"
           ]
index 67412f3..78f0fbd 100755 (executable)
@@ -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
index 4952616..9fee634 100644 (file)
@@ -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
index 6703160..9a344c1 100755 (executable)
 
 # 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
index 2da2b73..096bd07 100644 (file)
@@ -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
 
 
index 09ccbe1..ec7d295 100644 (file)
@@ -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
index 76e167f..30715af 100755 (executable)
@@ -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
 -----------------------------------------