CPS-1401 Implement V2 of GET Data Node API 86/132886/21
authorarpitsingh <as00745003@techmahindra.com>
Mon, 9 Jan 2023 14:23:10 +0000 (19:53 +0530)
committerArpit Singh <as00745003@techmahindra.com>
Fri, 17 Feb 2023 11:20:35 +0000 (11:20 +0000)
- Modified the GET Data Node API so it returns all the data nodes when
  xpath set to root "/"
- Fragment Repository now returns a collection of Fragment Entities
- Instead of returning only the first Fragment Entity now all fragment
  entities are returned when xpath is set to root
- The Fragemnt Entities are further processed to a Collection of Data
  Nodes. As opposed to singular Data Node in current implementation.
- Finally the DataRestController also returns a Collection of Data
  Nodes when xpath is set to root and valid data is present
- Response body changed from JSON object to JSON Array.
- Exception handling for invalid xpath and non-existing xpath is now
  done separately at persistence layer.
- Refactored code against CPS-1422
- Deprecated getDataNode method from Service and Persistence layer
- Modified V1 of Get Data Node API to use the getDataNodes method and
  get the first data node from the collection returned.
- Modified NCMP to use getDataNodes method
- NCMP still does not support multiple data nodes. It retrieves the
  first data node from the collection returned by getDataNodes

Signed-off-by: arpitsingh <as00745003@techmahindra.com>
Change-Id: I494a5740a53f65376d135fcb9f1e2e8900a2803e

27 files changed:
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyCmHandlerQueryServiceImpl.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServiceImpl.java
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandler.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/impl/NetworkCmProxyDataServiceImplSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/NetworkCmProxyDataServicePropertyHandlerSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/CmHandleQueriesImplSpec.groovy
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/inventory/InventoryPersistenceImplSpec.groovy
cps-rest/docs/openapi/cpsDataV2.yml
cps-rest/src/main/java/org/onap/cps/rest/controller/DataRestController.java
cps-rest/src/test/groovy/org/onap/cps/rest/controller/DataRestControllerSpec.groovy
cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.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/impl/CpsPersistenceSpecBase.groovy
cps-ri/src/test/groovy/org/onap/cps/spi/performance/CpsDataPersistenceServicePerfTest.groovy
cps-ri/src/test/resources/data/fragment.sql
cps-service/src/main/java/org/onap/cps/api/CpsDataService.java
cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java
cps-service/src/main/java/org/onap/cps/notification/CpsDataUpdatedEventFactory.java
cps-service/src/main/java/org/onap/cps/spi/CpsDataPersistenceService.java
cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy
cps-service/src/test/groovy/org/onap/cps/notification/CpsDataUpdateEventFactorySpec.groovy
integration-test/src/test/groovy/org/onap/cps/integration/CpsPersistenceSpec.groovy

index b67ae0c..a98c600 100644 (file)
@@ -1,6 +1,7 @@
 /*
  *  ============LICENSE_START=======================================================
  *  Copyright (C) 2022 Nordix Foundation
+ *  Modifications Copyright (C) 2023 TechMahindra Ltd.
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -217,8 +218,8 @@ public class NetworkCmProxyCmHandlerQueryServiceImpl implements NetworkCmProxyCm
         final Map<String, NcmpServiceCmHandle> queryResult = new HashMap<>(cmHandleIdsByModuleName.size());
         if (previousQueryResult == NO_QUERY_TO_EXECUTE) {
             cmHandleIdsByModuleName.forEach(cmHandleId ->
-                    queryResult.put(cmHandleId, createNcmpServiceCmHandle(
-                            inventoryPersistence.getDataNode("/dmi-registry/cm-handles[@id='" + cmHandleId + "']")))
+                    queryResult.put(cmHandleId, createNcmpServiceCmHandle(inventoryPersistence
+                            .getDataNode("/dmi-registry/cm-handles[@id='" + cmHandleId + "']").iterator().next()))
             );
             return queryResult;
         }
@@ -298,13 +299,14 @@ public class NetworkCmProxyCmHandlerQueryServiceImpl implements NetworkCmProxyCm
     }
 
     private Set<NcmpServiceCmHandle> getAllCmHandles() {
-        return inventoryPersistence.getDataNode("/dmi-registry")
-                .getChildDataNodes().stream().map(this::createNcmpServiceCmHandle).collect(Collectors.toSet());
+        final DataNode dataNode = inventoryPersistence.getDataNode("/dmi-registry").iterator().next();
+        return dataNode.getChildDataNodes().stream().map(this::createNcmpServiceCmHandle).collect(Collectors.toSet());
     }
 
     private Set<String> getAllCmHandleIds() {
-        return inventoryPersistence.getDataNode("/dmi-registry", FETCH_DIRECT_CHILDREN_ONLY)
-                .getChildDataNodes().stream().map(dataNode -> dataNode.getLeaves().get("id").toString())
+        final DataNode dataNodes = inventoryPersistence.getDataNode("/dmi-registry", FETCH_DIRECT_CHILDREN_ONLY)
+                .iterator().next();
+        return dataNodes.getChildDataNodes().stream().map(dataNode -> dataNode.getLeaves().get("id").toString())
                 .collect(Collectors.toSet());
     }
 
index e71b72a..d5500b8 100755 (executable)
@@ -4,6 +4,7 @@
  *  Modifications Copyright (C) 2021-2023 Nordix Foundation
  *  Modifications Copyright (C) 2021 Pantheon.tech
  *  Modifications Copyright (C) 2021-2022 Bell Canada
+ *  Modifications Copyright (C) 2023 TechMahindra Ltd.
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -131,8 +132,8 @@ public class NetworkCmProxyDataServiceImpl implements NetworkCmProxyDataService
     public Object getResourceDataOperational(final String cmHandleId,
                                              final String resourceIdentifier,
                                              final FetchDescendantsOption fetchDescendantsOption) {
-        return cpsDataService.getDataNode(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, cmHandleId, resourceIdentifier,
-                fetchDescendantsOption);
+        return cpsDataService.getDataNodes(NFP_OPERATIONAL_DATASTORE_DATASPACE_NAME, cmHandleId, resourceIdentifier,
+                fetchDescendantsOption).iterator().next();
     }
 
     @Override
index f39c2f9..bbb2c0f 100644 (file)
@@ -2,6 +2,7 @@
  *  ============LICENSE_START=======================================================
  *  Copyright (C) 2022 Nordix Foundation
  *  Modifications Copyright (C) 2022 Bell Canada
+ *  Modifications Copyright (C) 2023 TechMahindra Ltd.
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -66,7 +67,8 @@ public class NetworkCmProxyDataServicePropertyHandler {
         for (final NcmpServiceCmHandle ncmpServiceCmHandle : ncmpServiceCmHandles) {
             final String cmHandleId = ncmpServiceCmHandle.getCmHandleId();
             try {
-                final DataNode existingCmHandleDataNode = inventoryPersistence.getCmHandleDataNode(cmHandleId);
+                final DataNode existingCmHandleDataNode = inventoryPersistence.getCmHandleDataNode(cmHandleId)
+                        .iterator().next();
                 processUpdates(existingCmHandleDataNode, ncmpServiceCmHandle);
                 cmHandleRegistrationResponses.add(CmHandleRegistrationResponse.createSuccessResponse(cmHandleId));
             } catch (final DataNodeNotFoundException e) {
index bda0a72..0f86cb7 100644 (file)
@@ -1,6 +1,7 @@
 /*
  *  ============LICENSE_START=======================================================
  *  Copyright (C) 2022 Nordix Foundation
+ *  Modifications Copyright (C) 2023 TechMahindra Ltd.
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -174,8 +175,8 @@ public class CmHandleQueriesImpl implements CmHandleQueries {
 
     private DataNode getCmHandleState(final String cmHandleId) {
         final String xpath = "/dmi-registry/cm-handles[@id='" + cmHandleId + "']/state";
-        return cpsDataPersistenceService.getDataNode(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,
-                xpath, OMIT_DESCENDANTS);
+        return cpsDataPersistenceService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,
+                xpath, OMIT_DESCENDANTS).iterator().next();
     }
 }
 
index 10227cf..d22cf0e 100644 (file)
@@ -1,6 +1,7 @@
 /*
  *  ============LICENSE_START=======================================================
  *  Copyright (C) 2022-2023 Nordix Foundation
+ *  Modifications Copyright (C) 2023 TechMahindra Ltd.
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -119,7 +120,7 @@ public interface InventoryPersistence {
      * @param xpath xpath
      * @return data node
      */
-    DataNode getDataNode(String xpath);
+    Collection<DataNode> getDataNode(String xpath);
 
     /**
      * Get data node via xpath.
@@ -128,7 +129,7 @@ public interface InventoryPersistence {
      * @param fetchDescendantsOption fetch descendants option
      * @return data node
      */
-    DataNode getDataNode(String xpath, FetchDescendantsOption fetchDescendantsOption);
+    Collection<DataNode> getDataNode(String xpath, FetchDescendantsOption fetchDescendantsOption);
 
     /**
      * Get collection of data nodes via xpaths.
@@ -153,7 +154,7 @@ public interface InventoryPersistence {
      * @param cmHandleId cmHandle ID
      * @return data node
      */
