Part-2: Grouping of Data Nodes in Delta Report 73/141273/1
authorArpit Singh <AS00745003@techmahindra.com>
Tue, 18 Feb 2025 06:49:21 +0000 (12:19 +0530)
committerArpit Singh <AS00745003@techmahindra.com>
Fri, 13 Jun 2025 05:54:14 +0000 (11:24 +0530)
- Part two of new delta report format, proposed as an add-on feature to
  existing delta feature.
- Contains functionality of 'updated' and 'removed' delta report entries
  following the new Delta Report format
- The format for existing updated entries is modified as it is common
  between the two delta report formats
- A separate patch will be delivered in future to modify the format of
  existing delta report entries for added and removed data nodes to make
  it consistent with the new format.
- Documentation to Impacts and changes in old delta report format:
  https://lf-onap.atlassian.net/wiki/spaces/DW/pages/232194058/Proposed+changes+to+original+Delta+Report+Format

Issue-ID: CPS-2547
Change-Id: I489a5e8f0da7f012d5411870bccf886a987f600f
Signed-off-by: Arpit Singh <AS00745003@techmahindra.com>
cps-service/src/main/java/org/onap/cps/impl/CpsDeltaServiceImpl.java
cps-service/src/test/groovy/org/onap/cps/impl/CpsDeltaServiceImplSpec.groovy
integration-test/src/test/groovy/org/onap/cps/integration/functional/cps/DataServiceIntegrationSpec.groovy
integration-test/src/test/groovy/org/onap/cps/integration/functional/cps/DeltaServiceIntegrationSpec.groovy

index 727c6b7..c902c00 100644 (file)
@@ -27,6 +27,7 @@ import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -42,6 +43,7 @@ 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.CpsPathQuery;
 import org.onap.cps.cpspath.parser.CpsPathUtil;
 import org.onap.cps.utils.DataMapUtils;
 import org.onap.cps.utils.DataMapper;
