Part 1: Grouping of Data Nodes in Delta Report 50/139850/39
authorArpit Singh <AS00745003@techmahindra.com>
Mon, 13 Jan 2025 13:23:30 +0000 (18:53 +0530)
committerArpit Singh <AS00745003@techmahindra.com>
Tue, 27 May 2025 11:59:06 +0000 (17:29 +0530)
Add-on feature in delta report to generate condensed delta report by
grouping data nodes based on parent child relationship.

The patch adds grouping feature for "create" operation in delta report.

- Added boolean flag "groupingEnabled" to enable or disable grouping of
  data nodes in delta report. Default value is false.
- Added a method getCondensedAddedDeltaReports to generate condensed
  delta reports for create operation.
- Part 2 of this patch will have code for updated and removed delta
  report entries when grouping is enabled.
- A separate patch to add integration tests will be done after feature
  is implemented. This is done to keep patch sizes small

Issue-ID: CPS-2547
Change-Id: Ibb4d35b03098be7b57cb59852a87f6b4e0c7b706
Signed-off-by: Arpit Singh <AS00745003@techmahindra.com>
cps-rest/docs/openapi/components.yml
cps-rest/docs/openapi/cpsDelta.yml
cps-rest/src/main/java/org/onap/cps/rest/controller/DeltaRestController.java
cps-rest/src/test/groovy/org/onap/cps/rest/controller/DeltaRestControllerSpec.groovy
cps-service/src/main/java/org/onap/cps/api/CpsDeltaService.java
cps-service/src/main/java/org/onap/cps/impl/CpsDeltaServiceImpl.java
cps-service/src/test/groovy/org/onap/cps/impl/CpsDeltaServiceImplSpec.groovy
docs/api/swagger/cps/openapi.yaml
integration-test/src/test/groovy/org/onap/cps/integration/functional/cps/DeltaServiceIntegrationSpec.groovy

index f3a5411..e151706 100644 (file)
@@ -470,6 +470,15 @@ components:
         type: boolean
         default: false
         example: false
+    groupDataNodesInQuery:
+      name: grouping-enabled
+      in: query
+      description: Boolean flag to enable or disable grouping of data nodes. Enabling it generates a condensed delta report.
+      required: false
+      schema:
+        type: boolean
+        default: false
+        example: true
 
   responses:
     NotFound:
index 644dc27..14655ea 100644 (file)
@@ -29,6 +29,7 @@ delta:
       - $ref: 'components.yml#/components/parameters/targetAnchorNameInQuery'
       - $ref: 'components.yml#/components/parameters/xpathInQuery'
       - $ref: 'components.yml#/components/parameters/descendantsInQuery'
+      - $ref: 'components.yml#/components/parameters/groupDataNodesInQuery'
     responses:
       '200':
         description: OK
@@ -56,6 +57,7 @@ delta:
       - $ref: 'components.yml#/components/parameters/dataspaceNameInPath'
       - $ref: 'components.yml#/components/parameters/sourceAnchorNameInPath'
       - $ref: 'components.yml#/components/parameters/xpathInQuery'
+      - $ref: 'components.yml#/components/parameters/groupDataNodesInQuery'
     requestBody:
       content:
         multipart/form-data:
index 641a4db..c4631c7 100644 (file)
@@ -56,12 +56,13 @@ public class DeltaRestController implements CpsDeltaApi {
                                                                 final String sourceAnchorName,
                                                                 final String targetAnchorName,
                                                                 final String xpath,
-                                                                final String descendants) {
+                                                                final String descendants,
+                                                                final Boolean groupDataNodes) {
         final FetchDescendantsOption fetchDescendantsOption =
             FetchDescendantsOption.getFetchDescendantsOption(descendants);
         final List<DeltaReport> deltaBetweenAnchors =
             cpsDeltaService.getDeltaByDataspaceAndAnchors(dataspaceName, sourceAnchorName,
-                targetAnchorName, xpath, fetchDescendantsOption);
+                targetAnchorName, xpath, fetchDescendantsOption, groupDataNodes);
         return new ResponseEntity<>(jsonObjectMapper.asJsonString(deltaBetweenAnchors), HttpStatus.OK);
     }
 
@@ -72,6 +73,7 @@ public class DeltaRestController implements CpsDeltaApi {
                                                                       final String sourceAnchorName,
                                                                       final MultipartFile targetDataAsJsonFile,
                                                                       final String xpath,
+                                                                      final Boolean groupDataNodes,
                                                                       final MultipartFile yangResourceFile) {
         final FetchDescendantsOption fetchDescendantsOption = FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS;
         final String targetData = MultipartFileUtil.extractJsonContent(targetDataAsJsonFile, jsonObjectMapper);
@@ -83,7 +85,7 @@ public class DeltaRestController implements CpsDeltaApi {
         }
         final Collection<DeltaReport> deltaReports = Collections.unmodifiableList(
             cpsDeltaService.getDeltaByDataspaceAnchorAndPayload(dataspaceName, sourceAnchorName,
-                xpath, yangResourceMap, targetData, fetchDescendantsOption));
+                xpath, yangResourceMap, targetData, fetchDescendantsOption, groupDataNodes));
         return new ResponseEntity<>(jsonObjectMapper.asJsonString(deltaReports), HttpStatus.OK);
     }
 }
