Merge "API versioning supported and added different versions for POST APIs"
authorJoseph Keenan <joseph.keenan@est.tech>
Wed, 21 Dec 2022 11:42:39 +0000 (11:42 +0000)
committerGerrit Code Review <gerrit@onap.org>
Wed, 21 Dec 2022 11:42:39 +0000 (11:42 +0000)
35 files changed:
cps-dependencies/pom.xml
cps-ncmp-rest-stub/pom.xml
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/inventory/CmHandleQueriesImpl.java
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/inventory/InventoryPersistenceImplSpec.groovy
cps-parent/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-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java
cps-ri/src/main/java/org/onap/cps/spi/impl/CpsModulePersistenceServiceImpl.java
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/performance/CpsToDataNodePerfTest.groovy
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/DataNode.java
cps-service/src/main/java/org/onap/cps/spi/model/DataNodeBuilder.java
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/JsonParserStreamSpec.groovy
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/data-with-choice-node.json [new file with mode: 0644]
cps-service/src/test/resources/yang-with-choice-node.yang [new file with mode: 0644]
csit/prepare-csit.sh
csit/run-csit.sh
docs/release-notes.rst

index 5bdf793..fb0638e 100755 (executable)
                 <artifactId>hazelcast-spring</artifactId>
                 <version>4.2.5</version>
             </dependency>
+            <dependency>
+                <groupId>com.google.guava</groupId>
+                <artifactId>guava</artifactId>
+                <version>31.1-jre</version>
+            </dependency>
         </dependencies>
     </dependencyManagement>
 </project>
index 93c73fc..35784fb 100644 (file)
             <artifactId>slf4j-simple</artifactId>
             <version>1.8.0-beta4</version>
         </dependency>