-    DataNode getCmHandleDataNode(String cmHandleId);
+    Collection<DataNode> getCmHandleDataNode(String cmHandleId);
 
     /**
      * Get collection of data nodes of given cm handles.
index 4d1202b..1c799c6 100644 (file)
@@ -2,6 +2,7 @@
  *  ============LICENSE_START=======================================================
  *  Copyright (C) 2022-2023 Nordix Foundation
  *  Modifications Copyright (C) 2022 Bell Canada
+ *  Modifications Copyright (C) 2023 TechMahindra Ltd.
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -73,9 +74,9 @@ public class InventoryPersistenceImpl implements InventoryPersistence {
 
     @Override
     public CompositeState getCmHandleState(final String cmHandleId) {
-        final DataNode stateAsDataNode = cpsDataService.getDataNode(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,
+        final DataNode stateAsDataNode = cpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,
                 createCmHandleXPath(cmHandleId) + "/state",
-                FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS);
+                FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS).iterator().next();
         cpsValidator.validateNameCharacters(cmHandleId);
         return new CompositeStateBuilder().fromDataNode(stateAsDataNode).build();
     }
@@ -101,7 +102,8 @@ public class InventoryPersistenceImpl implements InventoryPersistence {
     @Override
     public YangModelCmHandle getYangModelCmHandle(final String cmHandleId) {
         cpsValidator.validateNameCharacters(cmHandleId);
-        return YangDataConverter.convertCmHandleToYangModel(getCmHandleDataNode(cmHandleId), cmHandleId);
+        final DataNode dataNode = getCmHandleDataNode(cmHandleId).iterator().next();
+        return YangDataConverter.convertCmHandleToYangModel(dataNode, cmHandleId);
     }
 
     @Override
@@ -169,15 +171,15 @@ public class InventoryPersistenceImpl implements InventoryPersistence {
     @Override
     @Timed(value = "cps.ncmp.inventory.persistence.datanode.get",
             description = "Time taken to get a data node (from ncmp dmi registry)")
-    public DataNode getDataNode(final String xpath) {
+    public Collection<DataNode> getDataNode(final String xpath) {
         return getDataNode(xpath, INCLUDE_ALL_DESCENDANTS);
     }
 
     @Override
     @Timed(value = "cps.ncmp.inventory.persistence.datanode.get",
             description = "Time taken to get a data node (from ncmp dmi registry)")
-    public DataNode getDataNode(final String xpath, final FetchDescendantsOption fetchDescendantsOption) {
-        return cpsDataService.getDataNode(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,
+    public Collection<DataNode> getDataNode(final String xpath, final FetchDescendantsOption fetchDescendantsOption) {
+        return cpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,
                 xpath, fetchDescendantsOption);
     }
 
@@ -189,12 +191,12 @@ public class InventoryPersistenceImpl implements InventoryPersistence {
     @Override
     public Collection<DataNode> getDataNodes(final Collection<String> xpaths,
                                              final FetchDescendantsOption fetchDescendantsOption) {
-        return cpsDataService.getDataNodes(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,
+        return cpsDataService.getDataNodesForMultipleXpaths(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,
                 xpaths, fetchDescendantsOption);
     }
 
     @Override
-    public DataNode getCmHandleDataNode(final String cmHandleId) {
+    public Collection<DataNode> getCmHandleDataNode(final String cmHandleId) {
         return this.getDataNode(createCmHandleXPath(cmHandleId));
     }
 
index 05856d0..5cd702a 100644 (file)
@@ -1,6 +1,7 @@
 /*
  *  ============LICENSE_START=======================================================
  *  Copyright (C) 2022 Nordix Foundation
+ *  Modifications Copyright (C) 2023 TechMahindra Ltd.
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -40,7 +41,7 @@ class NetworkCmProxyCmHandlerQueryServiceSpec extends Specification {
     def partiallyMockedCmHandleQueries = Spy(CmHandleQueriesImpl)
     def mockInventoryPersistence = Mock(InventoryPersistence)
 
-    def static someCmHandleDataNode = new DataNode(xpath: '/dmi-registry/cm-handles[@id=\'some-cmhandle-id\']', leaves: ['id':'some-cmhandle-id'])
+    def static someCmHandleDataNode = [new DataNode(xpath: '/dmi-registry/cm-handles[@id=\'some-cmhandle-id\']', leaves: ['id':'some-cmhandle-id'])]
     def dmiRegistry = new DataNode(xpath: '/dmi-registry', childDataNodes: createDataNodeList(['PNFDemo1', 'PNFDemo2', 'PNFDemo3', 'PNFDemo4']))
 
     static def queryResultCmHandleMap = createCmHandleMap(['H1', 'H2'])
@@ -136,7 +137,7 @@ class NetworkCmProxyCmHandlerQueryServiceSpec extends Specification {
         and: 'cmHandles are returned from the module names query'
             mockInventoryPersistence.getCmHandleIdsWithGivenModules(['some-module-name']) >> anchorsForModuleQuery
         and: 'cmHandleQueries returns a datanode result'
-            2 * cmHandleQueries.queryCmHandleDataNodesByCpsPath(*_) >> [someCmHandleDataNode]
+            2 * cmHandleQueries.queryCmHandleDataNodesByCpsPath(*_) >> someCmHandleDataNode
         when: 'the query is executed for both cm handle ids and details'
             def returnedCmHandlesJustIds = objectUnderTest.queryCmHandleIds(cmHandleQueryParameters)
             def returnedCmHandlesWithData = objectUnderTest.queryCmHandles(cmHandleQueryParameters)
@@ -157,9 +158,9 @@ class NetworkCmProxyCmHandlerQueryServiceSpec extends Specification {
         given: 'We use an empty query'
             def cmHandleQueryParameters = new CmHandleQueryServiceParameters()
         and: 'the inventory persistence returns the dmi registry datanode with just ids'
-            mockInventoryPersistence.getDataNode("/dmi-registry", FetchDescendantsOption.FETCH_DIRECT_CHILDREN_ONLY) >> dmiRegistry
+            mockInventoryPersistence.getDataNode("/dmi-registry", FetchDescendantsOption.FETCH_DIRECT_CHILDREN_ONLY) >> [dmiRegistry]
         and: 'the inventory persistence returns the dmi registry datanode with data'
-            mockInventoryPersistence.getDataNode("/dmi-registry") >> dmiRegistry
+            mockInventoryPersistence.getDataNode("/dmi-registry") >> [dmiRegistry]
         when: 'the query is executed for both cm handle ids and details'
             def returnedCmHandlesJustIds = objectUnderTest.queryCmHandleIds(cmHandleQueryParameters)
             def returnedCmHandlesWithData = objectUnderTest.queryCmHandles(cmHandleQueryParameters)
@@ -173,7 +174,7 @@ class NetworkCmProxyCmHandlerQueryServiceSpec extends Specification {
         given: 'We query without any parameters'
             def cmHandleQueryParameters = new CmHandleQueryServiceParameters()
         and: 'the inventoryPersistence returns all four CmHandleIds'
-            mockInventoryPersistence.getDataNode(*_) >> dmiRegistry
+            mockInventoryPersistence.getDataNode(*_) >> [dmiRegistry]
         when: 'the query executed'
             def resultSet = objectUnderTest.queryCmHandleIdsForInventory(cmHandleQueryParameters)
         then: 'the size of the result list equals the size of all cmHandleIds.'
index 578f7f3..fe6fa32 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) 2023 TechMahindra Ltd.
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -89,11 +90,11 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
 
     def cmHandleXPath = "/dmi-registry/cm-handles[@id='testCmHandle']"
 
-    def dataNode = new DataNode(leaves: ['id': 'some-cm-handle', 'dmi-service-name': 'testDmiService'])
+    def dataNode = [new DataNode(leaves: ['id': 'some-cm-handle', 'dmi-service-name': 'testDmiService'])]
 
     def 'Write resource data for pass-through running from DMI using POST.'() {
         given: 'cpsDataService returns valid datanode'
-            mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
+            mockCpsDataService.getDataNodes('NCMP-Admin', 'ncmp-dmi-registry',
                 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode
         when: 'write resource data is called'
             objectUnderTest.writeResourceDataPassThroughRunningForCmHandle('testCmHandle',
@@ -107,7 +108,7 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
 
     def 'Get resource data for pass-through operational from DMI.'() {
         given: 'get data node is called'
-            mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
+            mockCpsDataService.getDataNodes('NCMP-Admin', 'ncmp-dmi-registry',
                 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode
         and: 'get resource data from DMI is called'
             mockDmiDataOperations.getResourceDataFromDmi(
@@ -129,7 +130,7 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
 
     def 'Get resource data for pass-through running from DMI.'() {
         given: 'cpsDataService returns valid data node'
-            mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
+            mockCpsDataService.getDataNodes('NCMP-Admin', 'ncmp-dmi-registry',
                 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode
         and: 'DMI returns valid response and data'
             mockDmiDataOperations.getResourceDataFromDmi('testCmHandle',
@@ -233,7 +234,7 @@ class NetworkCmProxyDataServiceImplSpec extends Specification {
 
     def 'Update resource data for pass-through running from dmi using POST #scenario DMI properties.'() {
         given: 'cpsDataService returns valid datanode'
-            mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
+            mockCpsDataService.getDataNodes('NCMP-Admin', 'ncmp-dmi-registry',
                 cmHandleXPath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode
         when: 'get resource data is called'
             objectUnderTest.writeResourceDataPassThroughRunningForCmHandle('testCmHandle',
index 64461fa..0df61f4 100644 (file)
@@ -2,6 +2,7 @@
  * ============LICENSE_START=======================================================
  * Copyright (C) 2022 Nordix Foundation
  * Modifications Copyright (C) 2022 Bell Canada
+ * Modifications Copyright (C) 2023 TechMahindra Ltd.
  * ================================================================================
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -47,11 +48,11 @@ class NetworkCmProxyDataServicePropertyHandlerSpec extends Specification {
                                     new DataNodeBuilder().withXpath("/dmi-registry/cm-handles[@id='${cmHandleId}']/additional-properties[@name='additionalProp2']").withLeaves(['name': 'additionalProp2', 'value': 'additionalValue2']).build(),
                                     new DataNodeBuilder().withXpath("/dmi-registry/cm-handles[@id='${cmHandleId}']/public-properties[@name='publicProp3']").withLeaves(['name': 'publicProp3', 'value': 'publicValue3']).build(),
                                     new DataNodeBuilder().withXpath("/dmi-registry/cm-handles[@id='${cmHandleId}']/public-properties[@name='publicProp4']").withLeaves(['name': 'publicProp4', 'value': 'publicValue4']).build()]
-    def static cmHandleDataNode = new DataNode(xpath: cmHandleXpath, childDataNodes: propertyDataNodes)
+    def static cmHandleDataNodeAsCollection = [new DataNode(xpath: cmHandleXpath, childDataNodes: propertyDataNodes)]
 
     def 'Update CM Handle Public Properties: #scenario'() {
         given: 'the CPS service return a CM handle'
-            mockInventoryPersistence.getCmHandleDataNode(cmHandleId) >> cmHandleDataNode
+            mockInventoryPersistence.getCmHandleDataNode(cmHandleId) >> cmHandleDataNodeAsCollection
         and: 'an update cm handle request with public properties updates'
             def cmHandleUpdateRequest = [new NcmpServiceCmHandle(cmHandleId: cmHandleId, publicProperties: updatedPublicProperties)]
         when: 'update data node leaves is called with the update request'
@@ -73,7 +74,7 @@ class NetworkCmProxyDataServicePropertyHandlerSpec extends Specification {
 
     def 'Update DMI Properties: #scenario'() {
         given: 'the CPS service return a CM handle'
-            mockInventoryPersistence.getCmHandleDataNode(cmHandleId) >> cmHandleDataNode
+            mockInventoryPersistence.getCmHandleDataNode(cmHandleId) >> cmHandleDataNodeAsCollection
         and: 'an update cm handle request with DMI properties updates'
             def cmHandleUpdateRequest = [new NcmpServiceCmHandle(cmHandleId: cmHandleId, dmiProperties: updatedDmiProperties)]
         when: 'update data node leaves is called with the update request'
@@ -97,7 +98,7 @@ class NetworkCmProxyDataServicePropertyHandlerSpec extends Specification {
     def 'Update CM Handle Properties, remove all properties: #scenario'() {
         given: 'the CPS service return a CM handle'
             def cmHandleDataNode = new DataNode(xpath: cmHandleXpath, childDataNodes: originalPropertyDataNodes)
-            mockInventoryPersistence.getCmHandleDataNode(cmHandleId) >> cmHandleDataNode
+            mockInventoryPersistence.getCmHandleDataNode(cmHandleId) >> [cmHandleDataNode]
         and: 'an update cm handle request that removes all public properties(existing and non-existing)'
             def cmHandleUpdateRequest = [new NcmpServiceCmHandle(cmHandleId: cmHandleId, publicProperties: ['publicProp3': null, 'publicProp4': null])]
         when: 'update data node leaves is called with the update request'
@@ -145,7 +146,8 @@ class NetworkCmProxyDataServicePropertyHandlerSpec extends Specification {
                                          new NcmpServiceCmHandle(cmHandleId: cmHandleId, publicProperties: ['publicProp1': "value"], dmiProperties: [:]),
                                          new NcmpServiceCmHandle(cmHandleId: cmHandleId, publicProperties: ['publicProp1': "value"], dmiProperties: [:])]
         and: 'data node can be found for 1st and 3rd cm-handle but not for 2nd cm-handle'
-            mockInventoryPersistence.getCmHandleDataNode(*_) >> cmHandleDataNode >> { throw new DataNodeNotFoundException('NCMP-Admin', 'ncmp-dmi-registry') } >> cmHandleDataNode
+            mockInventoryPersistence.getCmHandleDataNode(*_) >> cmHandleDataNodeAsCollection >> {
+                throw new DataNodeNotFoundException('NCMP-Admin', 'ncmp-dmi-registry') } >> cmHandleDataNodeAsCollection
         when: 'update data node leaves is called using correct parameters'
             def cmHandleResponseList = objectUnderTest.updateCmHandleProperties(cmHandleUpdateRequest)
         then: 'response has 3 values'
index 2800f5c..771198e 100644 (file)
@@ -1,6 +1,7 @@
 /*
  *  ============LICENSE_START=======================================================
  *  Copyright (C) 2022 Nordix Foundation
+ *  Modifications Copyright (C) 2023 TechMahindra Ltd.
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -120,8 +121,8 @@ class CmHandleQueriesImplSpec extends Specification {
         given: 'a cm handle state to compare'
             def cmHandleState = state
         and: 'the persistence service returns a list of data nodes'
-            cpsDataPersistenceService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
-                    '/dmi-registry/cm-handles[@id=\'some-cm-handle\']/state', OMIT_DESCENDANTS) >> new DataNode(leaves: ['cm-handle-state': 'READY'])
+            cpsDataPersistenceService.getDataNodes('NCMP-Admin', 'ncmp-dmi-registry',
+                    '/dmi-registry/cm-handles[@id=\'some-cm-handle\']/state', OMIT_DESCENDANTS) >> [new DataNode(leaves: ['cm-handle-state': 'READY'])]
         when: 'cm handles are compared by state'
             def result = objectUnderTest.cmHandleHasState('some-cm-handle', cmHandleState)
         then: 'the returned result matches the expected result from the persistence service'
@@ -136,8 +137,8 @@ class CmHandleQueriesImplSpec extends Specification {
         given: 'a cm handle state to query'
             def cmHandleState = CmHandleState.READY
         and: 'cps data service returns a list of data nodes'
-            cpsDataPersistenceService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
-                '/dmi-registry/cm-handles[@id=\'some-cm-handle\']/state', OMIT_DESCENDANTS) >> new DataNode(leaves: ['cm-handle-state': 'READY'])
+            cpsDataPersistenceService.getDataNodes('NCMP-Admin', 'ncmp-dmi-registry',
+                '/dmi-registry/cm-handles[@id=\'some-cm-handle\']/state', OMIT_DESCENDANTS) >> [new DataNode(leaves: ['cm-handle-state': 'READY'])]
         when: 'cm handles are fetched by state and id'
             def result = objectUnderTest.getCmHandleState('some-cm-handle')
         then: 'the returned result is a list of data nodes returned by cps data service'
index 929ea84..4a98526 100644 (file)
@@ -2,6 +2,7 @@
  *  ============LICENSE_START=======================================================
  *  Copyright (C) 2022-2023 Nordix Foundation
  *  Modifications Copyright (C) 2022 Bell Canada
+ *  Modifications Copyright (C) 2023 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,7 +40,6 @@ import spock.lang.Specification
 import java.time.OffsetDateTime
 import java.time.ZoneOffset
 import java.time.format.DateTimeFormatter
-import java.util.stream.Collectors
 
 import static org.onap.cps.ncmp.api.impl.constants.DmiRegistryConstants.NO_TIMESTAMP
 import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
@@ -85,7 +85,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)
-            mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry', xpath, INCLUDE_ALL_DESCENDANTS) >> dataNode
+            mockCpsDataService.getDataNodes('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'
@@ -112,7 +112,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: [:])
-            mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry', xpath, INCLUDE_ALL_DESCENDANTS) >> dataNode
+            mockCpsDataService.getDataNodes('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'
@@ -126,7 +126,7 @@ class InventoryPersistenceImplSpec extends Specification {
     def "Retrieve multiple YangModelCmHandles"() {
         given: 'the cps data service returns 2 data nodes from the DMI registry'
             def dataNodes = [new DataNode(xpath: xpath), new DataNode(xpath: xpath2)]
-            mockCpsDataService.getDataNodes('NCMP-Admin', 'ncmp-dmi-registry', [xpath, xpath2] , INCLUDE_ALL_DESCENDANTS) >> dataNodes
+            mockCpsDataService.getDataNodesForMultipleXpaths('NCMP-Admin', 'ncmp-dmi-registry', [xpath, xpath2] , INCLUDE_ALL_DESCENDANTS) >> dataNodes
         when: 'retrieving the yang modelled cm handle'
             def results = objectUnderTest.getYangModelCmHandles([cmHandleId, cmHandleId2])
         then: 'verify both have returned and cmhandleIds are correct'
@@ -139,8 +139,8 @@ class InventoryPersistenceImplSpec extends Specification {
             def cmHandleId = 'Some-Cm-Handle'
             def dataNode = new DataNode(leaves: ['cm-handle-state': 'ADVISED'])
         and: 'cps data service returns a valid data node'
-            mockCpsDataService.getDataNode('NCMP-Admin', 'ncmp-dmi-registry',
-                    '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle\']/state', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode
+            mockCpsDataService.getDataNodes('NCMP-Admin', 'ncmp-dmi-registry',
+                    '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle\']/state', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> [dataNode]
         when: 'get cm handle state is invoked'
             def result = objectUnderTest.getCmHandleState(cmHandleId)
         then: 'result has returned the correct cm handle state'
@@ -251,7 +251,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 * mockCpsDataService.getDataNode('NCMP-Admin','ncmp-dmi-registry','sample xPath', INCLUDE_ALL_DESCENDANTS)
+            1 * mockCpsDataService.getDataNodes('NCMP-Admin','ncmp-dmi-registry','sample xPath', INCLUDE_ALL_DESCENDANTS)
     }
 
     def 'Get cmHandle data node'() {
@@ -260,7 +260,7 @@ 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 * mockCpsDataService.getDataNode('NCMP-Admin','ncmp-dmi-registry',expectedXPath, INCLUDE_ALL_DESCENDANTS)
+            1 * mockCpsDataService.getDataNodes('NCMP-Admin','ncmp-dmi-registry',expectedXPath, INCLUDE_ALL_DESCENDANTS)
     }
 
     def 'Get CM handles that has given module names'() {
index 61663ab..ad0c299 100644 (file)
@@ -46,4 +46,4 @@ nodeByDataspaceAndAnchor:
         $ref: 'components.yml#/components/responses/Forbidden'
       '500':
         $ref: 'components.yml#/components/responses/InternalServerError'
-    x-codegen-request-body-name: xpath
+    x-codegen-request-body-name: xpath
\ No newline at end of file
index 3a9c764..80cfb8c 100755 (executable)
@@ -3,7 +3,7 @@
  *  Copyright (C) 2020-2022 Bell Canada.
  *  Modifications Copyright (C) 2021 Pantheon.tech
  *  Modifications Copyright (C) 2021-2022 Nordix Foundation
- *  Modifications Copyright (C) 2023 TechMahindra Ltd.
+ *  Modifications Copyright (C) 2022-2023 TechMahindra Ltd.
  *  Modifications Copyright (C) 2022 Deutsche Telekom AG
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
@@ -26,6 +26,10 @@ package org.onap.cps.rest.controller;
 
 import java.time.OffsetDateTime;
 import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
 import javax.validation.ValidationException;
 import lombok.RequiredArgsConstructor;
 import org.apache.commons.lang3.StringUtils;
@@ -97,21 +101,27 @@ public class DataRestController implements CpsDataApi {
         final String anchorName, final String xpath, final Boolean includeDescendants) {
         final FetchDescendantsOption fetchDescendantsOption = Boolean.TRUE.equals(includeDescendants)
             ? FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS : FetchDescendantsOption.OMIT_DESCENDANTS;
-        final DataNode dataNode = cpsDataService.getDataNode(dataspaceName, anchorName, xpath,
-            fetchDescendantsOption);
-        final String prefix = prefixResolver.getPrefix(dataspaceName, anchorName, xpath);
+        final DataNode dataNode = cpsDataService.getDataNodes(dataspaceName, anchorName, xpath,
+            fetchDescendantsOption).iterator().next();
+        final String prefix = prefixResolver.getPrefix(dataspaceName, anchorName, dataNode.getXpath());
         return new ResponseEntity<>(DataMapUtils.toDataMapWithIdentifier(dataNode, prefix), HttpStatus.OK);
     }
 
     @Override
     public ResponseEntity<Object> getNodeByDataspaceAndAnchorV2(final String dataspaceName, final String anchorName,
-        final String xpath, final String fetchDescendantsOptionAsString) {
+                                                                final String xpath,
+                                                                final String fetchDescendantsOptionAsString) {
         final FetchDescendantsOption fetchDescendantsOption =
-            FetchDescendantsOption.getFetchDescendantsOption(fetchDescendantsOptionAsString);
-        final DataNode dataNode = cpsDataService.getDataNode(dataspaceName, anchorName, xpath,
-            fetchDescendantsOption);
-        final String prefix = prefixResolver.getPrefix(dataspaceName, anchorName, xpath);
-        return new ResponseEntity<>(DataMapUtils.toDataMapWithIdentifier(dataNode, prefix), HttpStatus.OK);
+                FetchDescendantsOption.getFetchDescendantsOption(fetchDescendantsOptionAsString);
+        final Collection<DataNode> dataNodes = cpsDataService.getDataNodes(dataspaceName, anchorName, xpath,
+                fetchDescendantsOption);
+        final List<Map<String, Object>> dataMaps = new ArrayList<>(dataNodes.size());
+        for (final DataNode dataNode: dataNodes) {
+            final String prefix = prefixResolver.getPrefix(dataspaceName, anchorName, dataNode.getXpath());
+            final Map<String, Object> dataMap = DataMapUtils.toDataMapWithIdentifier(dataNode, prefix);
+            dataMaps.add(dataMap);
+        }
+        return new ResponseEntity<>(jsonObjectMapper.asJsonString(dataMaps), HttpStatus.OK);
     }
 
     @Override
@@ -162,5 +172,4 @@ public class DataRestController implements CpsDataApi {
                 String.format("observed-timestamp must be in '%s' format", ISO_TIMESTAMP_FORMAT));
         }
     }
-
 }
index 16d106b..d88a9cd 100755 (executable)
@@ -25,7 +25,9 @@
 package org.onap.cps.rest.controller
 
 import com.fasterxml.jackson.databind.ObjectMapper
+import groovy.json.JsonSlurper
 import org.onap.cps.api.CpsDataService
+import org.onap.cps.spi.FetchDescendantsOption
 import org.onap.cps.spi.model.DataNode
 import org.onap.cps.spi.model.DataNodeBuilder
 import org.onap.cps.utils.ContentType
@@ -68,7 +70,7 @@ class DataRestControllerSpec extends Specification {
     @Value('${rest.api.cps-base-path}')
     def basePath
 
-    def dataNodeBaseEndpoint
+    def dataNodeBaseEndpointV1
     def dataNodeBaseEndpointV2
     def dataspaceName = 'my_dataspace'
     def anchorName = 'my_anchor'
@@ -87,21 +89,25 @@ class DataRestControllerSpec extends Specification {
     def expectedXmlData = '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n<bookstore xmlns="org:onap:ccsdk:sample">\n</bookstore>'
 
     @Shared
-    static DataNode dataNodeWithLeavesNoChildren = new DataNodeBuilder().withXpath('/xpath')
+    static DataNode dataNodeWithLeavesNoChildren = new DataNodeBuilder().withXpath('/parent-1')
         .withLeaves([leaf: 'value', leafList: ['leaveListElement1', 'leaveListElement2']]).build()
 
+    @Shared
+    static DataNode dataNodeWithLeavesNoChildren2 = new DataNodeBuilder().withXpath('/parent-2')
+        .withLeaves([leaf: 'value']).build()
+
     @Shared
     static DataNode dataNodeWithChild = new DataNodeBuilder().withXpath('/parent')
         .withChildDataNodes([new DataNodeBuilder().withXpath("/parent/child").build()]).build()
 
     def setup() {
-        dataNodeBaseEndpoint = "$basePath/v1/dataspaces/$dataspaceName"
+        dataNodeBaseEndpointV1 = "$basePath/v1/dataspaces/$dataspaceName"
         dataNodeBaseEndpointV2 = "$basePath/v2/dataspaces/$dataspaceName"
     }
 
     def 'Create a node: #scenario.'() {
         given: 'endpoint to create a node'
-            def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
+            def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes"
         when: 'post is invoked with datanode endpoint and json'
             def response =
                 mvc.perform(
@@ -124,7 +130,7 @@ class DataRestControllerSpec extends Specification {
 
     def 'Create a node with observed-timestamp'() {
         given: 'endpoint to create a node'
-            def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
+            def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes"
         when: 'post is invoked with datanode endpoint and json'
             def response =
                 mvc.perform(
@@ -148,7 +154,7 @@ class DataRestControllerSpec extends Specification {
 
     def 'Create a child node #scenario'() {
         given: 'endpoint to create a node'
-            def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
+            def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes"
         and: 'parent node xpath'
             def parentNodeXpath = 'some xpath'
         when: 'post is invoked with datanode endpoint and json'
@@ -177,7 +183,7 @@ class DataRestControllerSpec extends Specification {
         given: 'parent node xpath '
             def parentNodeXpath = 'parent node xpath'
         when: 'list-node endpoint is invoked with post (create) operation'
-            def postRequestBuilder = post("$dataNodeBaseEndpoint/anchors/$anchorName/list-nodes")
+            def postRequestBuilder = post("$dataNodeBaseEndpointV1/anchors/$anchorName/list-nodes")
                 .contentType(MediaType.APPLICATION_JSON)
                 .param('xpath', parentNodeXpath)
                 .content(requestBodyJson)
@@ -198,9 +204,9 @@ class DataRestControllerSpec extends Specification {
 
     def 'Get data node with leaves'() {
         given: 'the service returns data node leaves'
-            def xpath = 'xpath'
-            def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/node"
-            mockCpsDataService.getDataNode(dataspaceName, anchorName, xpath, OMIT_DESCENDANTS) >> dataNodeWithLeavesNoChildren
+            def xpath = 'parent-1'
+            def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/node"
+            mockCpsDataService.getDataNodes(dataspaceName, anchorName, xpath, OMIT_DESCENDANTS) >> [dataNodeWithLeavesNoChildren]
         when: 'get request is performed through REST API'
             def response =
                 mvc.perform(get(endpoint).param('xpath', xpath))
@@ -208,7 +214,7 @@ class DataRestControllerSpec extends Specification {
         then: 'a success response is returned'
             response.status == HttpStatus.OK.value()
         then: 'the response contains the the datanode in json format'
-            response.getContentAsString() == '{"xpath":{"leaf":"value","leafList":["leaveListElement1","leaveListElement2"]}}'
+            response.getContentAsString() == '{"parent-1":{"leaf":"value","leafList":["leaveListElement1","leaveListElement2"]}}'
         and: 'response contains expected leaf and value'
             response.contentAsString.contains('"leaf":"value"')
         and: 'response contains expected leaf-list and values'
@@ -218,8 +224,8 @@ class DataRestControllerSpec extends Specification {
     def 'Get data node with #scenario.'() {
         given: 'the service returns data node with #scenario'
             def xpath = 'some xPath'
-            def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/node"
-            mockCpsDataService.getDataNode(dataspaceName, anchorName, xpath, expectedCpsDataServiceOption) >> dataNode
+            def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/node"
+            mockCpsDataService.getDataNodes(dataspaceName, anchorName, xpath, expectedCpsDataServiceOption) >> [dataNode]
         when: 'get request is performed through REST API'
             def response =
                 mvc.perform(
@@ -235,25 +241,68 @@ class DataRestControllerSpec extends Specification {
             response.contentAsString.contains('"child"') == expectChildInResponse
         where:
             scenario                    | dataNode                     | includeDescendantsOption || expectedCpsDataServiceOption | expectChildInResponse | expectedRootidentifier
-            'no descendants by default' | dataNodeWithLeavesNoChildren | ''                       || OMIT_DESCENDANTS             | false                 | 'xpath'
-            'no descendant explicitly'  | dataNodeWithLeavesNoChildren | 'false'                  || OMIT_DESCENDANTS             | false                 | 'xpath'
+            'no descendants by default' | dataNodeWithLeavesNoChildren | ''                       || OMIT_DESCENDANTS             | false                 | 'parent-1'
+            'no descendant explicitly'  | dataNodeWithLeavesNoChildren | 'false'                  || OMIT_DESCENDANTS             | false                 | 'parent-1'
             'with descendants'          | dataNodeWithChild            | 'true'                   || INCLUDE_ALL_DESCENDANTS      | true                  | 'parent'
     }
 
+    def 'Get all the data trees as json array with root node xPath using V2'() {
+        given: 'the service returns all data node leaves'
+            def xpath = '/'
+            def endpoint = "$dataNodeBaseEndpointV2/anchors/$anchorName/node"
+            mockCpsDataService.getDataNodes(dataspaceName, anchorName, xpath, OMIT_DESCENDANTS) >> [dataNodeWithLeavesNoChildren, dataNodeWithLeavesNoChildren2]
+        when: 'V2 of get request is performed through REST API'
+            def response =
+                mvc.perform(get(endpoint).param('xpath', xpath))
+                    .andReturn().response
+        then: 'a success response is returned'
+            response.status == HttpStatus.OK.value()
+        and: 'the response contains the datanode in json array format'
+            response.getContentAsString() == '[{"parent-1":{"leaf":"value","leafList":["leaveListElement1","leaveListElement2"]}},' +
+                '{"parent-2":{"leaf":"value"}}]'
+        and: 'the json array contains expected number of data trees'
+            def numberOfDataTrees = new JsonSlurper().parseText(response.getContentAsString()).iterator().size()
+            assert numberOfDataTrees == 2
+    }
+
+    def 'Get data node with #scenario using V2.'() {
+        given: 'the service returns data nodes with #scenario'
+            def xpath = 'some xPath'
+            def endpoint = "$dataNodeBaseEndpointV2/anchors/$anchorName/node"
+            mockCpsDataService.getDataNodes(dataspaceName, anchorName, xpath, expectedCpsDataServiceOption) >> [dataNode]
+        when: 'V2 of get request is performed through REST API'
+            def response =
+                mvc.perform(
+                    get(endpoint)
+                        .param('xpath', xpath)
+                        .param('descendants', includeDescendantsOption))
+                    .andReturn().response
+        then: 'a success response is returned'
+            response.status == HttpStatus.OK.value()
+        and: 'the response contains the root node identifier: #expectedRootidentifier'
+            response.contentAsString.contains(expectedRootidentifier)
+        and: 'the response contains child is #expectChildInResponse'
+            response.contentAsString.contains('"child"') == expectChildInResponse
+        where:
+            scenario                    | dataNode                     | includeDescendantsOption || expectedCpsDataServiceOption | expectChildInResponse | expectedRootidentifier
+            'no descendants by default' | dataNodeWithLeavesNoChildren | ''                       || OMIT_DESCENDANTS             | false                 | 'parent-1'
+            'no descendant explicitly'  | dataNodeWithLeavesNoChildren | '0'                      || OMIT_DESCENDANTS             | false                 | 'parent-1'
+            'with descendants'          | dataNodeWithChild            | '-1'                     || INCLUDE_ALL_DESCENDANTS      | true                  | 'parent'
+    }
 
     def 'Get data node using v2 api'() {
         given: 'the service returns data node'
             def xpath = 'some xPath'
             def endpoint = "$dataNodeBaseEndpointV2/anchors/$anchorName/node"
-            mockCpsDataService.getDataNode(dataspaceName, anchorName, xpath, {descendantsOption -> {
-                assert descendantsOption.depth == 2}}) >> dataNodeWithChild
+            mockCpsDataService.getDataNodes(dataspaceName, anchorName, xpath, { descendantsOption -> {
+                assert descendantsOption.depth == 2}} as FetchDescendantsOption) >> [dataNodeWithChild]
         when: 'get request is performed through REST API'
             def response =
                 mvc.perform(
-                        get(endpoint)
-                                .param('xpath', xpath)
-                                .param('descendants', '2'))
-                        .andReturn().response
+                    get(endpoint)
+                        .param('xpath', xpath)
+                        .param('descendants', '2'))
+                    .andReturn().response
         then: 'a success response is returned'
             assert response.status == HttpStatus.OK.value()
         and: 'the response contains the root node identifier'
@@ -264,7 +313,7 @@ class DataRestControllerSpec extends Specification {
 
     def 'Update data node leaves: #scenario.'() {
         given: 'endpoint to update a node '
-            def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
+            def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes"
         when: 'patch request is performed'
             def response =
                 mvc.perform(
@@ -286,7 +335,7 @@ class DataRestControllerSpec extends Specification {
 
     def 'Update data node leaves with observedTimestamp'() {
         given: 'endpoint to update a node leaves '
-            def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
+            def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes"
         when: 'patch request is performed'
             def response =
                 mvc.perform(
@@ -309,7 +358,7 @@ class DataRestControllerSpec extends Specification {
 
     def 'Replace data node tree: #scenario.'() {
         given: 'endpoint to replace node'
-            def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
+            def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes"
         when: 'put request is performed'
             def response =
                 mvc.perform(
@@ -331,7 +380,7 @@ class DataRestControllerSpec extends Specification {
 
     def 'Update data node and descendants with observedTimestamp.'() {
         given: 'endpoint to replace node'
-            def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
+            def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes"
         when: 'put request is performed'
             def response =
                 mvc.perform(
@@ -354,7 +403,7 @@ class DataRestControllerSpec extends Specification {
 
     def 'Replace list content #scenario.'() {
         when: 'list-nodes endpoint is invoked with put (update) operation'
-            def putRequestBuilder = put("$dataNodeBaseEndpoint/anchors/$anchorName/list-nodes")
+            def putRequestBuilder = put("$dataNodeBaseEndpointV1/anchors/$anchorName/list-nodes")
                 .contentType(MediaType.APPLICATION_JSON)
                 .param('xpath', 'parent xpath')
                 .content(requestBodyJson)
@@ -375,7 +424,7 @@ class DataRestControllerSpec extends Specification {
 
     def 'Delete list element #scenario.'() {
         when: 'list-nodes endpoint is invoked with delete operation'
-            def deleteRequestBuilder = delete("$dataNodeBaseEndpoint/anchors/$anchorName/list-nodes")
+            def deleteRequestBuilder = delete("$dataNodeBaseEndpointV1/anchors/$anchorName/list-nodes")
                 .param('xpath', 'list element xpath')
             if (observedTimestamp != null)
                 deleteRequestBuilder.param('observed-timestamp', observedTimestamp)
@@ -396,7 +445,7 @@ class DataRestControllerSpec extends Specification {
         given: 'data node xpath'
             def dataNodeXpath = '/dataNodeXpath'
         when: 'delete data node endpoint is invoked'
-            def deleteDataNodeRequest = delete( "$dataNodeBaseEndpoint/anchors/$anchorName/nodes")
+            def deleteDataNodeRequest = delete( "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes")
                 .param('xpath', dataNodeXpath)
         and: 'observed timestamp is added to the parameters'
             if (observedTimestamp != null)
index 5b310ef..46439fd 100644 (file)
@@ -3,7 +3,7 @@
  *  Copyright (C) 2021-2023 Nordix Foundation
  *  Modifications Copyright (C) 2021 Pantheon.tech
  *  Modifications Copyright (C) 2020-2022 Bell Canada.
- *  Modifications Copyright (C) 2022 TechMahindra Ltd.
+ *  Modifications Copyright (C) 2022-2023 TechMahindra Ltd.
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -249,17 +249,22 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
     }
 
     @Override
-    public DataNode getDataNode(final String dataspaceName, final String anchorName, final String xpath,
-                                final FetchDescendantsOption fetchDescendantsOption) {
-        final FragmentEntity fragmentEntity = getFragmentByXpath(dataspaceName, anchorName, xpath,
-            fetchDescendantsOption);
-        return toDataNode(fragmentEntity, fetchDescendantsOption);
+    public Collection<DataNode> getDataNodes(final String dataspaceName, final String anchorName,
+                                             final String xpath,
+                                             final FetchDescendantsOption fetchDescendantsOption) {
+        final String targetXpath = isRootXpath(xpath) ? xpath : CpsPathUtil.getNormalizedXpath(xpath);
+        final Collection<DataNode> dataNodes = getDataNodesForMultipleXpaths(dataspaceName, anchorName,
+                Collections.singletonList(targetXpath), fetchDescendantsOption);
+        if (dataNodes.isEmpty()) {
+            throw new DataNodeNotFoundException(dataspaceName, anchorName, xpath);
+        }
+        return dataNodes;
     }
 
     @Override
-    public Collection<DataNode> getDataNodes(final String dataspaceName, final String anchorName,
-                                             final Collection<String> xpaths,
-                                             final FetchDescendantsOption fetchDescendantsOption) {
+    public Collection<DataNode> getDataNodesForMultipleXpaths(final String dataspaceName, final String anchorName,
+                                                              final Collection<String> xpaths,
+                                                              final FetchDescendantsOption fetchDescendantsOption) {
         final DataspaceEntity dataspaceEntity = dataspaceRepository.getByName(dataspaceName);
         final AnchorEntity anchorEntity = anchorRepository.getByDataspaceAndName(dataspaceEntity, anchorName);
 
@@ -271,7 +276,7 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
             try {
                 normalizedXpaths.add(CpsPathUtil.getNormalizedXpath(xpath));
             } catch (final PathParsingException e) {
-                log.warn("Error parsing xpath \"{}\" in getDataNodes: {}", xpath, e.getMessage());
+                log.warn("Error parsing xpath \"{}\" in getDataNodesForMultipleXpaths: {}", xpath, e.getMessage());
             }
         }
         final Collection<FragmentEntity> fragmentEntities =
index e4c5529..336d603 100755 (executable)
@@ -3,7 +3,7 @@
  *  Copyright (C) 2021-2023 Nordix Foundation
  *  Modifications Copyright (C) 2021 Pantheon.tech
  *  Modifications Copyright (C) 2021-2022 Bell Canada.
- *  Modifications Copyright (C) 2022 TechMahindra Ltd.
+ *  Modifications Copyright (C) 2022-2023 TechMahindra Ltd.
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -41,6 +41,7 @@ import org.springframework.beans.factory.annotation.Autowired
 import org.springframework.test.context.jdbc.Sql
 
 import javax.validation.ConstraintViolationException
+import java.nio.file.Path
 
 import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
 import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
@@ -82,15 +83,13 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
     ]
 
     @Sql([CLEAR_DATA, SET_DATA])
-    def 'Get existing datanode with descendants.'() {
-        when: 'the node is retrieved by its xpath'
-            def dataNode = objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_NAME1, '/parent-1', INCLUDE_ALL_DESCENDANTS)
-        then: 'the path and prefix are populated correctly'
-            assert dataNode.xpath == '/parent-1'
-        and: 'dataNode has no prefix (to be addressed by CPS-1301'
-            assert dataNode.moduleNamePrefix == null
-        and: 'the child node has the correct path'
-            assert dataNode.childDataNodes[0].xpath == '/parent-1/child-1'
+    def 'Get all datanodes with descendants .'() {
+        when: 'data nodes are retrieved by their xpath'
+            def dataNodes = objectUnderTest.getDataNodesForMultipleXpaths(DATASPACE_NAME, ANCHOR_NAME1, ['/parent-1'], INCLUDE_ALL_DESCENDANTS)
+        then: 'same data nodes are returned by getDataNodesForMultipleXpaths method'
+            assert objectUnderTest.getDataNodes(DATASPACE_NAME, ANCHOR_NAME1, '/parent-1', INCLUDE_ALL_DESCENDANTS) == dataNodes
+        and: 'the dataNodes have no prefix (to be addressed by CPS-1301)'
+            assert dataNodes[0].moduleNamePrefix == null
     }
 
     @Sql([CLEAR_DATA, SET_DATA])
@@ -102,11 +101,11 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
             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
+            def dataNode = objectUnderTest.getDataNodes(DATASPACE_NAME, ANCHOR_NAME1, parentXpath, INCLUDE_ALL_DESCENDANTS)
+            assert dataNode[0].xpath == parentXpath
         and: 'it has the correct child'
-            assert dataNode.childDataNodes.size() == 1
-            def childDataNode = dataNode.childDataNodes[0]
+            assert dataNode[0].childDataNodes.size() == 1
+            def childDataNode = dataNode[0].childDataNodes[0]
             assert childDataNode.xpath == childXpath
         and: 'and its grandchild'
             assert childDataNode.childDataNodes.size() == 1
@@ -236,18 +235,19 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
     }
 
     @Sql([CLEAR_DATA, SET_DATA])
-    def 'Get data node by xpath without descendants.'() {
-        when: 'data node is requested'
-            def result = objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_HAVING_SINGLE_TOP_LEVEL_FRAGMENT,
-                    inputXPath, OMIT_DESCENDANTS)
-        then: 'data node is returned with no descendants'
-            assert result.xpath == XPATH_DATA_NODE_WITH_LEAVES
-        and: 'expected leaves'
-            assert result.childDataNodes.size() == 0
-            assertLeavesMaps(result.leaves, expectedLeavesByXpathMap[XPATH_DATA_NODE_WITH_LEAVES])
-        where: 'the following data is used'
+    def 'Get all data nodes by single xpath without descendants : #scenario'() {
+        when: 'data nodes are requested'
+            def result = objectUnderTest.getDataNodesForMultipleXpaths(DATASPACE_NAME, ANCHOR_WITH_MULTIPLE_TOP_LEVEL_FRAGMENTS,
+                [inputXPath], OMIT_DESCENDANTS)
+        then: 'data nodes under root are returned'
+            assert result.childDataNodes.size() == 2
+        and: 'no descendants of parent nodes are returned'
+            result.each {assert it.childDataNodes.size() == 0}
+        and: 'same data nodes are returned when V2 of get Data Nodes API is executed'
+            assert objectUnderTest.getDataNodes(DATASPACE_NAME, ANCHOR_WITH_MULTIPLE_TOP_LEVEL_FRAGMENTS,
+                inputXPath, OMIT_DESCENDANTS) == result
+        where: 'the following xpath is used'
             scenario      | inputXPath
-            'some xpath'  | '/parent-207'
             'root xpath'  | '/'
             'empty xpath' | ''
     }
@@ -255,51 +255,50 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
     @Sql([CLEAR_DATA, SET_DATA])
     def 'Cps Path query with syntax error throws a CPS Path Exception.'() {
         when: 'trying to execute a query with a syntax (parsing) error'
-            objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, 'invalid-cps-path/child' , OMIT_DESCENDANTS)
+            objectUnderTest.getDataNodes(DATASPACE_NAME, ANCHOR_FOR_DATA_NODES_WITH_LEAVES, 'invalid-cps-path/child' , OMIT_DESCENDANTS)
         then: 'exception is thrown'
-            def exceptionThrown = thrown(CpsPathException)
-            assert exceptionThrown.getDetails().contains('failed to parse at line 1 due to extraneous input \'invalid-cps-path\' expecting \'/\'')
+            def exceptionThrown = thrown(PathParsingException)
+            assert exceptionThrown.getMessage().contains('failed to parse at line 1 due to extraneous input \'invalid-cps-path\' expecting \'/\'')
     }
 
     @Sql([CLEAR_DATA, SET_DATA])
-    def 'Get data node by xpath with all descendants.'() {
-        when: 'data node is requested with all descendants'
-            def result = objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_HAVING_SINGLE_TOP_LEVEL_FRAGMENT,
-                    inputXPath, INCLUDE_ALL_DESCENDANTS)
-            def mappedResult = treeToFlatMapByXpath(new HashMap<>(), result)
-        then: 'data node is returned with all the descendants populated'
-            assert mappedResult.size() == 4
+    def 'Get all data nodes by single xpath with all descendants : #scenario'() {
+        when: 'data nodes are requested with all descendants'
+            def result = objectUnderTest.getDataNodesForMultipleXpaths(DATASPACE_NAME, ANCHOR_WITH_MULTIPLE_TOP_LEVEL_FRAGMENTS,
+                [inputXPath], INCLUDE_ALL_DESCENDANTS)
+            def mappedResult = multipleTreesToFlatMapByXpath(new HashMap<>(), result)
+        then: 'data nodes are returned with all the descendants populated'
+            assert mappedResult.size() == 8
             assert result.childDataNodes.size() == 2
-            assert mappedResult.get('/parent-207/child-001').childDataNodes.size() == 0
-            assert mappedResult.get('/parent-207/child-002').childDataNodes.size() == 1
-        and: 'extracted leaves maps are matching expected'
-            mappedResult.forEach(
-                    (xPath, dataNode) -> assertLeavesMaps(dataNode.leaves, expectedLeavesByXpathMap[xPath]))
+            assert mappedResult.get('/parent-208/child-001').childDataNodes.size() == 0
+            assert mappedResult.get('/parent-208/child-002').childDataNodes.size() == 1
+            assert mappedResult.get('/parent-209/child-001').childDataNodes.size() == 0
+            assert mappedResult.get('/parent-209/child-002').childDataNodes.size() == 1
+        and: 'same data nodes are returned when V2 of Get Data Nodes API is executed'
+            assert objectUnderTest.getDataNodes(DATASPACE_NAME, ANCHOR_WITH_MULTIPLE_TOP_LEVEL_FRAGMENTS,
+                inputXPath, INCLUDE_ALL_DESCENDANTS) == result
         where: 'the following data is used'
             scenario      | inputXPath
-            'some xpath'  | '/parent-207'
             'root xpath'  | '/'
             'empty xpath' | ''
     }
 
     @Sql([CLEAR_DATA, SET_DATA])
-    def 'Get data node error scenario: #scenario.'() {
-        when: 'attempt to get data node with #scenario'
-            objectUnderTest.getDataNode(dataspaceName, anchorName, xpath, OMIT_DESCENDANTS)
-        then: 'a #expectedException is thrown'
+    def 'Get data nodes error scenario : #scenario.'() {
+        when: 'attempt to get data nodes with #scenario'
+            objectUnderTest.getDataNodes(dataspaceName, anchorName, xpath, OMIT_DESCENDANTS)
+        then: 'an #expectedException is thrown'
             thrown(expectedException)
         where: 'the following data is used'
-            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
-            'invalid xpath'          | DATASPACE_NAME | ANCHOR_FOR_DATA_NODES_WITH_LEAVES | 'INVALID XPATH' || CpsPathException
+            scenario             | dataspaceName  | anchorName                        | xpath           || expectedException
+            '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' || PathParsingException
     }
 
     @Sql([CLEAR_DATA, SET_DATA])
-    def 'Get multiple data nodes by xpath.'() {
+    def 'Get data nodes for multiple xpaths.'() {
         when: 'fetch #scenario.'
-            def results = objectUnderTest.getDataNodes(DATASPACE_NAME, ANCHOR_NAME3, inputXpaths, OMIT_DESCENDANTS)
+            def results = objectUnderTest.getDataNodesForMultipleXpaths(DATASPACE_NAME, ANCHOR_NAME3, inputXpaths, OMIT_DESCENDANTS)
         then: 'the expected number of data nodes are returned'
             assert results.size() == expectedResultSize
         where: 'following parameters were used'
@@ -323,9 +322,9 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
     }
 
     @Sql([CLEAR_DATA, SET_DATA])
-    def 'Get multiple data nodes error scenario: #scenario.'() {
+    def 'Get data nodes for collection of xpath error scenario : #scenario.'() {
         when: 'attempt to get data nodes with #scenario'
-            objectUnderTest.getDataNodes(dataspaceName, anchorName, ['/not-relevant'], OMIT_DESCENDANTS)
+            objectUnderTest.getDataNodesForMultipleXpaths(dataspaceName, anchorName, ['/not-relevant'], OMIT_DESCENDANTS)
         then: 'a #expectedException is thrown'
             thrown(expectedException)
         where: 'the following data is used'
@@ -599,11 +598,11 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
             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()
+            def numberOfChildrenBeforeDelete = objectUnderTest.getDataNodes(DATASPACE_NAME, ANCHOR_NAME3, pathToParentOfDeletedNode, INCLUDE_ALL_DESCENDANTS)[0].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()
+            def numberOfChildrenAfterDelete = objectUnderTest.getDataNodes(DATASPACE_NAME, ANCHOR_NAME3, pathToParentOfDeletedNode, INCLUDE_ALL_DESCENDANTS)[0].childDataNodes.size()
             assert numberOfChildrenAfterDelete == numberOfChildrenBeforeDelete - 1
         where:
             scenario                | deleteTarget              | pathToParentOfDeletedNode
@@ -634,13 +633,13 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
         and: 'data nodes are deleted'
             objectUnderTest.deleteDataNode(DATASPACE_NAME, ANCHOR_NAME3, xpathForDeletion)
         when: 'verify data nodes are removed'
-            objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_NAME3, xpathForDeletion, INCLUDE_ALL_DESCENDANTS)
+            objectUnderTest.getDataNodes(DATASPACE_NAME, ANCHOR_NAME3, xpathForDeletion, INCLUDE_ALL_DESCENDANTS)
         then:
             thrown(DataNodeNotFoundException)
         and: 'some related object is not deleted'
             if (xpathSurvivor!=null) {
-                dataNode = objectUnderTest.getDataNode(DATASPACE_NAME, ANCHOR_NAME3, xpathSurvivor, INCLUDE_ALL_DESCENDANTS)
-                assert dataNode.xpath == xpathSurvivor
+                dataNode = objectUnderTest.getDataNodes(DATASPACE_NAME, ANCHOR_NAME3, xpathSurvivor, INCLUDE_ALL_DESCENDANTS)
+                assert dataNode[0].xpath == xpathSurvivor
             }
         where: 'following parameters were used'
             scenario                               | xpathForDeletion                                  || xpathSurvivor
@@ -711,6 +710,15 @@ class CpsDataPersistenceServiceIntegrationSpec extends CpsPersistenceSpecBase {
         return flatMap
     }
 
+    def static multipleTreesToFlatMapByXpath(Map<String, DataNode> flatMap, Collection<DataNode> dataNodeTrees) {
+        for (DataNode dataNodeTree: dataNodeTrees){
+            flatMap.put(dataNodeTree.xpath, dataNodeTree)
+            dataNodeTree.childDataNodes
+                .forEach(childDataNode -> multipleTreesToFlatMapByXpath(flatMap, [childDataNode]))
+        }
+        return flatMap
+    }
+
     def keysToXpaths(parent, Collection keys) {
         return keys.collect { "${parent}/child-list[@key='${it}']".toString() }
     }
index 5cabc85..ac66e8c 100644 (file)
@@ -25,7 +25,6 @@ import org.hibernate.StaleStateException
 import org.onap.cps.spi.FetchDescendantsOption
 import org.onap.cps.spi.entities.AnchorEntity
 import org.onap.cps.spi.entities.FragmentEntity
-import org.onap.cps.spi.entities.FragmentExtract
 import org.onap.cps.spi.exceptions.ConcurrencyException
 import org.onap.cps.spi.exceptions.DataValidationException
 import org.onap.cps.spi.model.DataNode
@@ -112,10 +111,10 @@ class CpsDataPersistenceServiceSpec extends Specification {
         given: 'the db has a fragment with an attribute property JSON value of #scenario'
             mockFragmentWithJson("{\"some attribute\": ${dataString}}")
         when: 'getting the data node represented by this fragment'
-            def dataNode = objectUnderTest.getDataNode('my-dataspace', 'my-anchor',
+            def dataNode = objectUnderTest.getDataNodes('my-dataspace', 'my-anchor',
                     '/parent-01', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS)
         then: 'the leaf is of the correct value and data type'
-            def attributeValue = dataNode.leaves.get('some attribute')
+            def attributeValue = dataNode[0].leaves.get('some attribute')
             assert attributeValue == expectedValue
             assert attributeValue.class == expectedDataClass
         where: 'the following Data Type is passed'
@@ -136,7 +135,7 @@ class CpsDataPersistenceServiceSpec extends Specification {
         given: 'a fragment with invalid JSON'
             mockFragmentWithJson('{invalid json')
         when: 'getting the data node represented by this fragment'
-            objectUnderTest.getDataNode('my-dataspace', 'my-anchor',
+            objectUnderTest.getDataNodes('my-dataspace', 'my-anchor',
                     '/parent-01', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS)
         then: 'a data validation exception is thrown'
             thrown(DataValidationException)
@@ -151,7 +150,7 @@ class CpsDataPersistenceServiceSpec extends Specification {
             def fragmentEntity2 = new FragmentEntity(xpath: '/xpath2', childFragments: [])
             mockFragmentRepository.findByAnchorAndMultipleCpsPaths(123, ['/xpath1', '/xpath2'] as Set<String>) >> [fragmentEntity1, fragmentEntity2]
         when: 'getting data nodes for 2 xpaths'
-            def result = objectUnderTest.getDataNodes('some-dataspace', 'some-anchor', ['/xpath1', '/xpath2'], FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS)
+            def result = objectUnderTest.getDataNodesForMultipleXpaths('some-dataspace', 'some-anchor', ['/xpath1', '/xpath2'], FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS)
         then: '2 data nodes are returned'
             assert result.size() == 2
     }
@@ -243,10 +242,8 @@ class CpsDataPersistenceServiceSpec extends Specification {
     def mockFragmentWithJson(json) {
         def anchorEntity = new AnchorEntity(id:123)
         mockAnchorRepository.getByDataspaceAndName(*_) >> anchorEntity
-        def mockFragmentExtract = Mock(FragmentExtract)
-        mockFragmentExtract.getId() >> 456
-        mockFragmentExtract.getAttributes() >> json
-        mockFragmentRepository.findByAnchorIdAndParentXpath(*_) >> [mockFragmentExtract]
+        def fragmentEntity = new FragmentEntity(xpath: '/parent-01', childFragments: [], attributes: json)
+        mockFragmentRepository.findByAnchorAndMultipleCpsPaths(123, ['/parent-01'] as Set<String>) >> [fragmentEntity]
     }
 
 }
index 1ecad4e..30ff11b 100644 (file)
@@ -3,6 +3,7 @@
  *  Copyright (C) 2021-2022 Nordix Foundation
  *  Modifications Copyright (C) 2021 Pantheon.tech
  *  Modifications Copyright (C) 2021 Bell Canada.
+ *  Modifications Copyright (C) 2023 TechMahindra Ltd.
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the 'License');
  *  you may not use this file except in compliance with the License.
@@ -60,13 +61,14 @@ class CpsPersistenceSpecBase extends Specification {
 
     static final String CLEAR_DATA = '/data/clear-all.sql'
 
-    static final String DATASPACE_NAME = 'DATASPACE-001'
-    static final String SCHEMA_SET_NAME1 = 'SCHEMA-SET-001'
-    static final String SCHEMA_SET_NAME2 = 'SCHEMA-SET-002'
-    static final String ANCHOR_NAME1 = 'ANCHOR-001'
-    static final String ANCHOR_NAME2 = 'ANCHOR-002'
-    static final String ANCHOR_NAME3 = 'ANCHOR-003'
-    static final String ANCHOR_FOR_DATA_NODES_WITH_LEAVES = 'ANCHOR-003'
-    static final String ANCHOR_FOR_SHOP_EXAMPLE = 'ANCHOR-004'
-    static final String ANCHOR_HAVING_SINGLE_TOP_LEVEL_FRAGMENT = 'ANCHOR-005'
+    static def DATASPACE_NAME = 'DATASPACE-001'
+    static def SCHEMA_SET_NAME1 = 'SCHEMA-SET-001'
+    static def SCHEMA_SET_NAME2 = 'SCHEMA-SET-002'
+    static def ANCHOR_NAME1 = 'ANCHOR-001'
+    static def ANCHOR_NAME2 = 'ANCHOR-002'
+    static def ANCHOR_NAME3 = 'ANCHOR-003'
+    static def ANCHOR_FOR_DATA_NODES_WITH_LEAVES = 'ANCHOR-003'
+    static def ANCHOR_FOR_SHOP_EXAMPLE = 'ANCHOR-004'
+    static def ANCHOR_HAVING_SINGLE_TOP_LEVEL_FRAGMENT = 'ANCHOR-005'
+    static def ANCHOR_WITH_MULTIPLE_TOP_LEVEL_FRAGMENTS = 'ANCHOR-006'
 }
index 0c4f5ec..246fd80 100644 (file)
@@ -64,13 +64,13 @@ class CpsDataPersistenceServicePerfTest extends CpsPersistencePerfSpecBase {
     def 'Get data node with many descendants by xpath #scenario'() {
         when: 'get parent is executed with all descendants'
             stopWatch.start()
-            def result = objectUnderTest.getDataNode(PERF_DATASPACE, PERF_ANCHOR, xpath, INCLUDE_ALL_DESCENDANTS)
+            def result = objectUnderTest.getDataNodes(PERF_DATASPACE, PERF_ANCHOR, xpath, INCLUDE_ALL_DESCENDANTS)
             stopWatch.stop()
             def readDurationInMillis = stopWatch.getTotalTimeMillis()
         then: 'read duration is under 500 milliseconds'
             recordAndAssertPerformance("Get ${scenario}", 500, readDurationInMillis)
         and: 'data node is returned with all the descendants populated'
-            assert countDataNodes(result) == TOTAL_NUMBER_OF_NODES
+            assert countDataNodes(result[0]) == TOTAL_NUMBER_OF_NODES
         where: 'the following xPaths are used'
             scenario || xpath
             'parent' || PERF_TEST_PARENT
@@ -93,7 +93,7 @@ class CpsDataPersistenceServicePerfTest extends CpsPersistencePerfSpecBase {
         when: 'we query for all grandchildren (except 1 for fun) with the new native method'
             xpathsToAllGrandChildren.remove(0)
             stopWatch.start()
-            def result = objectUnderTest.getDataNodes(PERF_DATASPACE, PERF_ANCHOR, xpathsToAllGrandChildren, INCLUDE_ALL_DESCENDANTS)
+            def result = objectUnderTest.getDataNodesForMultipleXpaths(PERF_DATASPACE, PERF_ANCHOR, xpathsToAllGrandChildren, INCLUDE_ALL_DESCENDANTS)
             stopWatch.stop()
             def readDurationInMillis = stopWatch.getTotalTimeMillis()
         then: 'the returned number of entities equal to the number of children * number of grandchildren'
index ad463cf..caafcd3 100755 (executable)
@@ -52,7 +52,8 @@ INSERT INTO ANCHOR (ID, NAME, DATASPACE_ID, SCHEMA_SET_ID) VALUES
     (3001, 'ANCHOR-001', 1001, 2001),
     (3003, 'ANCHOR-003', 1001, 2001),
     (3004, 'ncmp-dmi-registry', 1002, 2001),
-    (3005, 'ANCHOR-005', 1001, 2001);
+    (3005, 'ANCHOR-005', 1001, 2001),
+    (3006, 'ANCHOR-006', 1001, 2001);
 
 INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH) VALUES
     (4001, 1001, 3001, null, '/parent-1'),
@@ -68,6 +69,16 @@ INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH, ATTRIBUTES)
     (5011, 1001, 3005, 5009, '/parent-207/child-002', '{"second-child-leaf": "second-child-leaf value"}'),
     (5012, 1001, 3005, 5011, '/parent-207/child-002/grand-child', '{"grand-child-leaf": "grand-child-leaf value"}');
 
+INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH, ATTRIBUTES) VALUES
+    (5013, 1001, 3006, null, '/parent-208', '{"parent-leaf-1": "parent-leaf value-1"}'),
+    (5014, 1001, 3006, 5013, '/parent-208/child-001', '{"first-child-leaf": "first-child-leaf value"}'),
+    (5015, 1001, 3006, 5013, '/parent-208/child-002', '{"second-child-leaf": "second-child-leaf value"}'),
+    (5016, 1001, 3006, 5015, '/parent-208/child-002/grand-child', '{"grand-child-leaf": "grand-child-leaf value"}'),
+    (5017, 1001, 3006, null, '/parent-209', '{"parent-leaf-2": "parent-leaf value-2"}'),
+    (5018, 1001, 3006, 5017, '/parent-209/child-001', '{"first-child-leaf": "first-child-leaf value"}'),
+    (5019, 1001, 3006, 5017, '/parent-209/child-002', '{"second-child-leaf": "second-child-leaf value"}'),
+    (5020, 1001, 3006, 5019, '/parent-209/child-002/grand-child', '{"grand-child-leaf": "grand-child-leaf value"}');
+
 INSERT INTO FRAGMENT (ID, DATASPACE_ID, ANCHOR_ID, PARENT_ID, XPATH, ATTRIBUTES) VALUES
     (4201, 1001, 3003, null, '/parent-200', '{"leaf-value": "original"}'),
     (4202, 1001, 3003, 4201, '/parent-200/child-201', '{"leaf-value": "original"}'),
index 174d71f..9f96df2 100644 (file)
@@ -4,6 +4,7 @@
  *  Modifications Copyright (C) 2021 Pantheon.tech
  *  Modifications Copyright (C) 2021-2022 Bell Canada
  *  Modifications Copyright (C) 2022 Deutsche Telekom AG
+ *  Modifications Copyright (C) 2023 TechMahindra Ltd.
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -110,30 +111,31 @@ public interface CpsDataService {
             Collection<String> jsonDataList, OffsetDateTime observedTimestamp);
 
     /**
-     * Retrieves datanode by XPath for given dataspace and anchor.
+     * Retrieves all the datanodes by XPath for given dataspace and anchor.
      *
-     * @param dataspaceName          dataspace name
-     * @param anchorName             anchor name
-     * @param xpath                  xpath
-     * @param fetchDescendantsOption defines the scope of data to fetch: either single node or all the descendant nodes
-     *                               (recursively) as well
-     * @return data node object
+     * @param dataspaceName           dataspace name
+     * @param anchorName              anchor name
+     * @param xpath                   xpath
+     * @param fetchDescendantsOption  defines the scope of data to fetch: either single node or all the descendant nodes
+     *                                (recursively) as well
+     * @return collection of data node objects
      */