index db4ef57..fe9f230 100644 (file)
@@ -63,6 +63,7 @@ class DeltaRestControllerSpec extends Specification {
     def dataNodeBaseEndpointV2
     def dataspaceName = 'my_dataspace'
     def anchorName = 'my_anchor'
+    def NO_GROUPING = false
 
     @Shared
     def requestBodyJson = '{"some-key":"some-value","categories":[{"books":[{"authors":["Iain M. Banks"]}]}]}'
@@ -90,7 +91,7 @@ class DeltaRestControllerSpec extends Specification {
         given: 'the service returns a list containing delta reports'
             def deltaReports = new DeltaReportBuilder().actionReplace().withXpath('some xpath').withSourceData('some key': 'some value').withTargetData('some key': 'some value').build()
             def xpath = 'some xpath'
-            mockCpsDeltaService.getDeltaByDataspaceAndAnchors(dataspaceName, anchorName, 'targetAnchor', xpath, OMIT_DESCENDANTS) >> [deltaReports]
+            mockCpsDeltaService.getDeltaByDataspaceAndAnchors(dataspaceName, anchorName, 'targetAnchor', xpath, OMIT_DESCENDANTS, NO_GROUPING) >> [deltaReports]
         when: 'get delta request is performed using REST API'
             def response =
                 mvc.perform(get(dataNodeBaseEndpointV2)
@@ -108,7 +109,7 @@ class DeltaRestControllerSpec extends Specification {
             def deltaReports = new DeltaReportBuilder().actionCreate().withXpath('some xpath').build()
             def xpath = 'some xpath'
         and: 'the service layer returns a list containing delta reports'
-            mockCpsDeltaService.getDeltaByDataspaceAnchorAndPayload(dataspaceName, anchorName, xpath, ['filename.yang':'content'], expectedJsonData, INCLUDE_ALL_DESCENDANTS) >> [deltaReports]
+            mockCpsDeltaService.getDeltaByDataspaceAnchorAndPayload(dataspaceName, anchorName, xpath, ['filename.yang':'content'], expectedJsonData, INCLUDE_ALL_DESCENDANTS, NO_GROUPING) >> [deltaReports]
         when: 'get delta request is performed using REST API'
             def response =
                 mvc.perform(multipart(dataNodeBaseEndpointV2)
@@ -128,7 +129,7 @@ class DeltaRestControllerSpec extends Specification {
             def deltaReports = new DeltaReportBuilder().actionRemove().withXpath('some xpath').build()
             def xpath = 'some xpath'
         and: 'the service layer returns a list containing delta reports'
-            mockCpsDeltaService.getDeltaByDataspaceAnchorAndPayload(dataspaceName, anchorName, xpath, [:], expectedJsonData, INCLUDE_ALL_DESCENDANTS) >> [deltaReports]
+            mockCpsDeltaService.getDeltaByDataspaceAnchorAndPayload(dataspaceName, anchorName, xpath, [:], expectedJsonData, INCLUDE_ALL_DESCENDANTS, NO_GROUPING) >> [deltaReports]
         when: 'get delta request is performed using REST API'
             def response =
                 mvc.perform(multipart(dataNodeBaseEndpointV2)
index 671b1d6..a7f8fc3 100644 (file)
@@ -37,11 +37,15 @@ public interface CpsDeltaService {
      * @param xpath                  xpath
      * @param fetchDescendantsOption defines the scope of data to fetch: either single node or all the descendant
      *                               nodes (recursively) as well
+     * @param groupDataNodes         boolean flag to enable or disable grouping of data nodes in delta report.
+     *                               If enabled, data nodes are grouped based on parent-child relationship, providing a
+     *                               condensed version of delta report.
      * @return                       list containing {@link DeltaReport} objects
      */
     List<DeltaReport> getDeltaByDataspaceAndAnchors(String dataspaceName, String sourceAnchorName,
                                                     String targetAnchorName, String xpath,
-                                                    FetchDescendantsOption fetchDescendantsOption);
+                                                    FetchDescendantsOption fetchDescendantsOption,
+                                                    boolean groupDataNodes);
 
     /**
      * Retrieves the delta between an anchor and JSON payload by xpath, using dataspace name and anchor name.
@@ -54,13 +58,17 @@ public interface CpsDeltaService {
      * @param yangResourceContentPerName   YANG resources (files) map where key is a name and value is content
      * @param targetData                   target data to be compared in JSON string format
      * @param fetchDescendantsOption       defines the scope of data to fetch: defaulted to INCLUDE_ALL_DESCENDANTS
+     * @param groupDataNodes               boolean flag to enable or disable grouping of data nodes in delta report.
+     *                                     If enabled, data nodes are grouped based on parent-child relationship,
+     *                                     providing a condensed version of delta report.
      *
      * @return                             list containing {@link DeltaReport} objects
      */
     List<DeltaReport> getDeltaByDataspaceAnchorAndPayload(String dataspaceName, String sourceAnchorName, String xpath,
                                                           Map<String, String> yangResourceContentPerName,
                                                           String targetData,
-                                                          FetchDescendantsOption fetchDescendantsOption);
+                                                          FetchDescendantsOption fetchDescendantsOption,
+                                                          boolean groupDataNodes);
 
 
 }
index 650aa99..727c6b7 100644 (file)
@@ -31,6 +31,7 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.stream.Collectors;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.onap.cps.api.CpsAnchorService;
@@ -41,6 +42,8 @@ import org.onap.cps.api.model.Anchor;
 import org.onap.cps.api.model.DataNode;
 import org.onap.cps.api.model.DeltaReport;
 import org.onap.cps.api.parameters.FetchDescendantsOption;
+import org.onap.cps.cpspath.parser.CpsPathUtil;
+import org.onap.cps.utils.DataMapUtils;
 import org.onap.cps.utils.DataMapper;
 import org.onap.cps.utils.JsonObjectMapper;
 import org.springframework.stereotype.Service;
@@ -63,13 +66,14 @@ public class CpsDeltaServiceImpl implements CpsDeltaService {
                                                            final String sourceAnchorName,
                                                            final String targetAnchorName,
                                                            final String xpath,
-                                                           final FetchDescendantsOption fetchDescendantsOption) {
+                                                           final FetchDescendantsOption fetchDescendantsOption,
+                                                           final boolean groupDataNodes) {
 
         final Collection<DataNode> sourceDataNodes = cpsDataService.getDataNodesForMultipleXpaths(dataspaceName,
             sourceAnchorName, Collections.singletonList(xpath), fetchDescendantsOption);
         final Collection<DataNode> targetDataNodes = cpsDataService.getDataNodesForMultipleXpaths(dataspaceName,
             targetAnchorName, Collections.singletonList(xpath), fetchDescendantsOption);
-        return getDeltaReports(sourceDataNodes, targetDataNodes);
+        return getDeltaReports(sourceDataNodes, targetDataNodes, groupDataNodes);
     }
 
     @Timed(value = "cps.delta.service.get.delta",
@@ -80,7 +84,8 @@ public class CpsDeltaServiceImpl implements CpsDeltaService {
                                                                  final String xpath,
                                                                  final Map<String, String> yangResourceContentPerName,
                                                                  final String targetData,
-                                                                 final FetchDescendantsOption fetchDescendantsOption) {
+                                                                 final FetchDescendantsOption fetchDescendantsOption,
+                                                                 final boolean groupDataNodes) {
 
         final Anchor sourceAnchor = cpsAnchorService.getAnchor(dataspaceName, sourceAnchorName);
         final Collection<DataNode> sourceDataNodes = cpsDataService.getDataNodesForMultipleXpaths(dataspaceName,
@@ -89,25 +94,26 @@ public class CpsDeltaServiceImpl implements CpsDeltaService {
             rebuildSourceDataNodes(xpath, sourceAnchor, sourceDataNodes);
         final Collection<DataNode> targetDataNodes = new ArrayList<>(
             buildTargetDataNodes(sourceAnchor, xpath, yangResourceContentPerName, targetData));
-        return getDeltaReports(sourceDataNodesRebuilt, targetDataNodes);
+        return getDeltaReports(sourceDataNodesRebuilt, targetDataNodes, groupDataNodes);
     }
 
-    private List<DeltaReport> getDeltaReports(final Collection<DataNode> sourceDataNodes,
-                                              final Collection<DataNode> targetDataNodes) {
+    private static List<DeltaReport> getDeltaReports(final Collection<DataNode> sourceDataNodes,
+                                              final Collection<DataNode> targetDataNodes,
+                                              final boolean groupDataNodes) {
 
         final List<DeltaReport> deltaReport = new ArrayList<>();
-
-        final Map<String, DataNode> xpathToSourceDataNodes = convertToXPathToDataNodesMap(sourceDataNodes);
-        final Map<String, DataNode> xpathToTargetDataNodes = convertToXPathToDataNodesMap(targetDataNodes);
-
-        deltaReport.addAll(getRemovedAndUpdatedDeltaReports(xpathToSourceDataNodes, xpathToTargetDataNodes));
-        deltaReport.addAll(getAddedDeltaReports(xpathToSourceDataNodes, xpathToTargetDataNodes));
-
+        if (groupDataNodes) {
+            deltaReport.addAll(getCondensedAddedDeltaReports(sourceDataNodes, targetDataNodes));
+        } else {
+            final Map<String, DataNode> xpathToSourceDataNodes = convertToXPathToDataNodesMap(sourceDataNodes);
+            final Map<String, DataNode> xpathToTargetDataNodes = convertToXPathToDataNodesMap(targetDataNodes);
+            deltaReport.addAll(getRemovedAndUpdatedDeltaReports(xpathToSourceDataNodes, xpathToTargetDataNodes));
+            deltaReport.addAll(getAddedDeltaReports(xpathToSourceDataNodes, xpathToTargetDataNodes));
+        }
         return Collections.unmodifiableList(deltaReport);
     }
 
-    private static Map<String, DataNode> convertToXPathToDataNodesMap(
-                                                                    final Collection<DataNode> dataNodes) {
+    private static Map<String, DataNode> convertToXPathToDataNodesMap(final Collection<DataNode> dataNodes) {
         final Map<String, DataNode> xpathToDataNode = new LinkedHashMap<>();
         for (final DataNode dataNode : dataNodes) {
             xpathToDataNode.put(dataNode.getXpath(), dataNode);
@@ -232,7 +238,6 @@ public class CpsDeltaServiceImpl implements CpsDeltaService {
                     .withXpath(xpath).withSourceData(entry.getKey()).withTargetData(entry.getValue()).build();
             updatedDeltaReportEntries.add(updatedDataForDeltaReport);
         }
-
     }
 
     private static List<DeltaReport> getAddedDeltaReports(final Map<String, DataNode> xpathToSourceDataNodes,
@@ -276,4 +281,40 @@ public class CpsDeltaServiceImpl implements CpsDeltaService {
                 .createDataNodesWithYangResourceXpathAndNodeData(yangResourceContentPerName, xpath, targetData, JSON);
         }
     }
+
+    private static List<DeltaReport> getCondensedAddedDeltaReports(final Collection<DataNode> sourceDataNodes,
+            final Collection<DataNode> targetDataNodes) {
+
+        final List<DeltaReport> addedDeltaReportEntries = new ArrayList<>();
+        final Collection<DataNode> addedDataNodes =
+                getDataNodesForDeltaReport(targetDataNodes, flattenToXpathToFirstLevelDataNodeMap(sourceDataNodes));
+        if (!addedDataNodes.isEmpty()) {
+            final String xpath = getXpathForDeltaReport(addedDataNodes);
+            addedDeltaReportEntries.add(new DeltaReportBuilder().actionCreate().withXpath(xpath)
+                .withTargetData(getCondensedDataForDeltaReport(addedDataNodes)).build());
+        }
+        return addedDeltaReportEntries;
+    }
+
+    private static String getXpathForDeltaReport(final Collection<DataNode> dataNodes) {
+        final String firstNodeXpath = dataNodes.iterator().next().getXpath();
+        final String parentNodeXpath = CpsPathUtil.getNormalizedParentXpath(firstNodeXpath);
+        return parentNodeXpath.isEmpty() ? firstNodeXpath : parentNodeXpath;
+    }
+
+    private static Collection<DataNode> getDataNodesForDeltaReport(final Collection<DataNode> dataNodes,
+                                                                   final Map<String, DataNode> xpathToDataNodes) {
+        return dataNodes.stream().filter(dataNode -> !xpathToDataNodes.containsKey(dataNode.getXpath())).toList();
+    }
+
+    private static Map<String, Serializable> getCondensedDataForDeltaReport(final Collection<DataNode> dataNodes) {
+        final DataNode containerNode = new DataNodeBuilder().withChildDataNodes(dataNodes).build();
+        final Map<String, Object> condensedData = DataMapUtils.toDataMap(containerNode);
+        return condensedData.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey,
+            entry -> (Serializable) entry.getValue()));
+    }
+
+    private static Map<String, DataNode> flattenToXpathToFirstLevelDataNodeMap(final Collection<DataNode> dataNodes) {
+        return dataNodes.stream().collect(Collectors.toMap(DataNode::getXpath, dataNode -> dataNode));
+    }
 }
index a1bfbb0..d979be4 100644 (file)
@@ -30,7 +30,6 @@ import org.onap.cps.api.CpsDataService
 import org.onap.cps.api.exceptions.DataValidationException
 import org.onap.cps.api.model.Anchor
 import org.onap.cps.api.model.DataNode
-import org.onap.cps.api.parameters.FetchDescendantsOption
 import org.onap.cps.utils.ContentType
 import org.onap.cps.utils.DataMapper
 import org.onap.cps.utils.JsonObjectMapper
@@ -45,6 +44,9 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext
 import spock.lang.Shared
 import spock.lang.Specification
 
+import static org.onap.cps.api.parameters.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
+import static org.onap.cps.api.parameters.FetchDescendantsOption.OMIT_DESCENDANTS
+
 class CpsDeltaServiceImplSpec extends Specification {
 
     def mockCpsAnchorService = Mock(CpsAnchorService)
@@ -64,10 +66,12 @@ class CpsDeltaServiceImplSpec extends Specification {
     static def bookstoreDataAsMapForChildNode = [categories: ['code': '02', 'name': 'Kids']]
     static def bookstoreJsonForParentNode = '{"bookstore":{"bookstore-name":"My Store"}}'
     static def bookstoreJsonForChildNode = '{"categories":[{"name":"Child","code":"02"}]}'
-
-    static def sourceDataNodeWithLeafData = [new DataNode(xpath: '/parent', leaves: ['parent-leaf': 'parent-payload-in-source'])]
+    static def sourceDataNode = [new DataNode(xpath: '/parent', leaves: ['parent-leaf': 'parent-leaf-as-source-data'])]
+    static def sourceDataNodeWithChild = [new DataNode(xpath: '/parent', leaves: ['parent-leaf': 'parent-leaf-as-source-data'], childDataNodes: [new DataNode(xpath: '/parent/child', leaves: ['child-leaf': 'child-leaf-as-source-data'])])]
     static def sourceDataNodeWithoutLeafData = [new DataNode(xpath: '/parent')]
-    static def targetDataNodeWithLeafData = [new DataNode(xpath: '/parent', leaves: ['parent-leaf': 'parent-payload-in-target'])]
+    static def targetDataNode = [new DataNode(xpath: '/parent', leaves: ['parent-leaf': 'parent-leaf-as-target-data'])]
+    static def targetDataNodeWithChild = [new DataNode(xpath: '/parent', leaves: ['parent-leaf': 'parent-leaf-as-target-data'], childDataNodes: [new DataNode(xpath: '/parent/child', leaves: ['child-leaf': 'child-leaf-as-target-data'])])]
+    static def targetDataNodeWithXpath = [new DataNode(xpath: '/parent/child', leaves: ['child-leaf': 'child-leaf-as-target-data'])]
     static def targetDataNodeWithoutLeafData = [new DataNode(xpath: '/parent')]
     static def sourceDataNodeWithMultipleLeaves = [new DataNode(xpath: '/parent', leaves: ['leaf-1': 'leaf-1-in-source', 'leaf-2': 'leaf-2-in-source'])]
     static def targetDataNodeWithMultipleLeaves = [new DataNode(xpath: '/parent', leaves: ['leaf-1': 'leaf-1-in-target', 'leaf-2': 'leaf-2-in-target'])]
@@ -79,11 +83,12 @@ class CpsDeltaServiceImplSpec extends Specification {
     @Shared
     static def ANCHOR_NAME_1 = 'some-anchor-1'
     static def ANCHOR_NAME_2 = 'some-anchor-2'
-    static def INCLUDE_ALL_DESCENDANTS = FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
     def dataspaceName = 'some-dataspace'
     def schemaSetName = 'some-schema-set'
     def anchor1 = Anchor.builder().name(ANCHOR_NAME_1).dataspaceName(dataspaceName).schemaSetName(schemaSetName).build()
     def anchor2 = Anchor.builder().name(ANCHOR_NAME_2).dataspaceName(dataspaceName).schemaSetName(schemaSetName).build()
+    def GROUPING_ENABLED = true
+    def GROUPING_DISABLED = false
 
     def setup() {
         mockCpsAnchorService.getAnchor(dataspaceName, ANCHOR_NAME_1) >> anchor1
@@ -100,15 +105,15 @@ class CpsDeltaServiceImplSpec extends Specification {
         applicationContext.close()
     }
 
-    def 'Get Delta between 2 anchors for #scenario'() {
+    def 'Get Delta between 2 anchors for #scenario with grouping of data nodes disabled'() {
         given: 'xpath to get delta'
             def xpath = '/'
         when: 'attempt to get delta between 2 anchors'
-            def deltaReport = objectUnderTest.getDeltaByDataspaceAndAnchors(dataspaceName, ANCHOR_NAME_1, ANCHOR_NAME_2, xpath, INCLUDE_ALL_DESCENDANTS)
+            def deltaReport = objectUnderTest.getDeltaByDataspaceAndAnchors(dataspaceName, ANCHOR_NAME_1, ANCHOR_NAME_2, xpath, OMIT_DESCENDANTS, GROUPING_DISABLED)
         then: 'cps data service is invoked and returns source data nodes'
-            mockCpsDataService.getDataNodesForMultipleXpaths(dataspaceName, ANCHOR_NAME_1, [xpath], INCLUDE_ALL_DESCENDANTS) >> sourceDataNodes
+            mockCpsDataService.getDataNodesForMultipleXpaths(dataspaceName, ANCHOR_NAME_1, [xpath], OMIT_DESCENDANTS) >> sourceDataNodes
         and: 'cps data service is invoked again to return target data nodes'
-            mockCpsDataService.getDataNodesForMultipleXpaths(dataspaceName, ANCHOR_NAME_2, [xpath], INCLUDE_ALL_DESCENDANTS) >> targetDataNodes
+            mockCpsDataService.getDataNodesForMultipleXpaths(dataspaceName, ANCHOR_NAME_2, [xpath], OMIT_DESCENDANTS) >> targetDataNodes
         and: 'the delta report contains the expected information'
             deltaReport.size() == 1
             deltaReport[0].action.equals(expectedAction)
@@ -116,44 +121,49 @@ class CpsDeltaServiceImplSpec extends Specification {
             deltaReport[0].sourceData == expectedSourceData
             deltaReport[0].targetData == expectedTargetData
         where: 'following data was used'
-            scenario               | sourceDataNodes            | targetDataNodes            || expectedAction | expectedSourceData                          | expectedTargetData
-            'Data node is added'   | []                         | targetDataNodeWithLeafData || 'create'       | null                                        | ['parent-leaf': 'parent-payload-in-target']
-            'Data node is removed' | sourceDataNodeWithLeafData | []                         || 'remove'       | ['parent-leaf': 'parent-payload-in-source'] | null
-            'Data node is updated' | sourceDataNodeWithLeafData | targetDataNodeWithLeafData || 'replace'      | ['parent-leaf': 'parent-payload-in-source'] |['parent-leaf': 'parent-payload-in-target']
+            scenario               | sourceDataNodes | targetDataNodes || expectedAction | expectedSourceData                            | expectedTargetData
+            'Data node is added'   | []              | targetDataNode  || 'create'       | null                                          | ['parent-leaf': 'parent-leaf-as-target-data']
+            'Data node is removed' | sourceDataNode  | []              || 'remove'       | ['parent-leaf': 'parent-leaf-as-source-data'] | null
+            'Data node is updated' | sourceDataNode  | targetDataNode  || 'replace'      | ['parent-leaf': 'parent-leaf-as-source-data'] |['parent-leaf': 'parent-leaf-as-target-data']
     }
 
-    def 'Delta Report between parent nodes containing child nodes'() {
-        given: 'Two data nodes and xpath'
+    def 'Delta Report between parent nodes containing child nodes where #scenario with grouping of data nodes disabled'() {
+        given: 'root node xpath'
             def xpath = '/'
-            def sourceDataNode  = [new DataNode(xpath: '/parent', leaves: ['parent-leaf': 'parent-payload'], childDataNodes: [new DataNode(xpath: '/parent/child', leaves: ['child-leaf': 'child-payload'])])]
-            def targetDataNode  = [new DataNode(xpath: '/parent', leaves: ['parent-leaf': 'parent-payload-updated'], childDataNodes: [new DataNode(xpath: '/parent/child', leaves: ['child-leaf': 'child-payload-updated'])])]
         when: 'attempt to get delta between 2 anchors'
-            def deltaReport = objectUnderTest.getDeltaByDataspaceAndAnchors(dataspaceName, ANCHOR_NAME_1, ANCHOR_NAME_2, xpath, INCLUDE_ALL_DESCENDANTS)
+            def deltaReport = objectUnderTest.getDeltaByDataspaceAndAnchors(dataspaceName, ANCHOR_NAME_1, ANCHOR_NAME_2, xpath, INCLUDE_ALL_DESCENDANTS, GROUPING_DISABLED)
         then: 'cps data service is invoked and returns source data nodes'
-            mockCpsDataService.getDataNodesForMultipleXpaths(dataspaceName, ANCHOR_NAME_1, [xpath], INCLUDE_ALL_DESCENDANTS) >> sourceDataNode
+            mockCpsDataService.getDataNodesForMultipleXpaths(dataspaceName, ANCHOR_NAME_1, [xpath], INCLUDE_ALL_DESCENDANTS) >> sourceDataNodes
         and: 'cps data service is invoked again to return target data nodes'
-            mockCpsDataService.getDataNodesForMultipleXpaths(dataspaceName, ANCHOR_NAME_2, [xpath], INCLUDE_ALL_DESCENDANTS) >> targetDataNode
+            mockCpsDataService.getDataNodesForMultipleXpaths(dataspaceName, ANCHOR_NAME_2, [xpath], INCLUDE_ALL_DESCENDANTS) >> targetDataNodes
+        and: 'delta report contains correct number of entries'
+            deltaReport.size() == 2
         and: 'the delta report contains expected details for parent node'
-            assert deltaReport[0].action.equals('replace')
+            assert deltaReport[0].action == expectedAction
             assert deltaReport[0].xpath == '/parent'
-            assert deltaReport[0].sourceData == ['parent-leaf': 'parent-payload']
-            assert deltaReport[0].targetData == ['parent-leaf': 'parent-payload-updated']
+            assert deltaReport[0].sourceData == expectedSourceDataForParent
+            assert deltaReport[0].targetData == expectedTargetDataForParent
         and: 'the delta report contains expected details for child node'
-            assert deltaReport[1].action.equals('replace')
+            assert deltaReport[1].action == expectedAction
             assert deltaReport[1].xpath == '/parent/child'
-            assert deltaReport[1].sourceData == ['child-leaf': 'child-payload']
-            assert deltaReport[1].targetData == ['child-leaf': 'child-payload-updated']
+            assert deltaReport[1].sourceData == expectedSourceDataForChild
+            assert deltaReport[1].targetData == expectedTargetDataForChild
+        where: 'the following data is used'
+            scenario               | sourceDataNodes         | targetDataNodes         || expectedAction | expectedSourceDataForParent                   | expectedTargetDataForParent                   | expectedSourceDataForChild                  | expectedTargetDataForChild
+            'Data node is added'   | []                      | targetDataNodeWithChild || 'create'       | null                                          | ['parent-leaf': 'parent-leaf-as-target-data'] | null                                        | ['child-leaf': 'child-leaf-as-target-data']
+            'Data node is removed' | sourceDataNodeWithChild | []                      || 'remove'       | ['parent-leaf': 'parent-leaf-as-source-data'] | null                                          | ['child-leaf': 'child-leaf-as-source-data'] | null
+            'Data node is updated' | sourceDataNodeWithChild | targetDataNodeWithChild || 'replace'      | ['parent-leaf': 'parent-leaf-as-source-data'] | ['parent-leaf': 'parent-leaf-as-target-data'] | ['child-leaf': 'child-leaf-as-source-data'] | ['child-leaf': 'child-leaf-as-target-data']
     }
 
     def 'Delta report between leaves, #scenario'() {
     given: 'xpath to fetch delta between two anchors'
         def xpath = '/'
     when: 'attempt to get delta between 2 anchors'
-        def deltaReport = objectUnderTest.getDeltaByDataspaceAndAnchors(dataspaceName, ANCHOR_NAME_1, ANCHOR_NAME_2, xpath, INCLUDE_ALL_DESCENDANTS)
+        def deltaReport = objectUnderTest.getDeltaByDataspaceAndAnchors(dataspaceName, ANCHOR_NAME_1, ANCHOR_NAME_2, xpath, INCLUDE_ALL_DESCENDANTS, GROUPING_DISABLED)
     then: 'cps data service is invoked and returns source data nodes'
-        mockCpsDataService.getDataNodesForMultipleXpaths(dataspaceName, ANCHOR_NAME_1, [xpath], INCLUDE_ALL_DESCENDANTS) >> sourceDataNode
+        mockCpsDataService.getDataNodesForMultipleXpaths(dataspaceName, ANCHOR_NAME_1, [xpath], INCLUDE_ALL_DESCENDANTS) >> sourceDataNodes
     and: 'cps data service is invoked again to return target data nodes'
-        mockCpsDataService.getDataNodesForMultipleXpaths(dataspaceName, ANCHOR_NAME_2, [xpath], INCLUDE_ALL_DESCENDANTS) >> targetDataNode
+        mockCpsDataService.getDataNodesForMultipleXpaths(dataspaceName, ANCHOR_NAME_2, [xpath], INCLUDE_ALL_DESCENDANTS) >> targetDataNodes
     and: 'the delta report contains expected "replace" action'
         assert deltaReport[0].action.equals('replace')
     and: 'the delta report contains expected xpath'
@@ -162,18 +172,18 @@ class CpsDeltaServiceImplSpec extends Specification {
         assert deltaReport[0].sourceData == expectedSourceData
         assert deltaReport[0].targetData == expectedTargetData
     where: 'the following data was used'
-        scenario                                           | sourceDataNode                   | targetDataNode                   || expectedSourceData                                           | expectedTargetData
-        'source and target data nodes have leaves'         | sourceDataNodeWithLeafData       | targetDataNodeWithLeafData       || ['parent-leaf': 'parent-payload-in-source']                  | ['parent-leaf': 'parent-payload-in-target']
-        'only source data node has leaves'                 | sourceDataNodeWithLeafData       | targetDataNodeWithoutLeafData    || ['parent-leaf': 'parent-payload-in-source']                  | null
-        'only target data node has leaves'                 | sourceDataNodeWithoutLeafData    | targetDataNodeWithLeafData       || null                                                         | ['parent-leaf': 'parent-payload-in-target']
-        'source and target dsta node with multiple leaves' | sourceDataNodeWithMultipleLeaves | targetDataNodeWithMultipleLeaves || ['leaf-1': 'leaf-1-in-source', 'leaf-2': 'leaf-2-in-source'] | ['leaf-1': 'leaf-1-in-target', 'leaf-2': 'leaf-2-in-target']
+        scenario                                           | sourceDataNodes                  | targetDataNodes                  || expectedSourceData                                           | expectedTargetData
+        'source and target data nodes have leaves'         | sourceDataNode                   | targetDataNode                   || ['parent-leaf': 'parent-leaf-as-source-data']                | ['parent-leaf': 'parent-leaf-as-target-data']
+        'only source data node has leaves'                 | sourceDataNode                   | targetDataNodeWithoutLeafData    || ['parent-leaf': 'parent-leaf-as-source-data']                | null
+        'only target data node has leaves'                 | sourceDataNodeWithoutLeafData    | targetDataNode                   || null                                                         | ['parent-leaf': 'parent-leaf-as-target-data']
+        'source and target data node with multiple leaves' | sourceDataNodeWithMultipleLeaves | targetDataNodeWithMultipleLeaves || ['leaf-1': 'leaf-1-in-source', 'leaf-2': 'leaf-2-in-source'] | ['leaf-1': 'leaf-1-in-target', 'leaf-2': 'leaf-2-in-target']
     }
 
     def 'Get delta between data nodes for updated data, where source and target data nodes have no leaves '() {
         given: 'xpath to get delta between anchors'
             def xpath = '/'
         when: 'attempt to get delta between 2 data nodes'
-            def deltaReport = objectUnderTest.getDeltaByDataspaceAndAnchors(dataspaceName, ANCHOR_NAME_1, ANCHOR_NAME_2, xpath, INCLUDE_ALL_DESCENDANTS)
+            def deltaReport = objectUnderTest.getDeltaByDataspaceAndAnchors(dataspaceName, ANCHOR_NAME_1, ANCHOR_NAME_2, xpath, INCLUDE_ALL_DESCENDANTS, GROUPING_DISABLED)
         then: 'cps data service is invoked and returns source data nodes'
             mockCpsDataService.getDataNodesForMultipleXpaths(dataspaceName, ANCHOR_NAME_1, [xpath], INCLUDE_ALL_DESCENDANTS) >> sourceDataNodeWithoutLeafData
         and: 'cps data service is invoked again to return target data nodes'
@@ -187,7 +197,7 @@ class CpsDeltaServiceImplSpec extends Specification {
             def yangResourceContentPerName = TestUtils.getYangResourcesAsMap('bookstore.yang')
             setupSchemaSetMocksForDelta(yangResourceContentPerName)
         when: 'attempt to get delta between an anchor and a JSON payload'
-            def deltaReport = objectUnderTest.getDeltaByDataspaceAnchorAndPayload(dataspaceName, ANCHOR_NAME_1, xpath, yangResourceContentPerName, jsonData, INCLUDE_ALL_DESCENDANTS)
+            def deltaReport = objectUnderTest.getDeltaByDataspaceAnchorAndPayload(dataspaceName, ANCHOR_NAME_1, xpath, yangResourceContentPerName, jsonData, INCLUDE_ALL_DESCENDANTS, GROUPING_DISABLED)
         then: 'cps data service is invoked and returns source data nodes'
             mockCpsDataService.getDataNodesForMultipleXpaths(dataspaceName, ANCHOR_NAME_1, [xpath], INCLUDE_ALL_DESCENDANTS) >> sourceDataNodes
         and: 'source data nodes are rebuilt (to match the data type with target data nodes)'
@@ -210,7 +220,7 @@ class CpsDeltaServiceImplSpec extends Specification {
         given: 'schema set for a given dataspace and anchor'
             setupSchemaSetMocks('bookstore.yang')
         when: 'attempt to get delta between an anchor and a JSON payload'
-            def deltaReport = objectUnderTest.getDeltaByDataspaceAnchorAndPayload(dataspaceName, ANCHOR_NAME_1, xpath, [:], jsonData, INCLUDE_ALL_DESCENDANTS)
+            def deltaReport = objectUnderTest.getDeltaByDataspaceAnchorAndPayload(dataspaceName, ANCHOR_NAME_1, xpath, [:], jsonData, INCLUDE_ALL_DESCENDANTS, GROUPING_DISABLED)
         then: 'cps data service is invoked and returns source data nodes'
             mockCpsDataService.getDataNodesForMultipleXpaths(dataspaceName, ANCHOR_NAME_1, [xpath], INCLUDE_ALL_DESCENDANTS) >> sourceDataNodes
         and: 'source data nodes are rebuilt (to match the data type with target data nodes)'
@@ -234,7 +244,7 @@ class CpsDeltaServiceImplSpec extends Specification {
             def yangResourceContentPerName = TestUtils.getYangResourcesAsMap('bookstore.yang')
             setupSchemaSetMocksForDelta(yangResourceContentPerName)
         when: 'attempt to get delta between anchor and payload'
-            objectUnderTest.getDeltaByDataspaceAnchorAndPayload(dataspaceName, ANCHOR_NAME_1, xpath, yangResourceContentPerName, jsonData, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS)
+            objectUnderTest.getDeltaByDataspaceAnchorAndPayload(dataspaceName, ANCHOR_NAME_1, xpath, yangResourceContentPerName, jsonData, INCLUDE_ALL_DESCENDANTS, GROUPING_DISABLED)
         then: 'expected exception is thrown'
             thrown(DataValidationException)
         where: 'following parameters were used'
@@ -246,6 +256,43 @@ class CpsDeltaServiceImplSpec extends Specification {
             'empty json data with xpath'               | '/bookstore/categories[@code=\'02\']' | '{}'
     }
 
+    def 'Get Delta Report between anchors with grouping of data nodes enabled and data node with #scenario'() {
+        given: 'xpath and source data node'
+            def xpath = '/parent'
+            def sourceDataNodes  = []
+        when: 'attempt to get delta between 2 anchors'
+            def deltaReport = objectUnderTest.getDeltaByDataspaceAndAnchors(dataspaceName, ANCHOR_NAME_1, ANCHOR_NAME_2, xpath, INCLUDE_ALL_DESCENDANTS, GROUPING_ENABLED)
+        then: 'cps data service is invoked and returns source data nodes'
+            mockCpsDataService.getDataNodesForMultipleXpaths(dataspaceName, ANCHOR_NAME_1, [xpath], INCLUDE_ALL_DESCENDANTS) >> sourceDataNodes
+        and: 'cps data service is invoked again to return target data nodes'
+            mockCpsDataService.getDataNodesForMultipleXpaths(dataspaceName, ANCHOR_NAME_2, [xpath], INCLUDE_ALL_DESCENDANTS) >> targetDataNodes
+        and: 'the delta report contains expected "create" action'
+            assert deltaReport[0].action == 'create'
+        and: 'the delta report contains expected xpath'
+            assert deltaReport[0].xpath == '/parent'
+        and: 'the delta report does not contain any source data'
+            assert deltaReport[0].sourceData == null
+        and: 'the delta report contains expected target data, with child data node information included under same delta report entry'
+            assert deltaReport[0].targetData == expectedTargetData
+        where: 'following data was used'
+            scenario            | targetDataNodes         || expectedTargetData
+            'parent node xpath' | targetDataNode          || ['parent':['parent-leaf': 'parent-leaf-as-target-data']]
+            'xpath'             | targetDataNodeWithXpath || ['child':['child-leaf': 'child-leaf-as-target-data']]
+    }
+
+    def 'Delta Report between identical nodes, with grouping of data nodes enabled'() {
+        given: 'parent node xpath'
+            def xpath = '/parent'
+        when: 'attempt to get delta between 2 anchors'
+            def deltaReport = objectUnderTest.getDeltaByDataspaceAndAnchors(dataspaceName, ANCHOR_NAME_1, ANCHOR_NAME_2, xpath, INCLUDE_ALL_DESCENDANTS, GROUPING_ENABLED)
+        then: 'cps data service is invoked and returns source data nodes'
+            mockCpsDataService.getDataNodesForMultipleXpaths(dataspaceName, ANCHOR_NAME_1, [xpath], INCLUDE_ALL_DESCENDANTS) >> sourceDataNodeWithoutLeafData
+        and: 'cps data service is invoked again to return target data nodes'
+            mockCpsDataService.getDataNodesForMultipleXpaths(dataspaceName, ANCHOR_NAME_2, [xpath], INCLUDE_ALL_DESCENDANTS) >> targetDataNodeWithoutLeafData
+        and: 'the delta report contains expected details for parent node and child node'
+            assert deltaReport.isEmpty()
+    }
+
     def setupSchemaSetMocks(String... yangResources) {
         def mockYangTextSchemaSourceSet = Mock(YangTextSchemaSourceSet)
         mockYangTextSchemaSourceSetCache.get(dataspaceName, schemaSetName) >> mockYangTextSchemaSourceSet
index 62ac66a..a7663ce 100644 (file)
@@ -2249,6 +2249,15 @@ paths:
           default: none
           example: "3"
           type: string
+      - description: Boolean flag to enable or disable grouping of data nodes to generate
+          a condensed delta report.
+        in: query
+        name: grouping-enabled
+        required: false
+        schema:
+          default: false
+          example: false
+          type: boolean
       responses:
         "200":
           content:
@@ -2324,6 +2333,15 @@ paths:
         schema:
           default: /
           type: string
+      - description: Boolean flag to enable or disable grouping of data nodes to generate
+          a condensed delta report.
+        in: query
+        name: grouping-enabled
+        required: false
+        schema:
+          default: false
+          example: false
+          type: boolean
       requestBody:
         content:
           multipart/form-data:
@@ -3142,6 +3160,16 @@ components:
       schema:
         example: my-anchor
         type: string
+    groupingEnabledInQuery:
+      description: Boolean flag to enable or disable grouping of data nodes to generate
+        a condensed delta report.
+      in: query
+      name: grouping-enabled
+      required: false
+      schema:
+        default: false
+        example: false
+        type: boolean
     cpsPathInQuery:
       description: "For more details on cps path, please refer https://docs.onap.org/projects/onap-cps/en/latest/cps-path.html"
       examples:
index 691e714..5890450 100644 (file)
@@ -37,6 +37,7 @@ class DeltaServiceIntegrationSpec extends FunctionalSpecBase {
     static def INCLUDE_ALL_DESCENDANTS = FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
     static def OMIT_DESCENDANTS = FetchDescendantsOption.OMIT_DESCENDANTS
     static def DIRECT_CHILDREN_ONLY = FetchDescendantsOption.DIRECT_CHILDREN_ONLY
+    def NO_GROUPING = false
 
     def setup() {
         objectUnderTest = cpsDeltaService
@@ -47,7 +48,7 @@ class DeltaServiceIntegrationSpec extends FunctionalSpecBase {
 
     def 'Get delta between 2 anchors'() {
         when: 'attempt to get delta report between anchors'
-            def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_ANCHOR_3, BOOKSTORE_ANCHOR_5, '/', OMIT_DESCENDANTS)
+            def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_ANCHOR_3, BOOKSTORE_ANCHOR_5, '/', OMIT_DESCENDANTS, NO_GROUPING)
         and: 'report is ordered based on xpath'
             result = result.toList().sort { it.xpath }
         then: 'delta report contains expected number of changes'
@@ -65,7 +66,7 @@ class DeltaServiceIntegrationSpec extends FunctionalSpecBase {
 
     def 'Get delta between 2 anchors returns empty response when #scenario'() {
         when: 'attempt to get delta report between anchors'
-            def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_ANCHOR_3, targetAnchor, xpath, INCLUDE_ALL_DESCENDANTS)
+            def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_ANCHOR_3, targetAnchor, xpath, INCLUDE_ALL_DESCENDANTS, NO_GROUPING)
         then: 'delta report is empty'
             assert result.isEmpty()
         where: 'following data was used'
@@ -77,7 +78,7 @@ class DeltaServiceIntegrationSpec extends FunctionalSpecBase {
 
     def 'Get delta between anchors error scenario: #scenario'() {
         when: 'attempt to get delta between anchors'
-            objectUnderTest.getDeltaByDataspaceAndAnchors(dataspaceName, sourceAnchor, targetAnchor, '/some-xpath', INCLUDE_ALL_DESCENDANTS)
+            objectUnderTest.getDeltaByDataspaceAndAnchors(dataspaceName, sourceAnchor, targetAnchor, '/some-xpath', INCLUDE_ALL_DESCENDANTS, NO_GROUPING)
         then: 'expected exception is thrown'
             thrown(expectedException)
         where: 'following data was used'
@@ -93,7 +94,7 @@ class DeltaServiceIntegrationSpec extends FunctionalSpecBase {
 
     def 'Get delta between anchors for remove action, where source data node #scenario'() {
         when: 'attempt to get delta between leaves of data nodes present in 2 anchors'
-            def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_ANCHOR_5, BOOKSTORE_ANCHOR_3, parentNodeXpath, INCLUDE_ALL_DESCENDANTS)
+            def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_ANCHOR_5, BOOKSTORE_ANCHOR_3, parentNodeXpath, INCLUDE_ALL_DESCENDANTS, NO_GROUPING)
         then: 'expected action is present in delta report'
             assert result.get(0).getAction() == 'remove'
         where: 'following data was used'
@@ -106,7 +107,7 @@ class DeltaServiceIntegrationSpec extends FunctionalSpecBase {
 
     def 'Get delta between anchors for "create" action, where target data node #scenario'() {
         when: 'attempt to get delta between leaves of data nodes present in 2 anchors'
-            def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_ANCHOR_3, BOOKSTORE_ANCHOR_5, parentNodeXpath, INCLUDE_ALL_DESCENDANTS)
+            def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_ANCHOR_3, BOOKSTORE_ANCHOR_5, parentNodeXpath, INCLUDE_ALL_DESCENDANTS, NO_GROUPING)
         then: 'the expected action is present in delta report'
             result.get(0).getAction() == 'create'
         and: 'the expected xapth is present in delta report'
@@ -121,7 +122,7 @@ class DeltaServiceIntegrationSpec extends FunctionalSpecBase {
 
     def 'Get delta between anchors when leaves of existing data nodes are updated,: #scenario'() {
         when: 'attempt to get delta between leaves of existing data nodes'
-            def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, sourceAnchor, targetAnchor, xpath, OMIT_DESCENDANTS)
+            def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, sourceAnchor, targetAnchor, xpath, OMIT_DESCENDANTS, NO_GROUPING)
         then: 'expected action is "replace"'
             assert result[0].getAction() == 'replace'
         and: 'the payload has expected leaf values'
@@ -138,7 +139,7 @@ class DeltaServiceIntegrationSpec extends FunctionalSpecBase {
 
     def 'Get delta between anchors when child data nodes under existing parent data nodes are updated: #scenario'() {
         when: 'attempt to get delta between leaves of existing data nodes'
-            def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, sourceAnchor, targetAnchor, xpath, DIRECT_CHILDREN_ONLY)
+            def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, sourceAnchor, targetAnchor, xpath, DIRECT_CHILDREN_ONLY, NO_GROUPING)
         then: 'expected action is "replace"'
             assert result[0].getAction() == 'replace'
         and: 'the delta report has expected child node xpaths'
@@ -160,7 +161,7 @@ class DeltaServiceIntegrationSpec extends FunctionalSpecBase {
             def expectedSourceDataInChildNode = [['lang' : 'English'],['price':20, 'editions':[1988, 2000]]]
             def expectedTargetDataInChildNode = [['lang':'English/German'], ['price':200, 'editions':[1988, 2000, 2023]]]
         when: 'attempt to get delta between leaves of existing data nodes'
-            def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_ANCHOR_3, BOOKSTORE_ANCHOR_5, parentNodeXpath, INCLUDE_ALL_DESCENDANTS)
+            def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_ANCHOR_3, BOOKSTORE_ANCHOR_5, parentNodeXpath, INCLUDE_ALL_DESCENDANTS, NO_GROUPING)
             def deltaReportEntities = getDeltaReportEntities(result)
         then: 'expected action is "replace"'
             assert result[0].getAction() == 'replace'
@@ -179,7 +180,7 @@ class DeltaServiceIntegrationSpec extends FunctionalSpecBase {
     def 'Get delta between anchor and JSON payload'() {
         when: 'attempt to get delta report between anchor and JSON payload'
             def jsonPayload = '{\"book-store:bookstore\":{\"bookstore-name\":\"Crossword Bookstores\"},\"book-store:bookstore-address\":{\"address\":\"Bangalore, India\",\"postal-code\":\"560062\",\"bookstore-name\":\"Crossword Bookstores\"}}'
-            def result = objectUnderTest.getDeltaByDataspaceAnchorAndPayload(FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_ANCHOR_3, '/', [:], jsonPayload, OMIT_DESCENDANTS)
+            def result = objectUnderTest.getDeltaByDataspaceAnchorAndPayload(FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_ANCHOR_3, '/', [:], jsonPayload, OMIT_DESCENDANTS, NO_GROUPING)
         then: 'delta report contains expected number of changes'
             result.size() == 3
         and: 'delta report contains "replace" action with expected xpath'
@@ -196,14 +197,14 @@ class DeltaServiceIntegrationSpec extends FunctionalSpecBase {
     def 'Get delta between anchor and payload returns empty response when JSON payload is identical to anchor data'() {
         when: 'attempt to get delta report between anchor and JSON payload (replacing the string Easons with Easons-1 because the data in JSON file is modified, to append anchor number, during the setup process of the integration tests)'
             def jsonPayload = readResourceDataFile('bookstore/bookstoreData.json').replace('Easons', 'Easons-1')
-            def result = objectUnderTest.getDeltaByDataspaceAnchorAndPayload(FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_ANCHOR_3, '/', [:], jsonPayload, INCLUDE_ALL_DESCENDANTS)
+            def result = objectUnderTest.getDeltaByDataspaceAnchorAndPayload(FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_ANCHOR_3, '/', [:], jsonPayload, INCLUDE_ALL_DESCENDANTS, NO_GROUPING)
         then: 'delta report is empty'
             assert result.isEmpty()
     }
 
     def 'Get delta between anchor and payload error scenario: #scenario'() {
         when: 'attempt to get delta between anchor and json payload'
-            objectUnderTest.getDeltaByDataspaceAnchorAndPayload(dataspaceName, sourceAnchor, xpath, [:], jsonPayload, INCLUDE_ALL_DESCENDANTS)
+            objectUnderTest.getDeltaByDataspaceAnchorAndPayload(dataspaceName, sourceAnchor, xpath, [:], jsonPayload, INCLUDE_ALL_DESCENDANTS, NO_GROUPING)
         then: 'expected exception is thrown'
             thrown(expectedException)
         where: 'following data was used'