-        <dependency>
-            <groupId>com.google.guava</groupId>
-            <artifactId>guava</artifactId>
-            <version>20.0</version>
-        </dependency>
         <dependency>
             <groupId>cglib</groupId>
             <artifactId>cglib-nodep</artifactId>
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())
index 1a54a82..bda0a72 100644 (file)
@@ -47,6 +47,7 @@ public class CmHandleQueriesImpl implements CmHandleQueries {
 
     private static final String NCMP_DATASPACE_NAME = "NCMP-Admin";
     private static final String NCMP_DMI_REGISTRY_ANCHOR = "ncmp-dmi-registry";
+    private static final String DESCENDANT_PATH = "//";
 
     private final CpsDataPersistenceService cpsDataPersistenceService;
     private static final Map<String, NcmpServiceCmHandle> NO_QUERY_TO_EXECUTE = null;
@@ -72,7 +73,7 @@ public class CmHandleQueriesImpl implements CmHandleQueries {
         }
         Map<String, NcmpServiceCmHandle> cmHandleIdToNcmpServiceCmHandles = null;
         for (final Map.Entry<String, String> publicPropertyQueryPair : propertyQueryPairs.entrySet()) {
-            final String cpsPath = "//" + propertyType.getYangContainerName() + "[@name=\""
+            final String cpsPath = DESCENDANT_PATH + propertyType.getYangContainerName() + "[@name=\""
                     + publicPropertyQueryPair.getKey()
                     + "\" and @value=\"" + publicPropertyQueryPair.getValue() + "\"]";
 
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.'() {
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 d3fe0f3..b8408f8 100755 (executable)
@@ -39,7 +39,7 @@
         <app>org.onap.cps.Application</app>
         <java.version>11</java.version>
         <minimum-coverage>0.97</minimum-coverage>
-        <postgres.version>42.5.0</postgres.version>
+        <postgres.version>42.5.1</postgres.version>
 
         <jacoco.reportDirectory.aggregate>${project.reporting.outputDirectory}/jacoco-aggregate</jacoco.reportDirectory.aggregate>
         <sonar.coverage.jacoco.xmlReportPaths>
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..7183120 100644 (file)
@@ -60,6 +60,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 (/)");
index 53490f3..a9bd5d8 100644 (file)
@@ -32,6 +32,7 @@ import lombok.Setter;
 public class CpsPathQuery {
 
     private String xpathPrefix;
+    private String normalizedParentPath;
     private String normalizedXpath;
     private CpsPathPrefixType cpsPathPrefixType = ABSOLUTE;
     private String descendantName;
index 97d7d1d..283463b 100644 (file)
@@ -20,6 +20,8 @@
 
 package org.onap.cps.cpspath.parser;
 
+import static org.onap.cps.cpspath.parser.CpsPathPrefixType.ABSOLUTE;
+
 import lombok.AccessLevel;
 import lombok.Getter;
 import lombok.NoArgsConstructor;
@@ -45,8 +47,29 @@ 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();
+    }
+
+
+    /**
+     * 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 +80,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..662e42b
--- /dev/null
@@ -0,0 +1,88 @@
+/*
+ *  ============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 org.springframework.util.StopWatch
+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'
+            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'
+            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 '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)
+    }
+
+    def 'CPS Path Processing Performance Test.'() {
+        when: '200,000 paths are processed'
+            def setupStopWatch = new StopWatch()
+            setupStopWatch.start()
+            (1..100000).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')
+            }
+            setupStopWatch.stop()
+        then: 'it takes less then 10,000 milliseconds'
+            // In CI this actually takes about 3-5 sec  which  is approx. 50+ parser executions per millisecond!
+            assert setupStopWatch.getTotalTimeMillis() < 10000
+    }
+}
index b22f171..82bcea2 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.
@@ -24,6 +25,7 @@ package org.onap.cps.spi.impl;
 
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSet.Builder;
+import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -77,9 +79,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,
@@ -87,6 +86,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) {
@@ -166,14 +171,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);
         }
     }
 
@@ -346,7 +382,7 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
     private DataNode toDataNode(final FragmentEntity fragmentEntity,
                                 final FetchDescendantsOption fetchDescendantsOption) {
         final List<DataNode> childDataNodes = getChildDataNodes(fragmentEntity, fetchDescendantsOption);
-        Map<String, Object> leaves = new HashMap<>();
+        Map<String, Serializable> leaves = new HashMap<>();
         if (fragmentEntity.getAttributes() != null) {
             leaves = jsonObjectMapper.convertJsonString(fragmentEntity.getAttributes(), Map.class);
         }
@@ -368,7 +404,7 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
 
     @Override
     public void updateDataLeaves(final String dataspaceName, final String anchorName, final String xpath,
-                                 final Map<String, Object> leaves) {
+                                 final Map<String, Serializable> leaves) {
         final FragmentEntity fragmentEntity = getFragmentWithoutDescendantsByXpath(dataspaceName, anchorName, xpath);
         fragmentEntity.setAttributes(jsonObjectMapper.asJsonString(leaves));
         fragmentRepository.save(fragmentEntity);
@@ -511,13 +547,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);
index 03f021e..c9f9a78 100755 (executable)
@@ -337,12 +337,14 @@ public class CpsModulePersistenceServiceImpl implements CpsModulePersistenceServ
      */
     private String getNameForChecksum(
             final String checksum, final Collection<YangResourceEntity> yangResourceEntities) {
-        return
-                yangResourceEntities.stream()
+        final Optional<String> optionalFileName = yangResourceEntities.stream()
                         .filter(entity -> StringUtils.equals(checksum, (entity.getChecksum())))
                         .findFirst()
-                        .map(YangResourceEntity::getFileName)
-                        .orElse(null);
+                        .map(YangResourceEntity::getFileName);
+        if (optionalFileName.isPresent()) {
+            return optionalFileName.get();
+        }
+        return null;
     }
 
     /**
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..255e8e5 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'
@@ -174,7 +197,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 fb6749c..b26cef4 100644 (file)
@@ -27,7 +27,11 @@ import org.onap.cps.spi.model.DataNode
 import org.onap.cps.spi.model.DataNodeBuilder
 import org.springframework.beans.factory.annotation.Autowired
 import org.springframework.test.context.jdbc.Sql
+
+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 {
 
@@ -36,66 +40,85 @@ class CpsToDataNodePerfTest extends CpsPersistenceSpecBase {
     @Autowired
     CpsDataPersistenceService objectUnderTest
 
-    def PERF_TEST_PARENT = '/perf-parent-1'
+    static def PERF_TEST_PARENT = '/perf-parent-1'
+    static def NUMBER_OF_CHILDREN = 200
+    static def NUMBER_OF_GRAND_CHILDREN = 50
+    static def TOTAL_NUMBER_OF_NODES = 1 + NUMBER_OF_CHILDREN + (NUMBER_OF_CHILDREN * NUMBER_OF_GRAND_CHILDREN)  //  Parent + Children +  Grand-children
+    static def ALLOWED_SETUP_TIME_MS = TimeUnit.SECONDS.toMillis(10)
+    static def ALLOWED_READ_TIME_AL_NODES_MS = 500
 
-    def EXPECTED_NUMBER_OF_NODES = 10051  //  1 Parent + 50 Children + 10000 Grand-children
+    def readStopWatch = new StopWatch()
 
     @Sql([CLEAR_DATA, PERF_TEST_DATA])
-    def 'Get data node by xpath with all descendants with many children'() {
-        given: 'nodes and grandchildren have been persisted'
+    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()
             createLineage()
             setupStopWatch.stop()
             def setupDurationInMillis = setupStopWatch.getTime()
-        and: 'setup duration is under 8000 milliseconds'
-            assert setupDurationInMillis < 8000
+        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'
-            def readStopWatch = new StopWatch()
             readStopWatch.start()
-            def result = objectUnderTest.getDataNode('PERF-DATASPACE', 'PERF-ANCHOR', PERF_TEST_PARENT, INCLUDE_ALL_DESCENDANTS)
+            def result = objectUnderTest.getDataNode('PERF-DATASPACE', 'PERF-ANCHOR', xpath, INCLUDE_ALL_DESCENDANTS)
             readStopWatch.stop()
             def readDurationInMillis = readStopWatch.getTime()
-        then: 'read duration is under 450 milliseconds'
-            assert readDurationInMillis < 450
+        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'
-            assert countDataNodes(result) == EXPECTED_NUMBER_OF_NODES
-        when: 'get root is executed with all descendants'
+            assert countDataNodes(result) == TOTAL_NUMBER_OF_NODES
+        where: 'the following xPaths are used'
+            scenario || xpath
+            'parent' || PERF_TEST_PARENT
+            'root'   || ''
+    }
+
+    def 'Query parent data node with many descendants by cps-path'() {
+        when: 'query is executed with all descendants'
             readStopWatch.reset()
             readStopWatch.start()
-            result = objectUnderTest.getDataNode('PERF-DATASPACE', 'PERF-ANCHOR', '', INCLUDE_ALL_DESCENDANTS)
+            def result = objectUnderTest.queryDataNodes('PERF-DATASPACE', 'PERF-ANCHOR', '//perf-parent-1' , INCLUDE_ALL_DESCENDANTS)
             readStopWatch.stop()
-            readDurationInMillis = readStopWatch.getTime()
-        then: 'read duration is under 450 milliseconds'
-            assert readDurationInMillis < 450
+            def readDurationInMillis = readStopWatch.getTime()
+        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'
-            assert countDataNodes(result) == EXPECTED_NUMBER_OF_NODES
+            assert countDataNodes(result) == TOTAL_NUMBER_OF_NODES
+    }
+
+    def 'Query many descendants by cps-path with #scenario'() {
         when: 'query is executed with all descendants'
             readStopWatch.reset()
             readStopWatch.start()
-            result = objectUnderTest.queryDataNodes('PERF-DATASPACE', 'PERF-ANCHOR', '//perf-parent-1', INCLUDE_ALL_DESCENDANTS)
+            def result = objectUnderTest.queryDataNodes('PERF-DATASPACE', 'PERF-ANCHOR',  '//perf-test-grand-child-1', descendantsOption)
             readStopWatch.stop()
-            readDurationInMillis = readStopWatch.getTime()
-        then: 'read duration is under 450 milliseconds'
-            assert readDurationInMillis < 450
+            def readDurationInMillis = readStopWatch.getTime()
+        then: 'read duration is under 500 milliseconds'
+            assert readDurationInMillis < alowedDuration
         and: 'data node is returned with all the descendants populated'
-            assert countDataNodes(result) == EXPECTED_NUMBER_OF_NODES
+            assert result.size() == NUMBER_OF_CHILDREN
+        where: 'the following options are used'
+            scenario                                        | descendantsOption        || alowedDuration
+            'omit descendants                             ' | OMIT_DESCENDANTS         || 150
+            'include descendants (although there are none)' | INCLUDE_ALL_DESCENDANTS  || 1500
     }
 
     def createLineage() {
-        def numOfChildren = 50
-        def numOfGrandChildren = 200
-        (1..numOfChildren).each {
+        (1..NUMBER_OF_CHILDREN).each {
             def childName = "perf-test-child-${it}".toString()
-            def newChild = goForthAndMultiply(PERF_TEST_PARENT, childName, numOfGrandChildren)
+            def newChild = goForthAndMultiply(PERF_TEST_PARENT, childName)
             objectUnderTest.addChildDataNode('PERF-DATASPACE', 'PERF-ANCHOR', PERF_TEST_PARENT, newChild)
         }
     }
 
-    def goForthAndMultiply(parentXpath, childName, numOfGrandChildren) {
+    def goForthAndMultiply(parentXpath, childName) {
         def children = []
-        (1..numOfGrandChildren).each {
-            def child = new DataNodeBuilder().withXpath("${parentXpath}/${childName}/${it}perf-test-grand-child").build()
+        (1..NUMBER_OF_GRAND_CHILDREN).each {
+            def child = new DataNodeBuilder().withXpath("${parentXpath}/${childName}/perf-test-grand-child-${it}").build()
             children.add(child)
         }
         return new DataNodeBuilder().withXpath("${parentXpath}/${childName}").withChildDataNodes(children).build()
index b08d8c1..732b494 100755 (executable)
@@ -3,6 +3,7 @@
  *  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.
  *  ================================================================================
  *  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 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;
@@ -45,7 +47,7 @@ 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.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;
 
@@ -67,8 +69,9 @@ public class CpsDataServiceImpl implements CpsDataService {
     public void saveData(final String dataspaceName, final String anchorName, final String jsonData,
         final OffsetDateTime observedTimestamp) {
         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, jsonData);
+        cpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName, dataNodes);
         processDataUpdatedEventAsync(dataspaceName, anchorName, ROOT_NODE_XPATH, CREATE, observedTimestamp);
     }
 
@@ -76,8 +79,9 @@ public class CpsDataServiceImpl implements CpsDataService {
     public void saveData(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);
-        cpsDataPersistenceService.addChildDataNode(dataspaceName, anchorName, parentNodeXpath, dataNode);
+        final Collection<DataNode> dataNodes =
+                buildDataNodes(dataspaceName, anchorName, parentNodeXpath, jsonData);
+        cpsDataPersistenceService.addChildDataNodes(dataspaceName, anchorName, parentNodeXpath, dataNodes);
         processDataUpdatedEventAsync(dataspaceName, anchorName, parentNodeXpath, CREATE, observedTimestamp);
     }
 
@@ -161,8 +165,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);
+        final ArrayList<DataNode> nodes = new ArrayList<>(dataNodes);
+        cpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName, nodes);
         processDataUpdatedEventAsync(dataspaceName, anchorName, parentNodeXpath, UPDATE, observedTimestamp);
     }
 
@@ -226,15 +232,16 @@ public class CpsDataServiceImpl implements CpsDataService {
         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.parseJsonData(jsonData, schemaContext);
+            return new DataNodeBuilder().withContainerNode(containerNode).build();
         }
 
-        final NormalizedNode normalizedNode = YangUtils.parseJsonData(jsonData, schemaContext, parentNodeXpath);
+        final ContainerNode containerNode = YangUtils
+                .parseJsonData(jsonData, 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,
@@ -251,11 +258,20 @@ public class CpsDataServiceImpl implements CpsDataService {
 
         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.parseJsonData(jsonData, 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.parseJsonData(jsonData, 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");
index 8d4df20..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.
@@ -22,6 +23,7 @@
 
 package org.onap.cps.spi;
 
+import java.io.Serializable;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
@@ -34,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.
      *
@@ -53,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.
      *
@@ -61,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);
 
@@ -97,7 +119,7 @@ public interface CpsDataPersistenceService {
      * @param xpath         xpath
      * @param leaves        the leaves as a map where key is a leaf name and a value is a leaf value
      */
-    void updateDataLeaves(String dataspaceName, String anchorName, String xpath, Map<String, Object> leaves);
+    void updateDataLeaves(String dataspaceName, String anchorName, String xpath, Map<String, Serializable> leaves);
 
     /**
      * Replaces an existing data node's content including descendants.
index 8170db3..76f33bb 100644 (file)
@@ -46,7 +46,7 @@ public class DataNode implements Serializable {
     private ModuleReference moduleReference;
     private String xpath;
     private String moduleNamePrefix;
-    private Map<String, Object> leaves = Collections.emptyMap();
+    private Map<String, Serializable> leaves = Collections.emptyMap();
     private Collection<String> xpathsChildren;
     private Collection<DataNode> childDataNodes = Collections.emptySet();
 }
index eaa2d77..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.
@@ -23,6 +24,7 @@ package org.onap.cps.spi.model;
 
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
+import java.io.Serializable;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
@@ -33,6 +35,8 @@ import lombok.extern.slf4j.Slf4j;
 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;
@@ -44,11 +48,11 @@ 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 = "";
-    private Map<String, Object> leaves = Collections.emptyMap();
+    private Map<String, Serializable> leaves = Collections.emptyMap();
     private Collection<DataNode> childDataNodes = Collections.emptySet();
 
     /**
@@ -62,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;
     }
 
@@ -102,7 +105,7 @@ public class DataNodeBuilder {
      * @param leaves for the data node
      * @return DataNodeBuilder
      */
-    public DataNodeBuilder withLeaves(final Map<String, Object> leaves) {
+    public DataNodeBuilder withLeaves(final Map<String, Serializable> leaves) {
         this.leaves = leaves;
         return this;
     }
@@ -126,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();
     }
 
     /**
@@ -139,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() {
@@ -155,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");
@@ -164,23 +165,29 @@ 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();
     }
 
     private static void addDataNodeFromNormalizedNode(final DataNode currentDataNode,
         final NormalizedNode normalizedNode) {
 
-        if (normalizedNode instanceof DataContainerNode) {
+        if (normalizedNode instanceof ChoiceNode) {
+            addChoiceNode(currentDataNode, (ChoiceNode) normalizedNode);
+        } else if (normalizedNode instanceof DataContainerNode) {
             addYangContainer(currentDataNode, (DataContainerNode) normalizedNode);
         } else if (normalizedNode instanceof MapNode) {
             addDataNodeForEachListElement(currentDataNode, (MapNode) normalizedNode);
         } else if (normalizedNode instanceof ValueNode) {
             final ValueNode<NormalizedNode> valuesNode = (ValueNode) normalizedNode;
             addYangLeaf(currentDataNode, valuesNode.getIdentifier().getNodeType().getLocalName(),
-                    valuesNode.body());
+                    (Serializable) valuesNode.body());
         } else if (normalizedNode instanceof LeafSetNode) {
             addYangLeafList(currentDataNode, (LeafSetNode<?>) normalizedNode);
         } else {
@@ -199,8 +206,9 @@ public class DataNodeBuilder {
         }
     }
 
-    private static void addYangLeaf(final DataNode currentDataNode, final String leafName, final Object leafValue) {
-        final Map<String, Object> leaves = new ImmutableMap.Builder<String, Object>()
+    private static void addYangLeaf(final DataNode currentDataNode, final String leafName,
+                                    final Serializable leafValue) {
+        final Map<String, Serializable> leaves = new ImmutableMap.Builder<String, Serializable>()
             .putAll(currentDataNode.getLeaves())
             .put(leafName, leafValue)
             .build();
@@ -213,7 +221,7 @@ public class DataNodeBuilder {
                 .stream()
                 .map(normalizedNode -> (normalizedNode).body())
                 .collect(Collectors.toUnmodifiableList());
-        addYangLeaf(currentDataNode, leafListName, leafListValues);
+        addYangLeaf(currentDataNode, leafListName, (Serializable) leafListValues);
     }
 
     private static void addDataNodeForEachListElement(final DataNode currentDataNode, final MapNode mapNode) {
@@ -236,4 +244,13 @@ public class DataNodeBuilder {
         return newChildDataNode;
     }
 
+    private static void addChoiceNode(final DataNode currentDataNode, final ChoiceNode choiceNode) {
+
+        final Collection<DataContainerChild> normalizedChildNodes = choiceNode.body();
+        for (final NormalizedNode normalizedNode : normalizedChildNodes) {
+            addDataNodeFromNormalizedNode(currentDataNode, normalizedNode);
+        }
+    }
+
+
 }
index 48241ed..9a61579 100644 (file)
@@ -3,6 +3,7 @@
  *  Copyright (C) 2020-2022 Nordix Foundation
  *  Modifications Copyright (C) 2021 Bell Canada.
  *  Modifications Copyright (C) 2021 Pantheon.tech
+ *  Modifications Copyright (C) 2022 TechMahindra Ltd.
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -39,13 +40,14 @@ import lombok.extern.slf4j.Slf4j;
 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.NormalizedNode;
+import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
+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.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;
 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
@@ -62,38 +64,40 @@ public class YangUtils {
     private static final String XPATH_NODE_KEY_ATTRIBUTES_REGEX = "\\[.*?\\]";
 
     /**
-     * Parses jsonData into NormalizedNode according to given schema context.
+     * Parses jsonData 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 NormalizedNode object
+     * @return the Collection of NormalizedNode object
      */
-    public static NormalizedNode parseJsonData(final String jsonData, final SchemaContext schemaContext) {
+    public static ContainerNode parseJsonData(final String jsonData, final SchemaContext schemaContext) {
         return parseJsonData(jsonData, schemaContext, Optional.empty());
     }
 
     /**
-     * Parses jsonData into NormalizedNode according to given schema context.
+     * 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,
+    public static ContainerNode parseJsonData(final String jsonData, final SchemaContext schemaContext,
         final String parentNodeXpath) {
         final Collection<QName> dataSchemaNodeIdentifiers =
                 getDataSchemaNodeIdentifiersByXpath(parentNodeXpath, schemaContext);
         return parseJsonData(jsonData, schemaContext, Optional.of(dataSchemaNodeIdentifiers));
     }
 
-    private static NormalizedNode parseJsonData(final String jsonData, final SchemaContext schemaContext,
+    private static ContainerNode parseJsonData(final String jsonData, final SchemaContext schemaContext,
         final Optional<Collection<QName>> dataSchemaNodeIdentifiers) {
         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(schemaContext.getQName()));
         final NormalizedNodeStreamWriter normalizedNodeStreamWriter = ImmutableNormalizedNodeStreamWriter
-            .from(normalizedNodeResult);
+                .from(dataContainerNodeBuilder);
         final JsonReader jsonReader = new JsonReader(new StringReader(jsonData));
         final JsonParserStream jsonParserStream;
 
@@ -119,7 +123,7 @@ public class YangUtils {
                 "Failed to parse json data. Unsupported xpath or json data:" + jsonData, illegalStateException
                 .getMessage(), illegalStateException);
         }
-        return normalizedNodeResult.getResult();
+        return dataContainerNodeBuilder.build();
     }
 
     /**
index b60e7e8..b78ab8a 100644 (file)
@@ -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.
@@ -62,17 +63,22 @@ class CpsDataServiceImplSpec extends Specification {
 
     def 'Saving 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 child data fragment under existing node.'() {
@@ -82,8 +88,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 +213,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 fcfb482..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 {
 
-    Map<String, Map<String, Object>> expectedLeavesByXpathMap = [
+    Map<String, Map<String, Serializable>> expectedLeavesByXpathMap = [
             '/test-tree'                                            : [],
             '/test-tree/branch[@name=\'Left\']'                     : [name: 'Left'],
             '/test-tree/branch[@name=\'Left\']/nest'                : [name: 'Small', birds: ['Sparrow', 'Robin', 'Finch']],
@@ -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,16 +138,35 @@ class DataNodeBuilderSpec extends Specification {
             assert result.leaves['source-tp'] == '1-2-1'
     }
 
-    def 'Converting NormalizedNode into DataNode collection: #scenario.'() {
+    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 container node object'
+            def jsonData = TestUtils.getResourceFileContent('data-with-choice-node.json')
+            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([
+                "/container-with-choice-leaves"
+            ])
+            assert result.leaves['leaf-1'] == "test"
+            assert result.leaves['choice-case1-leaf-a'] == "test"
+            assert result.leaves['choice-case1-leaf-b'] == "test"
+    }
+
+    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'
@@ -161,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 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'
index 65aa3af..990b718 100644 (file)
@@ -2,6 +2,7 @@
  * ============LICENSE_START=======================================================
  *  Copyright (C) 2020-2022 Nordix Foundation
  *  Modifications Copyright (C) 2021 Pantheon.tech
+ *  Modifications Copyright (C) 2022 TechMahindra Ltd.
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -31,14 +32,20 @@ import spock.lang.Specification
 class YangUtilsSpec extends Specification {
     def 'Parsing a valid 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)
-        then: 'the result is a normalized node of the correct type'
-            result.getIdentifier().nodeType == QName.create('org:onap:ccsdk:sample', '2020-09-15', 'bookstore')
+            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 invalid data: #description.'() {
@@ -62,8 +69,10 @@ class YangUtilsSpec extends Specification {
             def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourcesMap).getSchemaContext()
         when: 'json string is parsed'
             def result = YangUtils.parseJsonData(jsonData, 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'
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
 
diff --git a/cps-service/src/test/resources/data-with-choice-node.json b/cps-service/src/test/resources/data-with-choice-node.json
new file mode 100644 (file)
index 0000000..5f81ed8
--- /dev/null
@@ -0,0 +1,8 @@
+{
+  "container-with-choice-leaves": {
+    "leaf-1": "test",
+    "choice-case1-leaf-a": "test",
+    "choice-case1-leaf-b": "test"
+  }
+}
+
diff --git a/cps-service/src/test/resources/yang-with-choice-node.yang b/cps-service/src/test/resources/yang-with-choice-node.yang
new file mode 100644 (file)
index 0000000..55c0bfb
--- /dev/null
@@ -0,0 +1,27 @@
+module yang-with-choice-node {
+  yang-version 1.1;
+  namespace "org:onap:cps:test:yang-with-choice-node";
+  prefix "yang-with-choice-node";
+
+  container container-with-choice-leaves {
+    leaf leaf-1 {
+      type string;
+    }
+
+    choice choicenode {
+      case case-1 {
+        leaf choice-case1-leaf-a {
+          type string;
+        }
+        leaf choice-case1-leaf-b {
+          type string;
+        }
+      }
+      case case-2 {
+        leaf choice-case2-leaf-a {
+          type string;
+        }
+      }
+    }
+  }
+}
index 67412f3..dde9616 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
@@ -48,7 +63,10 @@ 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 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 6e6236b..3221900 100755 (executable)
@@ -39,6 +39,8 @@ 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-1187 <https://jira.onap.org/browse/CPS-1187>`_  Added API to get all schema sets for a given dataspace.
+   - `CPS-341 <https://jira.onap.org/browse/CPS-341>`_  Added support for multiple data tree instances under 1 anchor.
 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