-    DataNode getDataNode(String dataspaceName, String anchorName, String xpath,
-        FetchDescendantsOption fetchDescendantsOption);
+    Collection<DataNode> getDataNodes(String dataspaceName, String anchorName, String xpath,
+                                      FetchDescendantsOption fetchDescendantsOption);
 
     /**
-     * Retrieves datanodes by XPath for given dataspace and anchor.
+     * Retrieves all the datanodes for multiple XPaths for given dataspace and anchor.
      *
-     * @param dataspaceName          dataspace name
-     * @param anchorName             anchor name
-     * @param xpaths                 collection of xpath
-     * @param fetchDescendantsOption defines the scope of data to fetch: either single node or all the descendant nodes
-     *                               (recursively) as well
-     * @return data node object
+     * @param dataspaceName           dataspace name
+     * @param anchorName              anchor name
+     * @param xpaths                  collection of xpaths
+     * @param fetchDescendantsOption  defines the scope of data to fetch: either single node or all the descendant nodes
+     *                                (recursively) as well
+     * @return collection of data node objects
      */
-    Collection<DataNode> getDataNodes(String dataspaceName, String anchorName, Collection<String> xpaths,
-                         FetchDescendantsOption fetchDescendantsOption);
+    Collection<DataNode> getDataNodesForMultipleXpaths(String dataspaceName, String anchorName,
+                                                       Collection<String> xpaths,
+                                                       FetchDescendantsOption fetchDescendantsOption);
 
     /**
      * Updates data node for given dataspace and anchor using xpath to parent node.
index 06a0845..b149d42 100755 (executable)
@@ -3,7 +3,7 @@
  *  Copyright (C) 2021-2023 Nordix Foundation
  *  Modifications Copyright (C) 2020-2022 Bell Canada.
  *  Modifications Copyright (C) 2021 Pantheon.tech
- *  Modifications Copyright (C) 2022 TechMahindra Ltd.
+ *  Modifications Copyright (C) 2022-2023 TechMahindra Ltd.
  *  Modifications Copyright (C) 2022 Deutsche Telekom AG
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
@@ -134,21 +134,23 @@ public class CpsDataServiceImpl implements CpsDataService {
 
     @Override
     @Timed(value = "cps.data.service.datanode.get",
-        description = "Time taken to get a data node")
-    public DataNode getDataNode(final String dataspaceName, final String anchorName, final String xpath,
-        final FetchDescendantsOption fetchDescendantsOption) {
+            description = "Time taken to get data nodes for an xpath")
+    public Collection<DataNode> getDataNodes(final String dataspaceName, final String anchorName,
+                                             final String xpath,
+                                             final FetchDescendantsOption fetchDescendantsOption) {
         cpsValidator.validateNameCharacters(dataspaceName, anchorName);
-        return cpsDataPersistenceService.getDataNode(dataspaceName, anchorName, xpath, fetchDescendantsOption);
+        return cpsDataPersistenceService.getDataNodes(dataspaceName, anchorName, xpath, fetchDescendantsOption);
     }
 
     @Override
     @Timed(value = "cps.data.service.datanode.batch.get",
         description = "Time taken to get a batch of data nodes")
-    public Collection<DataNode> getDataNodes(final String dataspaceName, final String anchorName,
-                                             final Collection<String> xpaths,
-                                final FetchDescendantsOption fetchDescendantsOption) {
+    public Collection<DataNode> getDataNodesForMultipleXpaths(final String dataspaceName, final String anchorName,
+                                                              final Collection<String> xpaths,
+                                                              final FetchDescendantsOption fetchDescendantsOption) {
         cpsValidator.validateNameCharacters(dataspaceName, anchorName);
-        return cpsDataPersistenceService.getDataNodes(dataspaceName, anchorName, xpaths, fetchDescendantsOption);
+        return cpsDataPersistenceService.getDataNodesForMultipleXpaths(dataspaceName, anchorName, xpaths,
+                fetchDescendantsOption);
     }
 
     @Override
index f0cdaee..38f8988 100644 (file)
@@ -2,6 +2,7 @@
  * ============LICENSE_START=======================================================
  * Copyright (c) 2021-2022 Bell Canada.
  * Modifications Copyright (c) 2022 Nordix Foundation
+ * Modifications Copyright (C) 2023 TechMahindra Ltd.
  * ================================================================================
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -76,8 +77,8 @@ public class CpsDataUpdatedEventFactory {
     public CpsDataUpdatedEvent createCpsDataUpdatedEvent(final Anchor anchor,
         final OffsetDateTime observedTimestamp, final Operation operation) {
         final var dataNode = (operation == Operation.DELETE) ? null :
-            cpsDataService.getDataNode(anchor.getDataspaceName(), anchor.getName(),
-                "/", FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS);
+            cpsDataService.getDataNodes(anchor.getDataspaceName(), anchor.getName(),
+                "/", FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS).iterator().next();
         return toCpsDataUpdatedEvent(anchor, dataNode, observedTimestamp, operation);
     }
 
index 3e0b447..0b2cef9 100644 (file)
@@ -3,7 +3,7 @@
  *  Copyright (C) 2020-2023 Nordix Foundation.
  *  Modifications Copyright (C) 2021 Pantheon.tech
  *  Modifications Copyright (C) 2022 Bell Canada
- *  Modifications Copyright (C) 2022 TechMahindra Ltd.
+ *  Modifications Copyright (C) 2022-2023 TechMahindra Ltd.
  * ================================================================================
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -99,30 +99,33 @@ public interface CpsDataPersistenceService {
             Collection<Collection<DataNode>> newLists);
 
     /**
-     * Retrieves datanode by XPath for given dataspace and anchor.
+     * Retrieves multiple datanodes for a single XPath for given dataspace and anchor.
+     * Multiple data nodes are returned when xPath is set to root '/', otherwise single data node
+     * is returned when a specific xpath is used (Example: /bookstore).
      *
      * @param dataspaceName          dataspace name
      * @param anchorName             anchor name
-     * @param xpath                  xpath
+     * @param xpath                  one xpath
      * @param fetchDescendantsOption defines the scope of data to fetch: either single node or all the descendant nodes
      *                               (recursively) as well
-     * @return data node object
+     * @return collection of data node object
      */