@@ -103,7 +105,7 @@ public class CpsDeltaServiceImpl implements CpsDeltaService {
 
         final List<DeltaReport> deltaReport = new ArrayList<>();
         if (groupDataNodes) {
-            deltaReport.addAll(getCondensedAddedDeltaReports(sourceDataNodes, targetDataNodes));
+            deltaReport.addAll(getCondensedDeltaReports(sourceDataNodes, targetDataNodes));
         } else {
             final Map<String, DataNode> xpathToSourceDataNodes = convertToXPathToDataNodesMap(sourceDataNodes);
             final Map<String, DataNode> xpathToTargetDataNodes = convertToXPathToDataNodesMap(targetDataNodes);
@@ -135,58 +137,61 @@ public class CpsDeltaServiceImpl implements CpsDeltaService {
             final DataNode targetDataNode = xpathToTargetDataNodes.get(xpath);
             final List<DeltaReport> deltaReports;
             if (targetDataNode == null) {
-                deltaReports = getRemovedDeltaReports(xpath, sourceDataNode);
+                deltaReports = getDeltaReportsForRemove(xpath, sourceDataNode);
             } else {
-                deltaReports = getUpdatedDeltaReports(xpath, sourceDataNode, targetDataNode);
+                deltaReports = getDeltaReportsForUpdates(xpath, sourceDataNode, targetDataNode);
             }
             removedAndUpdatedDeltaReportEntries.addAll(deltaReports);
         }
         return removedAndUpdatedDeltaReportEntries;
     }
 
-    private static List<DeltaReport> getRemovedDeltaReports(final String xpath, final DataNode sourceDataNode) {
-        final List<DeltaReport> removedDeltaReportEntries = new ArrayList<>();
+    private static List<DeltaReport> getDeltaReportsForRemove(final String xpath, final DataNode sourceDataNode) {
+        final List<DeltaReport> deltaReportEntriesForRemove = new ArrayList<>();
         final Map<String, Serializable> sourceDataNodeLeaves = sourceDataNode.getLeaves();
         final DeltaReport removedDeltaReportEntry = new DeltaReportBuilder().actionRemove().withXpath(xpath)
                 .withSourceData(sourceDataNodeLeaves).build();
-        removedDeltaReportEntries.add(removedDeltaReportEntry);
-        return removedDeltaReportEntries;
+        deltaReportEntriesForRemove.add(removedDeltaReportEntry);
+        return deltaReportEntriesForRemove;
     }
 
-    private static List<DeltaReport> getUpdatedDeltaReports(final String xpath, final DataNode sourceDataNode,
-                                                            final DataNode targetDataNode) {
-        final List<DeltaReport> updatedDeltaReportEntries = new ArrayList<>();
-        final Map<Map<String, Serializable>, Map<String, Serializable>> updatedLeavesAsSourceDataToTargetData =
-                getUpdatedLeavesBetweenSourceAndTargetDataNode(sourceDataNode.getLeaves(), targetDataNode.getLeaves());
-        addUpdatedLeavesToDeltaReport(xpath, updatedLeavesAsSourceDataToTargetData, updatedDeltaReportEntries);
-        return updatedDeltaReportEntries;
+    private static List<DeltaReport> getDeltaReportsForUpdates(final String xpath, final DataNode sourceDataNode,
+                                                               final DataNode targetDataNode) {
+        final List<DeltaReport> deltaReportEntriesForUpdates = new ArrayList<>();
+        final Map<Map<String, Serializable>, Map<String, Serializable>> updatedSourceDataToTargetData =
+                getUpdatedSourceAndTargetDataNode(sourceDataNode, targetDataNode);
+        if (!updatedSourceDataToTargetData.isEmpty()) {
+            addUpdatedDataToDeltaReport(xpath, updatedSourceDataToTargetData, deltaReportEntriesForUpdates);
+        }
+        return deltaReportEntriesForUpdates;
     }
 
-    private static Map<Map<String, Serializable>,
-            Map<String, Serializable>> getUpdatedLeavesBetweenSourceAndTargetDataNode(
-                                                            final Map<String, Serializable> leavesOfSourceDataNode,
-                                                            final Map<String, Serializable> leavesOfTargetDataNode) {
-        final Map<Map<String, Serializable>, Map<String, Serializable>> updatedLeavesAsSourceDataToTargetData =
-                new LinkedHashMap<>();
-        final Map<String, Serializable> sourceDataInDeltaReport = new LinkedHashMap<>();
-        final Map<String, Serializable> targetDataInDeltaReport = new LinkedHashMap<>();
-        processLeavesPresentInSourceAndTargetDataNode(leavesOfSourceDataNode, leavesOfTargetDataNode,
-                sourceDataInDeltaReport, targetDataInDeltaReport);
-        processLeavesUniqueInTargetDataNode(leavesOfSourceDataNode, leavesOfTargetDataNode,
-                sourceDataInDeltaReport, targetDataInDeltaReport);
-        final boolean isUpdatedDataInDeltaReport =
-                !sourceDataInDeltaReport.isEmpty() || !targetDataInDeltaReport.isEmpty();
-        if (isUpdatedDataInDeltaReport) {
-            updatedLeavesAsSourceDataToTargetData.put(sourceDataInDeltaReport, targetDataInDeltaReport);
+    private static Map<Map<String, Serializable>, Map<String, Serializable>> getUpdatedSourceAndTargetDataNode(
+                                                            final DataNode sourceDataNode,
+                                                            final DataNode targetDataNode) {
+        final Map<String, Serializable> updatedLeavesInSourceData = new HashMap<>();
+        final Map<String, Serializable> updatedLeavesInTargetData = new HashMap<>();
+        processSourceAndTargetDataNode(sourceDataNode, targetDataNode,
+            updatedLeavesInSourceData, updatedLeavesInTargetData);
+        processUniqueDataInTargetDataNode(sourceDataNode, targetDataNode,
+            updatedLeavesInSourceData, updatedLeavesInTargetData);
+        final Map<String, Serializable> updatedSourceData =
+            getUpdatedNodeData(sourceDataNode, updatedLeavesInSourceData);
+        final Map<String, Serializable> updatedTargetData =
+            getUpdatedNodeData(targetDataNode, updatedLeavesInTargetData);
+        if (updatedSourceData.isEmpty() && updatedTargetData.isEmpty()) {
+            return Collections.emptyMap();
         }
-        return updatedLeavesAsSourceDataToTargetData;
+        return Collections.singletonMap(updatedSourceData, updatedTargetData);
     }
 
-    private static void processLeavesPresentInSourceAndTargetDataNode(
-                                                            final Map<String, Serializable> leavesOfSourceDataNode,
-                                                            final Map<String, Serializable> leavesOfTargetDataNode,
+    private static void processSourceAndTargetDataNode(
+                                                            final DataNode sourceDataNode,
+                                                            final DataNode targetDataNode,
                                                             final Map<String, Serializable> sourceDataInDeltaReport,
                                                             final Map<String, Serializable> targetDataInDeltaReport) {
+        final Map<String, Serializable> leavesOfSourceDataNode = sourceDataNode.getLeaves();
+        final Map<String, Serializable> leavesOfTargetDataNode = targetDataNode.getLeaves();
         for (final Map.Entry<String, Serializable> entry: leavesOfSourceDataNode.entrySet()) {
             final String key = entry.getKey();
             final Serializable sourceLeaf = entry.getValue();
@@ -195,13 +200,14 @@ public class CpsDeltaServiceImpl implements CpsDeltaService {
         }
     }
 
-    private static void processLeavesUniqueInTargetDataNode(
-                                                            final Map<String, Serializable> leavesOfSourceDataNode,
-                                                            final Map<String, Serializable> leavesOfTargetDataNode,
+    private static void processUniqueDataInTargetDataNode(
+                                                            final DataNode sourceDataNode,
+                                                            final DataNode targetDataNode,
                                                             final Map<String, Serializable> sourceDataInDeltaReport,
                                                             final Map<String, Serializable> targetDataInDeltaReport) {
+        final Map<String, Serializable> leavesOfSourceDataNode = sourceDataNode.getLeaves();
         final Map<String, Serializable> uniqueLeavesOfTargetDataNode =
-                new LinkedHashMap<>(leavesOfTargetDataNode);
+                new HashMap<>(targetDataNode.getLeaves());
         uniqueLeavesOfTargetDataNode.keySet().removeAll(leavesOfSourceDataNode.keySet());
         for (final Map.Entry<String, Serializable> entry: uniqueLeavesOfTargetDataNode.entrySet()) {
             final String key = entry.getKey();
@@ -228,15 +234,50 @@ public class CpsDeltaServiceImpl implements CpsDeltaService {
         }
     }
 
-    private static void addUpdatedLeavesToDeltaReport(final String xpath,
-                                                      final Map<Map<String, Serializable>, Map<String,
-                                                              Serializable>> updatedLeavesAsSourceDataToTargetData,
-                                                      final List<DeltaReport> updatedDeltaReportEntries) {
+    private static Map<String, Serializable> getUpdatedNodeData(final DataNode dataNode,
+                                                                final Map<String, Serializable> updatedLeaves) {
+        final Map<String, Serializable> updatedSourceData = new HashMap<>();
+        if (!updatedLeaves.isEmpty()) {
+            final String xpath = dataNode.getXpath();
+            if (CpsPathUtil.isPathToListElement(xpath)) {
+                addKeyLeavesToUpdatedData(xpath, updatedLeaves);
+            }
+            final Collection<DataNode> updatedDataNode = buildUpdatedDataNode(dataNode, updatedLeaves);
+            updatedSourceData.putAll(getCondensedDataForDeltaReport(updatedDataNode));
+        }
+        return updatedSourceData;
+    }
+
+    private static void addKeyLeavesToUpdatedData(final String xpath,
+                                                  final Map<String, Serializable> updatedLeaves) {
+        final Map<String, Serializable> keyLeaves = new HashMap<>();
+        final List<CpsPathQuery.LeafCondition> leafConditions = CpsPathUtil.getCpsPathQuery(xpath).getLeafConditions();
+        for (final CpsPathQuery.LeafCondition leafCondition: leafConditions) {
+            final String leafName = leafCondition.name();
+            final Serializable leafValue = (Serializable) leafCondition.value();
+            keyLeaves.put(leafName, leafValue);
+        }
+        updatedLeaves.putAll(keyLeaves);
+    }
+
+    private static Collection<DataNode> buildUpdatedDataNode(final DataNode dataNode,
+                                                             final Map<String, Serializable> updatedLeaves) {
+        final DataNode updatedDataNode = new DataNodeBuilder()
+            .withXpath(dataNode.getXpath())
+            .withModuleNamePrefix(dataNode.getModuleNamePrefix())
+            .withLeaves(updatedLeaves)
+            .build();
+        return Collections.singletonList(updatedDataNode);
+    }
+
+    private static void addUpdatedDataToDeltaReport(final String xpath,
+                        final Map<Map<String, Serializable>, Map<String, Serializable>> updatedSourceDataToTargetData,
+                        final List<DeltaReport> deltaReportEntriesForUpdates) {
         for (final Map.Entry<Map<String, Serializable>, Map<String, Serializable>> entry:
-                updatedLeavesAsSourceDataToTargetData.entrySet()) {
-            final DeltaReport updatedDataForDeltaReport = new DeltaReportBuilder().actionReplace()
-                    .withXpath(xpath).withSourceData(entry.getKey()).withTargetData(entry.getValue()).build();
-            updatedDeltaReportEntries.add(updatedDataForDeltaReport);
+            updatedSourceDataToTargetData.entrySet()) {
+            final DeltaReport updatedDataForDeltaReport = new DeltaReportBuilder().actionReplace().withXpath(xpath)
+                .withSourceData(entry.getKey()).withTargetData(entry.getValue()).build();
+            deltaReportEntriesForUpdates.add(updatedDataForDeltaReport);
         }
     }
 
@@ -282,6 +323,55 @@ public class CpsDeltaServiceImpl implements CpsDeltaService {
         }
     }
 
+    private static List<DeltaReport> getCondensedDeltaReports(final Collection<DataNode> sourceDataNodes,
+                                                              final Collection<DataNode> targetDataNodes) {
+
+        final List<DeltaReport> deltaReportEntries = new ArrayList<>();
+        final Map<String, DataNode> xpathToTargetDataNodes = flattenToXpathToFirstLevelDataNodeMap(targetDataNodes);
+        deltaReportEntries.addAll(getCondensedRemovedDeltaReports(sourceDataNodes, xpathToTargetDataNodes));
+        deltaReportEntries.addAll(getCondensedUpdatedDeltaReports(sourceDataNodes, xpathToTargetDataNodes));
+        deltaReportEntries.addAll(getCondensedAddedDeltaReports(sourceDataNodes, targetDataNodes));
+        return deltaReportEntries;
+    }
+
+    private static List<DeltaReport> getCondensedRemovedDeltaReports(final Collection<DataNode> sourceDataNodes,
+                                                                   final Map<String, DataNode> xpathToTargetDataNodes) {
+
+        final List<DeltaReport> deltaReportEntriesForRemove = new ArrayList<>();
+        final Collection<DataNode> removedDataNodes =
+            getDataNodesForDeltaReport(sourceDataNodes, xpathToTargetDataNodes);
+        if (!removedDataNodes.isEmpty()) {
+            final String xpath = getXpathForDeltaReport(removedDataNodes);
+            deltaReportEntriesForRemove.add(new DeltaReportBuilder().actionRemove().withXpath(xpath)
+                .withSourceData(getCondensedDataForDeltaReport(removedDataNodes)).build());
+        }
+        return deltaReportEntriesForRemove;
+    }
+
+    private static List<DeltaReport> getCondensedUpdatedDeltaReports(final Collection<DataNode> sourceDataNodes,
+                                                                   final Map<String, DataNode> xpathToTargetDataNodes) {
+        final List<DeltaReport> deltaReportEntriesForUpdates = new ArrayList<>();
+        for (final DataNode sourceDataNode : sourceDataNodes) {
+            final String xpath = sourceDataNode.getXpath();
+            if (xpathToTargetDataNodes.containsKey(xpath)) {
+                final DataNode targetDataNode = xpathToTargetDataNodes.get(xpath);
+                deltaReportEntriesForUpdates.addAll(getDeltaReportsForUpdates(xpath, sourceDataNode, targetDataNode));
+                getCondensedDeltaReportsForChildDataNodes(sourceDataNode, targetDataNode, deltaReportEntriesForUpdates);
+            }
+        }
+        return deltaReportEntriesForUpdates;
+    }
+
+    private static void getCondensedDeltaReportsForChildDataNodes(final DataNode sourceDataNode,
+                                                                  final DataNode targetDataNode,
+                                                                  final List<DeltaReport> deltaReportEntries) {
+        final Collection<DataNode> childrenOfSourceDataNodes = sourceDataNode.getChildDataNodes();
+        final Collection<DataNode> childrenOfTargetDataNodes = targetDataNode.getChildDataNodes();
+        if (!childrenOfSourceDataNodes.isEmpty() || !childrenOfTargetDataNodes.isEmpty()) {
+            deltaReportEntries.addAll(getCondensedDeltaReports(childrenOfSourceDataNodes, childrenOfTargetDataNodes));
+        }
+    }
+
     private static List<DeltaReport> getCondensedAddedDeltaReports(final Collection<DataNode> sourceDataNodes,
             final Collection<DataNode> targetDataNodes) {
 
index d979be4..82fc893 100644 (file)
@@ -62,6 +62,7 @@ class CpsDeltaServiceImplSpec extends Specification {
 
     static def bookstoreDataNodeWithParentXpath = [new DataNode(xpath: '/bookstore', leaves: ['bookstore-name': 'Easons'])]
     static def bookstoreDataNodeWithChildXpath = [new DataNode(xpath: '/bookstore/categories[@code=\'02\']', leaves: ['code': '02', 'name': 'Kids'])]
+    static def bookstoreDataNodesWithChildXpathAndNoLeaves = [new DataNode(xpath: '/bookstore/categories[@code=\'02\']')]
     static def bookstoreDataAsMapForParentNode = [bookstore: ['bookstore-name': 'Easons']]
     static def bookstoreDataAsMapForChildNode = [categories: ['code': '02', 'name': 'Kids']]
     static def bookstoreJsonForParentNode = '{"bookstore":{"bookstore-name":"My Store"}}'
@@ -71,10 +72,11 @@ class CpsDeltaServiceImplSpec extends Specification {
     static def sourceDataNodeWithoutLeafData = [new DataNode(xpath: '/parent')]
     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'])]
+    static def expectedParentSourceData = ['parent':['parent-leaf':'parent-leaf-as-source-data']]
+    static def expectedParentTargetData = ['parent':['parent-leaf':'parent-leaf-as-target-data']]
 
     def logger = (Logger) LoggerFactory.getLogger(objectUnderTest.class)
     def loggingListAppender
@@ -87,8 +89,8 @@ class CpsDeltaServiceImplSpec extends Specification {
     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
+    static def GROUPING_ENABLED = true
+    static def GROUPING_DISABLED = false
 
     def setup() {
         mockCpsAnchorService.getAnchor(dataspaceName, ANCHOR_NAME_1) >> anchor1
@@ -105,11 +107,11 @@ class CpsDeltaServiceImplSpec extends Specification {
         applicationContext.close()
     }
 
-    def 'Get Delta between 2 anchors for #scenario with grouping of data nodes disabled'() {
+    def 'Get Delta between 2 anchors where data node is #scenario'() {
         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, OMIT_DESCENDANTS, GROUPING_DISABLED)
+            def deltaReport = objectUnderTest.getDeltaByDataspaceAndAnchors(dataspaceName, ANCHOR_NAME_1, ANCHOR_NAME_2, xpath, OMIT_DESCENDANTS, groupDataNodes)
         then: 'cps data service is invoked and returns source data nodes'
             mockCpsDataService.getDataNodesForMultipleXpaths(dataspaceName, ANCHOR_NAME_1, [xpath], OMIT_DESCENDANTS) >> sourceDataNodes
         and: 'cps data service is invoked again to return target data nodes'
@@ -121,14 +123,17 @@ 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'   | []              | 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']
+            scenario                         | sourceDataNodes          | targetDataNodes         | groupDataNodes    || expectedAction | expectedSourceData                                                                                            | expectedTargetData
+            'added with grouping disabled'   | []                       | targetDataNode          | GROUPING_DISABLED || 'create'       | null                                                                                                          | ['parent-leaf':'parent-leaf-as-target-data']
+            'removed with grouping disabled' | sourceDataNode           | []                      | GROUPING_DISABLED || 'remove'       | ['parent-leaf':'parent-leaf-as-source-data']                                                                  | null
+            'updated with grouping disabled' | sourceDataNode           | targetDataNode          | GROUPING_DISABLED || 'replace'      | ['parent':['parent-leaf':'parent-leaf-as-source-data']]                                                       | ['parent':['parent-leaf':'parent-leaf-as-target-data']]
+            'added with grouping enabled'    | []                       | targetDataNodeWithChild | GROUPING_ENABLED  || 'create'       | null                                                                                                          | ['parent':['parent-leaf': 'parent-leaf-as-target-data', 'child':['child-leaf': 'child-leaf-as-target-data']]]
+            'removed with grouping enabled'  | sourceDataNodeWithChild  | []                      | GROUPING_ENABLED  || 'remove'       | ['parent':['parent-leaf': 'parent-leaf-as-source-data', 'child':['child-leaf': 'child-leaf-as-source-data']]] | null
+            'updated with grouping enabled'  | sourceDataNode           | targetDataNode          | GROUPING_ENABLED  || 'replace'      | ['parent':['parent-leaf': 'parent-leaf-as-source-data']]                                                      | ['parent':['parent-leaf': 'parent-leaf-as-target-data']]
     }
 
-    def 'Delta Report between parent nodes containing child nodes where #scenario with grouping of data nodes disabled'() {
-        given: 'root node xpath'
+    def 'Delta Report between parent nodes with children where data node is #scenario without grouping of data nodes'() {
+        given: 'root node xpath and expected source and target data'
             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, GROUPING_DISABLED)
@@ -149,10 +154,38 @@ class CpsDeltaServiceImplSpec extends Specification {
             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']
+            scenario  | sourceDataNodes         | targetDataNodes         || expectedAction | expectedSourceDataForParent                   | expectedTargetDataForParent                   | expectedSourceDataForChild                            | expectedTargetDataForChild
+            'added'   | []                      | targetDataNodeWithChild || 'create'       | null                                          | ['parent-leaf': 'parent-leaf-as-target-data'] | null                                                  | ['child-leaf': 'child-leaf-as-target-data']
+            'removed' | sourceDataNodeWithChild | []                      || 'remove'       | ['parent-leaf': 'parent-leaf-as-source-data'] | null                                          | ['child-leaf': 'child-leaf-as-source-data']           | null
+            'updated' | sourceDataNodeWithChild | targetDataNodeWithChild || 'replace'      | expectedParentSourceData                      | expectedParentTargetData                      | ['child':['child-leaf': 'child-leaf-as-source-data']] | ['child':['child-leaf': 'child-leaf-as-target-data']]
+    }
+
+    def 'Delta Report between parent nodes with children where parent is updated and child node is #scenario with grouping of data nodes'() {
+        given: 'root node xpath and expected source and target data'
+            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, 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: 'delta report contains correct number of entries'
+            deltaReport.size() == 2
+        and: 'the delta report contains expected details for parent node'
+            assert deltaReport[0].action == 'replace'
+            assert deltaReport[0].xpath == '/parent'
+            assert deltaReport[0].sourceData == expectedParentSourceData
+            assert deltaReport[0].targetData == expectedParentTargetData
+        and: 'the delta report contains expected details for child node'
+            assert deltaReport[1].action == expectedChildAction
+            assert deltaReport[1].xpath == expectedChildXpath
+            assert deltaReport[1].sourceData == expectedSourceDataForChild
+            assert deltaReport[1].targetData == expectedTargetDataForChild
+        where: 'the following data is used'
+            scenario  | sourceDataNodes         | targetDataNodes         || expectedChildAction | expectedChildXpath | expectedSourceDataForChild                            | expectedTargetDataForChild
+            'added'   | sourceDataNode          | targetDataNodeWithChild || 'create'            | '/parent'          | null                                                  | ['child':['child-leaf': 'child-leaf-as-target-data']]
+            'removed' | sourceDataNodeWithChild | targetDataNode          || 'remove'            | '/parent'          | ['child':['child-leaf':'child-leaf-as-source-data']]  | null
+            'updated' | sourceDataNodeWithChild | targetDataNodeWithChild || 'replace'           | '/parent/child'    | ['child':['child-leaf': 'child-leaf-as-source-data']] | ['child':['child-leaf': 'child-leaf-as-target-data']]
     }
 
     def 'Delta report between leaves, #scenario'() {
@@ -172,11 +205,11 @@ class CpsDeltaServiceImplSpec extends Specification {
         assert deltaReport[0].sourceData == expectedSourceData
         assert deltaReport[0].targetData == expectedTargetData
     where: 'the following data was used'
-        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']
+        scenario                                           | sourceDataNodes                  | targetDataNodes                  || expectedSourceData                                                    | expectedTargetData
+        'source and target data nodes have leaves'         | sourceDataNode                   | targetDataNode                   || ['parent':['parent-leaf':'parent-leaf-as-source-data']]               | ['parent':['parent-leaf':'parent-leaf-as-target-data']]
+        'only source data node has leaves'                 | sourceDataNode                   | targetDataNodeWithoutLeafData    || ['parent':['parent-leaf':'parent-leaf-as-source-data']]               | null
+        'only target data node has leaves'                 | sourceDataNodeWithoutLeafData    | targetDataNode                   || null                                                                  | ['parent':['parent-leaf':'parent-leaf-as-target-data']]
+        'source and target data node with multiple leaves' | sourceDataNodeWithMultipleLeaves | targetDataNodeWithMultipleLeaves || ['parent':['leaf-1':'leaf-1-in-source', 'leaf-2':'leaf-2-in-source']] | ['parent':['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 '() {
@@ -210,10 +243,10 @@ class CpsDeltaServiceImplSpec extends Specification {
             deltaReport[0].getSourceData().equals(expectedSourceData)
             deltaReport[0].getTargetData().equals(expectedTargetData)
         where: 'following data was used'
-            scenario          | xpath                                 | sourceDataNodes                  | sourceDataNodesAsMap            | jsonData                   || expectedNodeXpath                    | expectedSourceData          | expectedTargetData
-            'root node xpath' | '/'                                   | bookstoreDataNodeWithParentXpath | bookstoreDataAsMapForParentNode | bookstoreJsonForParentNode || '/bookstore'                         | ['bookstore-name':'Easons'] | ['bookstore-name':'My Store']
-            'parent xpath'    | '/bookstore'                          | bookstoreDataNodeWithParentXpath | bookstoreDataAsMapForParentNode | bookstoreJsonForParentNode || '/bookstore'                         | ['bookstore-name':'Easons'] | ['bookstore-name':'My Store']
-            'non-root xpath'  | '/bookstore/categories[@code=\'02\']' | bookstoreDataNodeWithChildXpath  | bookstoreDataAsMapForChildNode  | bookstoreJsonForChildNode  || '/bookstore/categories[@code=\'02\']'| ['name':'Kids']             | ['name':'Child']
+            scenario          | xpath                                 | sourceDataNodes                  | sourceDataNodesAsMap            | jsonData                   || expectedNodeXpath                    | expectedSourceData                            | expectedTargetData
+            'root node xpath' | '/'                                   | bookstoreDataNodeWithParentXpath | bookstoreDataAsMapForParentNode | bookstoreJsonForParentNode || '/bookstore'                         | ['bookstore':['bookstore-name':'Easons']]     | ['bookstore':['bookstore-name':'My Store']]
+            'parent xpath'    | '/bookstore'                          | bookstoreDataNodeWithParentXpath | bookstoreDataAsMapForParentNode | bookstoreJsonForParentNode || '/bookstore'                         | ['bookstore':['bookstore-name':'Easons']]     | ['bookstore':['bookstore-name':'My Store']]
+            'non-root xpath'  | '/bookstore/categories[@code=\'02\']' | bookstoreDataNodeWithChildXpath  | bookstoreDataAsMapForChildNode  | bookstoreJsonForChildNode  || '/bookstore/categories[@code=\'02\']'| ['categories':[['code':'02', 'name':'Kids']]] | ['categories':[['code':'02', 'name':'Child']]]
     }
 
     def 'Get delta between anchor and payload by using schema from anchor #scenario'() {
@@ -233,10 +266,10 @@ class CpsDeltaServiceImplSpec extends Specification {
             deltaReport[0].getSourceData().equals(expectedSourceData)
             deltaReport[0].getTargetData().equals(expectedTargetData)
         where: 'following data was used'
-             scenario         | xpath                                 | sourceDataNodes                  | sourceDataNodesAsMap            | jsonData                   || expectedNodeXpath                     | expectedSourceData          | expectedTargetData
-            'root node xpath' | '/'                                   | bookstoreDataNodeWithParentXpath | bookstoreDataAsMapForParentNode | bookstoreJsonForParentNode || '/bookstore'                          | ['bookstore-name':'Easons'] | ['bookstore-name':'My Store']
-            'parent xpath'    | '/bookstore'                          | bookstoreDataNodeWithParentXpath | bookstoreDataAsMapForParentNode | bookstoreJsonForParentNode || '/bookstore'                          | ['bookstore-name':'Easons'] | ['bookstore-name':'My Store']
-            'non-root xpath'  | '/bookstore/categories[@code=\'02\']' | bookstoreDataNodeWithChildXpath  | bookstoreDataAsMapForChildNode  | bookstoreJsonForChildNode  || '/bookstore/categories[@code=\'02\']' | ['name':'Kids']             | ['name':'Child']
+             scenario         | xpath                                 | sourceDataNodes                  | sourceDataNodesAsMap            | jsonData                   || expectedNodeXpath                     | expectedSourceData                            | expectedTargetData
+            'root node xpath' | '/'                                   | bookstoreDataNodeWithParentXpath | bookstoreDataAsMapForParentNode | bookstoreJsonForParentNode || '/bookstore'                          | ['bookstore':['bookstore-name':'Easons']]     | ['bookstore':['bookstore-name':'My Store']]
+            'parent xpath'    | '/bookstore'                          | bookstoreDataNodeWithParentXpath | bookstoreDataAsMapForParentNode | bookstoreJsonForParentNode || '/bookstore'                          | ['bookstore':['bookstore-name':'Easons']]     | ['bookstore':['bookstore-name':'My Store']]
+            'non-root xpath'  | '/bookstore/categories[@code=\'02\']' | bookstoreDataNodeWithChildXpath  | bookstoreDataAsMapForChildNode  | bookstoreJsonForChildNode  || '/bookstore/categories[@code=\'02\']' | ['categories':[['code':'02', 'name':'Kids']]] | ['categories':[['code':'02', 'name':'Child']]]
     }
 
     def 'Delta between anchor and payload error scenario #scenario'() {
@@ -256,41 +289,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 'Delta Report between identical nodes, with grouping of data nodes #scenario'() {
+        given: 'parent node xpath'
             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)
+            def deltaReport = objectUnderTest.getDeltaByDataspaceAndAnchors(dataspaceName, ANCHOR_NAME_1, ANCHOR_NAME_2, xpath, INCLUDE_ALL_DESCENDANTS, groupDataNodes)
         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], 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) >> 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']]
+            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()
+        where:
+            scenario   | groupDataNodes
+            'enabled'  | GROUPING_ENABLED
+            'disabled' | GROUPING_DISABLED
     }
 
-    def 'Delta Report between identical nodes, with grouping of data nodes enabled'() {
-        given: 'parent node xpath'
-            def xpath = '/parent'
+    def 'Delta Report between data nodes with list node xpath where leaf data is #scenario with grouping of data nodes enabled'() {
+        given: 'root node xpath'
+            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, GROUPING_ENABLED)
         then: 'cps data service is invoked and returns source data nodes'
-            mockCpsDataService.getDataNodesForMultipleXpaths(dataspaceName, ANCHOR_NAME_1, [xpath], INCLUDE_ALL_DESCENDANTS) >> sourceDataNodeWithoutLeafData
+            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) >> targetDataNodeWithoutLeafData
-        and: 'the delta report contains expected details for parent node and child node'
-            assert deltaReport.isEmpty()
+            mockCpsDataService.getDataNodesForMultipleXpaths(dataspaceName, ANCHOR_NAME_2, [xpath], INCLUDE_ALL_DESCENDANTS) >> targetDataNodes
+        and: 'delta report contains correct number of entries'
+            deltaReport.size() == 1
+        and: 'the delta report contains expected details for updated node'
+            assert deltaReport[0].action == 'replace'
+            assert deltaReport[0].xpath == "/bookstore/categories[@code='02']"
+            assert deltaReport[0].sourceData == expectedSourceData
+            assert deltaReport[0].targetData == expectedTargetData
+        where: 'the following data is used'
+            scenario  | sourceDataNodes                             | targetDataNodes                             || expectedSourceData                               | expectedTargetData
+            'added'   | bookstoreDataNodeWithChildXpath             | bookstoreDataNodesWithChildXpathAndNoLeaves || ['categories': [['code': '02', 'name': 'Kids']]] | null
+            'removed' | bookstoreDataNodesWithChildXpathAndNoLeaves | bookstoreDataNodeWithChildXpath             || null                                             | ['categories': [['code': '02', 'name': 'Kids']]]
     }
 
     def setupSchemaSetMocks(String... yangResources) {
index 6ecc3a5..c7f7b7d 100644 (file)
@@ -22,8 +22,6 @@
 package org.onap.cps.integration.functional.cps
 
 import org.onap.cps.api.CpsDataService
-import org.onap.cps.integration.base.FunctionalSpecBase
-import org.onap.cps.api.parameters.FetchDescendantsOption
 import org.onap.cps.api.exceptions.AlreadyDefinedException
 import org.onap.cps.api.exceptions.AnchorNotFoundException
 import org.onap.cps.api.exceptions.CpsPathException
@@ -31,6 +29,8 @@ import org.onap.cps.api.exceptions.DataNodeNotFoundException
 import org.onap.cps.api.exceptions.DataNodeNotFoundExceptionBatch
 import org.onap.cps.api.exceptions.DataValidationException
 import org.onap.cps.api.exceptions.DataspaceNotFoundException
+import org.onap.cps.api.parameters.FetchDescendantsOption
+import org.onap.cps.integration.base.FunctionalSpecBase
 import org.onap.cps.utils.ContentType
 
 import static org.onap.cps.api.parameters.FetchDescendantsOption.DIRECT_CHILDREN_ONLY
index 5890450..9f30b5a 100644 (file)
@@ -128,13 +128,13 @@ class DeltaServiceIntegrationSpec extends FunctionalSpecBase {
         and: 'the payload has expected leaf values'
             def sourceData = result[0].getSourceData()
             def targetData = result[0].getTargetData()
-            assert sourceData == expectedSourceValue
-            assert targetData == expectedTargetValue
+            assert sourceData.equals(expectedSourceValue)
+            assert targetData.equals(expectedTargetValue)
         where: 'following data was used'
-            scenario                           | sourceAnchor       | targetAnchor       | xpath                                                         || expectedSourceValue            | expectedTargetValue
-            'leaf is updated in target anchor' | BOOKSTORE_ANCHOR_3 | BOOKSTORE_ANCHOR_5 | '/bookstore'                                                  || ['bookstore-name': 'Easons-1'] | ['bookstore-name': 'Crossword Bookstores']
-            'leaf is removed in target anchor' | BOOKSTORE_ANCHOR_3 | BOOKSTORE_ANCHOR_5 | '/bookstore/categories[@code=\'5\']/books[@title=\'Book 1\']' || [price:1]                      | null
-            'leaf is added in target anchor'   | BOOKSTORE_ANCHOR_5 | BOOKSTORE_ANCHOR_3 | '/bookstore/categories[@code=\'5\']/books[@title=\'Book 1\']' || null                           | [price:1]
+            scenario                           | sourceAnchor       | targetAnchor       | xpath                                                         || expectedSourceValue                         | expectedTargetValue
+            'leaf is updated in target anchor' | BOOKSTORE_ANCHOR_3 | BOOKSTORE_ANCHOR_5 | '/bookstore'                                                  || ['bookstore':['bookstore-name':'Easons-1']] | ['bookstore':['bookstore-name': 'Crossword Bookstores']]
+            'leaf is removed in target anchor' | BOOKSTORE_ANCHOR_3 | BOOKSTORE_ANCHOR_5 | '/bookstore/categories[@code=\'5\']/books[@title=\'Book 1\']' || ['books':[['price':1, 'title':'Book 1']]]   | null
+            'leaf is added in target anchor'   | BOOKSTORE_ANCHOR_5 | BOOKSTORE_ANCHOR_3 | '/bookstore/categories[@code=\'5\']/books[@title=\'Book 1\']' || null                                        | ['books':[['title':'Book 1', 'price':1]]]
     }
 
     def 'Get delta between anchors when child data nodes under existing parent data nodes are updated: #scenario'() {
@@ -156,10 +156,10 @@ class DeltaServiceIntegrationSpec extends FunctionalSpecBase {
     def 'Get delta between anchors where source and target data nodes have leaves and child data nodes'() {
         given: 'parent node xpath and expected data in delta report'
             def parentNodeXpath = '/bookstore/categories[@code=\'1\']'
-            def expectedSourceDataInParentNode = ['name':'Children']
-            def expectedTargetDataInParentNode = ['name':'Kids']
-            def expectedSourceDataInChildNode = [['lang' : 'English'],['price':20, 'editions':[1988, 2000]]]
-            def expectedTargetDataInChildNode = [['lang':'English/German'], ['price':200, 'editions':[1988, 2000, 2023]]]
+            def expectedSourceDataInParentNode = ['categories':[['code':'1', 'name':'Children']]]
+            def expectedTargetDataInParentNode = ['categories':[['code':'1', 'name':'Kids']]]
+            def expectedSourceDataInChildNode = [['books':[['lang':'English', 'title':'The Gruffalo']]], ['books':[['editions':[1988, 2000], 'price':20, 'title':'Matilda']]]]
+            def expectedTargetDataInChildNode = [['books':[['lang':'English/German', 'title':'The Gruffalo']]], ['books':[['price':200, 'editions':[1988, 2000, 2023], 'title':'Matilda']]]]
         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, NO_GROUPING)
             def deltaReportEntities = getDeltaReportEntities(result)