-    DataNode getDataNode(String dataspaceName, String anchorName, String xpath,
-        FetchDescendantsOption fetchDescendantsOption);
+    Collection<DataNode> getDataNodes(String dataspaceName, String anchorName, String xpath,
+                                      FetchDescendantsOption fetchDescendantsOption);
 
     /**
-     * Retrieves datanode by XPath for given dataspace and anchor.
+     * Retrieves multiple datanodes for multiple XPaths, given a dataspace and anchor.
      *
-     * @param dataspaceName          dataspace name
-     * @param anchorName             anchor name
-     * @param xpaths                 collection of xpaths
-     * @param fetchDescendantsOption defines the scope of data to fetch: either single node or all the descendant nodes
-     *                               (recursively) as well
-     * @return data node object
-     */
-    Collection<DataNode> getDataNodes(String dataspaceName, String anchorName, Collection<String> xpaths,
-                         FetchDescendantsOption fetchDescendantsOption);
+     * @param dataspaceName           dataspace name
+     * @param anchorName              anchor name
+     * @param xpaths                  collection of xpaths
+     * @param fetchDescendantsOption  defines the scope of data to fetch: either single node or all the descendant nodes
+     *                                (recursively) as well
+     * @return collection of data node object
+     */
+    Collection<DataNode> getDataNodesForMultipleXpaths(String dataspaceName, String anchorName,
+                                                       Collection<String> xpaths,
+                                                       FetchDescendantsOption fetchDescendantsOption);
 
     /**
      * Updates leaves for existing data node.
index 8bbf4e5..5a74d07 100644 (file)
@@ -3,7 +3,7 @@
  *  Copyright (C) 2021-2023 Nordix Foundation
  *  Modifications Copyright (C) 2021 Pantheon.tech
  *  Modifications Copyright (C) 2021-2022 Bell Canada.
- *  Modifications Copyright (C) 2022 TechMahindra Ltd.
+ *  Modifications Copyright (C) 2022-2023 TechMahindra Ltd.
  *  Modifications Copyright (C) 2022 Deutsche Telekom AG
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -184,13 +184,27 @@ class CpsDataServiceImplSpec extends Specification {
             thrown(DataValidationException)
     }
 
-    def 'Get data node with option #fetchDescendantsOption.'() {
-        def xpath = '/xpath'
-        def dataNode = new DataNodeBuilder().withXpath(xpath).build()
+    def 'Get all data nodes #scenario.'() {
+        given: 'persistence service returns data for GET request'
+            mockCpsDataPersistenceService.getDataNodes(dataspaceName, anchorName, xpath, fetchDescendantsOption) >> dataNode
+        expect: 'service returns same data if using same parameters'
+            objectUnderTest.getDataNodes(dataspaceName, anchorName, xpath, fetchDescendantsOption) == dataNode
+        where: 'following parameters were used'
+            scenario                                   | xpath   | fetchDescendantsOption                         |   dataNode
+            'with root node xpath and descendants'     | '/'     | FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS | [new DataNodeBuilder().withXpath('/xpath-1').build(), new DataNodeBuilder().withXpath('/xpath-2').build()]
+            'with root node xpath and no descendants'  | '/'     | FetchDescendantsOption.OMIT_DESCENDANTS        | [new DataNodeBuilder().withXpath('/xpath-1').build(), new DataNodeBuilder().withXpath('/xpath-2').build()]
+            'with valid xpath and descendants'         | '/xpath'| FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS | [new DataNodeBuilder().withXpath('/xpath').build()]
+            'with valid xpath and no descendants'      | '/xpath'| FetchDescendantsOption.OMIT_DESCENDANTS        | [new DataNodeBuilder().withXpath('/xpath').build()]
+    }
+
+    def 'Get all data nodes over multiple xpaths with option #fetchDescendantsOption.'() {
+        def xpath1 = '/xpath-1'
+        def xpath2 = '/xpath-2'
+        def dataNode = [new DataNodeBuilder().withXpath(xpath1).build(), new DataNodeBuilder().withXpath(xpath2).build()]
         given: 'persistence service returns data for get data request'
-            mockCpsDataPersistenceService.getDataNode(dataspaceName, anchorName, xpath, fetchDescendantsOption) >> dataNode
+            mockCpsDataPersistenceService.getDataNodesForMultipleXpaths(dataspaceName, anchorName, [xpath1, xpath2], fetchDescendantsOption) >> dataNode
         expect: 'service returns same data if uses same parameters'
-            objectUnderTest.getDataNode(dataspaceName, anchorName, xpath, fetchDescendantsOption) == dataNode
+            objectUnderTest.getDataNodesForMultipleXpaths(dataspaceName, anchorName, [xpath1, xpath2], fetchDescendantsOption) == dataNode
         where: 'all fetch options are supported'
             fetchDescendantsOption << [FetchDescendantsOption.OMIT_DESCENDANTS, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS]
     }
@@ -404,4 +418,4 @@ class CpsDataServiceImplSpec extends Specification {
             1 * mockCpsDataPersistenceService.lockAnchor('some-sessionId', 'some-dataspaceName',
                     'some-anchorName', 250L)
     }
-}
+}
\ No newline at end of file
index 6f9a148..5dbc2bb 100644 (file)
@@ -2,6 +2,7 @@
  *  ============LICENSE_START=======================================================
  *  Copyright (c) 2021-2022 Bell Canada.
  *  Modifications Copyright (c) 2022 Nordix Foundation
+ *  Modifications Copyright (C) 2023 TechMahindra Ltd.
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -50,8 +51,8 @@ class CpsDataUpdateEventFactorySpec extends Specification {
         and: 'cps data service returns the data node details'
             def xpath = '/xpath'
             def dataNode = new DataNodeBuilder().withXpath(xpath).withLeaves(['leafName': 'leafValue']).build()
-            mockCpsDataService.getDataNode(
-                    'my-dataspace', 'my-anchorname', '/', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> dataNode
+            mockCpsDataService.getDataNodes(
+                    'my-dataspace', 'my-anchorname', '/', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> [dataNode]
         when: 'CPS data updated event is created'
             def cpsDataUpdatedEvent = objectUnderTest.createCpsDataUpdatedEvent(anchor,
                     DateTimeUtility.toOffsetDateTime(inputObservedTimestamp), Operation.CREATE)
index 94bcb0a..349f085 100644 (file)
@@ -1,6 +1,7 @@
 /*
  *  ============LICENSE_START=======================================================
  *  Copyright (C) 2023 Nordix Foundation
+ *  Modifications Copyright (C) 2023 TechMahindra Ltd.
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the 'License');
  *  you may not use this file except in compliance with the License.
@@ -36,7 +37,7 @@ class CpsPersistenceSpec extends CpsIntegrationSpecBase{
         and: 'The anchor has been persisted successfully'
             cpsAdminService.getAnchor(TEST_DATASPACE, TEST_ANCHOR).getName() == TEST_ANCHOR
         and: 'The data nodes have been persisted successfully'
-            cpsDataService.getDataNode(TEST_DATASPACE, TEST_ANCHOR, '/bookstore', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS).xpath == '/bookstore'
+            cpsDataService.getDataNodes(TEST_DATASPACE, TEST_ANCHOR, '/bookstore', FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS).iterator().next().xpath == '/bookstore'
     }
 
     def 'Test deletion of all test data'() {
@@ -56,6 +57,6 @@ class CpsPersistenceSpec extends CpsIntegrationSpecBase{
         when: 'data is persisted to the database'
             saveDataNodes(TEST_DATASPACE, TEST_ANCHOR, "/", "BookstoreDataNodes.json")
         then: 'the correct data is saved'
-            cpsDataService.getDataNode(TEST_DATASPACE, TEST_ANCHOR, '/bookstore', FetchDescendantsOption.OMIT_DESCENDANTS).leaves['bookstore-name'] == 'Easons'
+            cpsDataService.getDataNodes(TEST_DATASPACE, TEST_ANCHOR, '/bookstore', FetchDescendantsOption.OMIT_DESCENDANTS).iterator().next().leaves['bookstore-name'] == 'Easons'
     }
 }