Merge "CPS Delta API: Update action for delta service"
authorToine Siebelink <toine.siebelink@est.tech>
Thu, 18 Jan 2024 16:51:48 +0000 (16:51 +0000)
committerGerrit Code Review <gerrit@onap.org>
Thu, 18 Jan 2024 16:51:48 +0000 (16:51 +0000)
cps-service/src/main/java/org/onap/cps/api/impl/CpsDeltaServiceImpl.java
cps-service/src/main/java/org/onap/cps/spi/model/DeltaReport.java
cps-service/src/main/java/org/onap/cps/spi/model/DeltaReportBuilder.java
cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDeltaServiceImplSpec.groovy
integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsDataServiceIntegrationSpec.groovy
integration-test/src/test/resources/data/bookstore/bookstoreDataForDeltaReport.json

index 683ddce..1e1fe81 100644 (file)
@@ -28,7 +28,7 @@ import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
-import lombok.NoArgsConstructor;
+import java.util.Objects;
 import lombok.extern.slf4j.Slf4j;
 import org.onap.cps.api.CpsDeltaService;
 import org.onap.cps.spi.model.DataNode;
@@ -38,7 +38,6 @@ import org.springframework.stereotype.Service;
 
 @Slf4j
 @Service
-@NoArgsConstructor
 public class CpsDeltaServiceImpl implements CpsDeltaService {
 
     @Override
@@ -50,7 +49,7 @@ public class CpsDeltaServiceImpl implements CpsDeltaService {
         final Map<String, DataNode> xpathToSourceDataNodes = convertToXPathToDataNodesMap(sourceDataNodes);
         final Map<String, DataNode> xpathToTargetDataNodes = convertToXPathToDataNodesMap(targetDataNodes);
 
-        deltaReport.addAll(getRemovedDeltaReports(xpathToSourceDataNodes, xpathToTargetDataNodes));
+        deltaReport.addAll(getRemovedAndUpdatedDeltaReports(xpathToSourceDataNodes, xpathToTargetDataNodes));
 
         deltaReport.addAll(getAddedDeltaReports(xpathToSourceDataNodes, xpathToTargetDataNodes));
 
@@ -70,26 +69,122 @@ public class CpsDeltaServiceImpl implements CpsDeltaService {
         return xpathToDataNode;
     }
 
-    private static List<DeltaReport> getRemovedDeltaReports(
-                                                            final Map<String, DataNode> xpathToSourceDataNodes,
-                                                            final Map<String, DataNode> xpathToTargetDataNodes) {
-
-        final List<DeltaReport> removedDeltaReportEntries = new ArrayList<>();
+    private static List<DeltaReport> getRemovedAndUpdatedDeltaReports(
+                                                                final Map<String, DataNode> xpathToSourceDataNodes,
+                                                                final Map<String, DataNode> xpathToTargetDataNodes) {
+        final List<DeltaReport> removedAndUpdatedDeltaReportEntries = new ArrayList<>();
         for (final Map.Entry<String, DataNode> entry: xpathToSourceDataNodes.entrySet()) {
             final String xpath = entry.getKey();
             final DataNode sourceDataNode = entry.getValue();
             final DataNode targetDataNode = xpathToTargetDataNodes.get(xpath);
-
+            final List<DeltaReport> deltaReports;
             if (targetDataNode == null) {
-                final Map<String, Serializable> sourceDataNodeLeaves = sourceDataNode.getLeaves();
-                final DeltaReport removedData = new DeltaReportBuilder().actionRemove().withXpath(xpath)
-                        .withSourceData(sourceDataNodeLeaves).build();
-                removedDeltaReportEntries.add(removedData);
+                deltaReports = getRemovedDeltaReports(xpath, sourceDataNode);
+            } else {
+                deltaReports = getUpdatedDeltaReports(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<>();
+        final Map<String, Serializable> sourceDataNodeLeaves = sourceDataNode.getLeaves();
+        final DeltaReport removedDeltaReportEntry = new DeltaReportBuilder().actionRemove().withXpath(xpath)
+                .withSourceData(sourceDataNodeLeaves).build();
+        removedDeltaReportEntries.add(removedDeltaReportEntry);
         return removedDeltaReportEntries;
     }
 
+    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 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);
+        }
+        return updatedLeavesAsSourceDataToTargetData;
+    }
+
+    private static void processLeavesPresentInSourceAndTargetDataNode(
+                                                            final Map<String, Serializable> leavesOfSourceDataNode,
+                                                            final Map<String, Serializable> leavesOfTargetDataNode,
+                                                            final Map<String, Serializable> sourceDataInDeltaReport,
+                                                            final Map<String, Serializable> targetDataInDeltaReport) {
+        for (final Map.Entry<String, Serializable> entry: leavesOfSourceDataNode.entrySet()) {
+            final String key = entry.getKey();
+            final Serializable sourceLeaf = entry.getValue();
+            final Serializable targetLeaf = leavesOfTargetDataNode.get(key);
+            compareLeaves(key, sourceLeaf, targetLeaf, sourceDataInDeltaReport, targetDataInDeltaReport);
+        }
+    }
+
+    private static void processLeavesUniqueInTargetDataNode(
+                                                            final Map<String, Serializable> leavesOfSourceDataNode,
+                                                            final Map<String, Serializable> leavesOfTargetDataNode,
+                                                            final Map<String, Serializable> sourceDataInDeltaReport,
+                                                            final Map<String, Serializable> targetDataInDeltaReport) {
+        final Map<String, Serializable> uniqueLeavesOfTargetDataNode =
+                new LinkedHashMap<>(leavesOfTargetDataNode);
+        uniqueLeavesOfTargetDataNode.keySet().removeAll(leavesOfSourceDataNode.keySet());
+        for (final Map.Entry<String, Serializable> entry: uniqueLeavesOfTargetDataNode.entrySet()) {
+            final String key = entry.getKey();
+            final Serializable targetLeaf = entry.getValue();
+            final Serializable sourceLeaf = leavesOfSourceDataNode.get(key);
+            compareLeaves(key, sourceLeaf, targetLeaf, sourceDataInDeltaReport, targetDataInDeltaReport);
+        }
+    }
+
+    private static void compareLeaves(final String key,
+                                      final Serializable sourceLeaf,
+                                      final Serializable targetLeaf,
+                                      final Map<String, Serializable> sourceDataInDeltaReport,
+                                      final Map<String, Serializable> targetDataInDeltaReport) {
+        if (sourceLeaf != null && targetLeaf != null) {
+            if (!Objects.equals(sourceLeaf, targetLeaf)) {
+                sourceDataInDeltaReport.put(key, sourceLeaf);
+                targetDataInDeltaReport.put(key, targetLeaf);
+            }
+        } else if (sourceLeaf != null) {
+            sourceDataInDeltaReport.put(key, sourceLeaf);
+        } else if (targetLeaf != null) {
+            targetDataInDeltaReport.put(key, targetLeaf);
+        }
+    }
+
+    private static void addUpdatedLeavesToDeltaReport(final String xpath,
+                                                      final Map<Map<String, Serializable>, Map<String,
+                                                              Serializable>> updatedLeavesAsSourceDataToTargetData,
+                                                      final List<DeltaReport> updatedDeltaReportEntries) {
+        for (final Map.Entry<Map<String, Serializable>, Map<String, Serializable>> entry:
+                updatedLeavesAsSourceDataToTargetData.entrySet()) {
+            final DeltaReport updatedDataForDeltaReport = new DeltaReportBuilder().actionUpdate()
+                    .withXpath(xpath).withSourceData(entry.getKey()).withTargetData(entry.getValue()).build();
+            updatedDeltaReportEntries.add(updatedDataForDeltaReport);
+        }
+
+    }
+
     private static List<DeltaReport> getAddedDeltaReports(final Map<String, DataNode> xpathToSourceDataNodes,
                                                           final Map<String, DataNode> xpathToTargetDataNodes) {
 
index b9c05dc..fb9c197 100644 (file)
@@ -32,6 +32,7 @@ public class DeltaReport {
 
     public static final String ADD_ACTION = "add";
     public static final String REMOVE_ACTION = "remove";
+    public static final String UPDATE_ACTION = "update";
 
     DeltaReport() {}
 
index cef6ca3..1e151ee 100644 (file)
@@ -58,6 +58,11 @@ public class DeltaReportBuilder {
         return this;
     }
 
+    public DeltaReportBuilder actionUpdate() {
+        this.action = DeltaReport.UPDATE_ACTION;
+        return this;
+    }
+
     /**
      * To create a single entry of {@link DeltaReport}.
      *
index a4f4339..e21c6f0 100644 (file)
@@ -21,7 +21,6 @@
 package org.onap.cps.api.impl
 
 import org.onap.cps.spi.model.DataNode
-import org.onap.cps.spi.model.DataNodeBuilder
 import spock.lang.Shared
 import spock.lang.Specification
 
@@ -29,38 +28,81 @@ class CpsDeltaServiceImplSpec extends Specification{
 
     def objectUnderTest = new CpsDeltaServiceImpl()
 
-    @Shared
-    def dataNodeWithLeafAndChildDataNode = [new DataNodeBuilder().withXpath('/parent').withLeaves(['parent-leaf': 'parent-payload'])
-                            .withChildDataNodes([new DataNodeBuilder().withXpath("/parent/child").withLeaves('child-leaf': 'child-payload').build()]).build()]
-    @Shared
-    def dataNodeWithChildDataNode = [new DataNodeBuilder().withXpath('/parent').withLeaves(['parent-leaf': 'parent-payload'])
-                                             .withChildDataNodes([new DataNodeBuilder().withXpath("/parent/child").build()]).build()]
-    @Shared
-    def emptyDataNode = [new DataNodeBuilder().withXpath('/parent').build()]
 
-    def 'Get delta between data nodes for removed data where source data node has #scenario'() {
+    static def sourceDataNodeWithLeafData = [new DataNode(xpath: '/parent', leaves: ['parent-leaf': 'parent-payload-in-source'])]
+    static def sourceDataNodeWithoutLeafData = [new DataNode(xpath: '/parent')]
+    static def targetDataNodeWithLeafData = [new DataNode(xpath: '/parent', leaves: ['parent-leaf': 'parent-payload-in-target'])]
+    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'])]
+
+    def 'Get delta between data nodes for REMOVED data where source data node has #scenario'() {
+        when: 'attempt to get delta between 2 data nodes'
+            def result = objectUnderTest.getDeltaReports(sourceDataNodeWithLeafData, [])
+        then: 'the delta report contains expected "remove" action'
+            assert result[0].action.equals('remove')
+        and : 'the delta report contains the expected xpath'
+            assert result[0].xpath == '/parent'
+        and: 'the delta report contains expected source data'
+            assert result[0].sourceData == ['parent-leaf': 'parent-payload-in-source']
+        and: 'the delta report contains no target data'
+            assert  result[0].targetData == null
+    }
+
+    def 'Get delta between data nodes with ADDED data where target data node has #scenario'() {
+        when: 'attempt to get delta between 2 data nodes'
+            def result = objectUnderTest.getDeltaReports([], targetDataNodeWithLeafData)
+        then: 'the delta report contains expected "add" action'
+            assert result[0].action.equals('add')
+        and: 'the delta report contains expected xpath'
+            assert result[0].xpath == '/parent'
+        and: 'the delta report contains no source data'
+            assert result[0].sourceData == null
+        and: 'the delta report contains expected target data'
+            assert result[0].targetData == ['parent-leaf': 'parent-payload-in-target']
+    }
+
+    def 'Delta Report between leaves for parent and child nodes, #scenario'() {
+        given: 'Two data nodes'
+            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 data nodes'
+            def result = objectUnderTest.getDeltaReports(sourceDataNode, targetDataNode)
+        then: 'the delta report contains expected "update" action'
+            assert result[index].action.equals('update')
+        and: 'the delta report contains expected xpath'
+            assert result[index].xpath == expectedXpath
+        and: 'the delta report contains expected source and target data'
+            assert result[index].sourceData == expectedSourceData
+            assert result[index].targetData == expectedTargetData
+        where: 'the following data was used'
+            scenario           | index || expectedXpath   | expectedSourceData                | expectedTargetData
+            'parent data node' | 0     || '/parent'       | ['parent-leaf': 'parent-payload'] | ['parent-leaf': 'parent-payload-updated']
+            'child data node'  | 1     || '/parent/child' | ['child-leaf': 'child-payload']   | ['child-leaf': 'child-payload-updated']
+    }
+
+    def 'Delta report between leaves, #scenario'() {
         when: 'attempt to get delta between 2 data nodes'
-            def result = objectUnderTest.getDeltaReports(sourceDataNode as Collection<DataNode>, emptyDataNode)
-        then: 'the delta report contains "remove" action with right data'
-            assert result.first().action.equals("remove")
-            assert result.first().xpath == "/parent/child"
-            assert result.first().sourceData == expectedSourceData
-        where: 'following data was used'
-            scenario       | sourceDataNode                   || expectedSourceData
-            'leaf data'    | dataNodeWithLeafAndChildDataNode || ['child-leaf': 'child-payload']
-            'no leaf data' | dataNodeWithChildDataNode        || null
+            def result = objectUnderTest.getDeltaReports(sourceDataNode, targetDataNode)
+        then: 'the delta report contains expected "update" action'
+            assert result[0].action.equals('update')
+        and: 'the delta report contains expected xpath'
+            assert result[0].xpath == '/parent'
+        and: 'the delta report contains expected source and target data'
+            assert result[0].sourceData == expectedSourceData
+            assert result[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']
     }
 
-    def 'Get delta between data nodes with new data where target data node has #scenario'() {
+    def 'Get delta between data nodes for updated data, where source and target data nodes have no leaves '() {
         when: 'attempt to get delta between 2 data nodes'
-            def result = objectUnderTest.getDeltaReports(emptyDataNode, targetDataNode)
-        then: 'the delta report contains "add" action with right data'
-            assert result.first().action.equals("add")
-            assert result.first().xpath == "/parent/child"
-            assert result.first().targetData == expectedTargetData
-        where: 'following data was used'
-            scenario       | targetDataNode                   || expectedTargetData
-            'leaf data'    | dataNodeWithLeafAndChildDataNode || ['child-leaf': 'child-payload']
-            'no leaf data' | dataNodeWithChildDataNode        || null
+            def result = objectUnderTest.getDeltaReports(sourceDataNodeWithoutLeafData, targetDataNodeWithoutLeafData)
+        then: 'the delta report contains "update" action with right data'
+            assert result.isEmpty()
     }
 }
index e143099..3843a9f 100644 (file)
@@ -431,40 +431,30 @@ class CpsDataServiceIntegrationSpec extends FunctionalSpecBase {
 
     def 'Get delta between 2 anchors for when #scenario'() {
         when: 'attempt to get delta report between anchors'
-            def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_ANCHOR_3, BOOKSTORE_ANCHOR_5, xpath, fetchDescendantOption)
+            def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_ANCHOR_3, BOOKSTORE_ANCHOR_5, '/', OMIT_DESCENDANTS)
         then: 'delta report contains expected number of changes'
-            result.size() == 2
-        and: 'delta report contains expected action'
-            assert result.get(index).getAction() == expectedActions
-        and: 'delta report contains expected xpath'
-            assert result.get(index).getXpath() == expectedXpath
-        where: 'following data was used'
-            scenario            | index | xpath || expectedActions || expectedXpath                                                | fetchDescendantOption
-            'a node is removed' |   0   | '/'   ||    'remove'     || "/bookstore-address[@bookstore-name='Easons-1']"             | OMIT_DESCENDANTS
-            'a node is added'   |   1   | '/'   ||     'add'       || "/bookstore-address[@bookstore-name='Crossword Bookstores']" | OMIT_DESCENDANTS
-    }
-
-    def 'Get delta between 2 anchors where child nodes are added/removed but parent node remains unchanged'() {
-        def parentNodeXpath = "/bookstore"
-        when: 'attempt to get delta report between anchors'
-            def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_ANCHOR_3, BOOKSTORE_ANCHOR_5, parentNodeXpath, INCLUDE_ALL_DESCENDANTS)
-        then: 'delta report contains expected number of changes'
-            result.size() == 11
-        and: 'the delta report does not contain parent node xpath'
-            def xpaths = getDeltaReportEntities(result).get('xpaths')
-            assert !(xpaths.contains(parentNodeXpath))
+            result.size() == 3
+        and: 'delta report contains UPDATE action with expected xpath'
+            assert result[0].getAction() == 'update'
+            assert result[0].getXpath() == '/bookstore'
+        and: 'delta report contains REMOVE action with expected xpath'
+            assert result[1].getAction() == 'remove'
+            assert result[1].getXpath() == "/bookstore-address[@bookstore-name='Easons-1']"
+        and: 'delta report contains ADD action with expected xpath'
+            assert result[2].getAction() == 'add'
+            assert result[2].getXpath() == "/bookstore-address[@bookstore-name='Crossword Bookstores']"
     }
 
     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, sourceAnchor, targetAnchor, xpath, INCLUDE_ALL_DESCENDANTS)
+            def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_ANCHOR_3, targetAnchor, xpath, INCLUDE_ALL_DESCENDANTS)
         then: 'delta report is empty'
             assert result.isEmpty()
         where: 'following data was used'
-            scenario                              | sourceAnchor       | targetAnchor       | xpath
-        'anchors with identical data are queried' | BOOKSTORE_ANCHOR_3 | BOOKSTORE_ANCHOR_4 | '/'
-        'same anchor name is passed as parameter' | BOOKSTORE_ANCHOR_3 | BOOKSTORE_ANCHOR_3 | '/'
-        'non existing xpath'                      | BOOKSTORE_ANCHOR_3 | BOOKSTORE_ANCHOR_5 | '/non-existing-xpath'
+            scenario                              | targetAnchor       | xpath
+        'anchors with identical data are queried' | BOOKSTORE_ANCHOR_4 | '/'
+        'same anchor name is passed as parameter' | BOOKSTORE_ANCHOR_3 | '/'
+        'non existing xpath'                      | BOOKSTORE_ANCHOR_5 | '/non-existing-xpath'
     }
 
     def 'Get delta between anchors error scenario: #scenario'() {
@@ -511,6 +501,64 @@ class CpsDataServiceIntegrationSpec extends FunctionalSpecBase {
             'is empty'                   | "/bookstore/container-without-leaves"
     }
 
+    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)
+        then: 'expected action is update'
+            assert result[0].getAction() == 'update'
+        and: 'the payload has expected leaf values'
+            def sourceData = result[0].getSourceData()
+            def targetData = result[0].getTargetData()
+            assert sourceData == expectedSourceValue
+            assert targetData == 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]
+    }
+
+    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)
+        then: 'expected action is update'
+            assert result[0].getAction() == 'update'
+        and: 'the delta report has expected child node xpaths'
+            def deltaReportEntities = getDeltaReportEntities(result)
+            def childNodeXpathsInDeltaReport = deltaReportEntities.get('xpaths')
+            assert childNodeXpathsInDeltaReport.contains(expectedChildNodeXpath)
+        where: 'following data was used'
+            scenario                                          | sourceAnchor       | targetAnchor       | xpath                 || expectedChildNodeXpath
+            'source and target anchors have child data nodes' | BOOKSTORE_ANCHOR_3 | BOOKSTORE_ANCHOR_5 | '/bookstore/premises' || '/bookstore/premises/addresses[@house-number=\'2\' and @street=\'Main Street\']'
+            'removed child data nodes in target anchor'       | BOOKSTORE_ANCHOR_5 | BOOKSTORE_ANCHOR_3 | '/bookstore'          || '/bookstore/support-info'
+            'added  child data nodes in target anchor'        | BOOKSTORE_ANCHOR_3 | BOOKSTORE_ANCHOR_5 | '/bookstore'          || '/bookstore/support-info'
+    }
+
+    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':[2023, 1988, 2000]]]
+        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 deltaReportEntities = getDeltaReportEntities(result)
+        then: 'expected action is update'
+            assert result[0].getAction() == 'update'
+        and: 'the payload has expected parent node xpath'
+            assert deltaReportEntities.get('xpaths').contains(parentNodeXpath)
+        and: 'delta report has expected source and target data'
+            assert deltaReportEntities.get('sourcePayload').contains(expectedSourceDataInParentNode)
+            assert deltaReportEntities.get('targetPayload').contains(expectedTargetDataInParentNode)
+        and: 'the delta report also has expected child node xpaths'
+            assert deltaReportEntities.get('xpaths').containsAll(["/bookstore/categories[@code='1']/books[@title='The Gruffalo']", "/bookstore/categories[@code='1']/books[@title='Matilda']"])
+        and: 'the delta report also has expected source and target data of child nodes'
+            assert deltaReportEntities.get('sourcePayload').containsAll(expectedSourceDataInChildNode)
+            assert deltaReportEntities.get('targetPayload').containsAll(expectedTargetDataInChildNode)
+
+    }
+
     def getDeltaReportEntities(List<DeltaReport> deltaReport) {
         def xpaths = []
         def action = []
index 73b84fc..1dd6c0d 100644 (file)
@@ -7,7 +7,7 @@
     }
   ],
   "bookstore": {
-    "bookstore-name": "Easons",
+    "bookstore-name": "Crossword Bookstores",
     "premises": {
       "addresses": [
         {
@@ -96,8 +96,7 @@
             "title": "Book 1",
             "lang": "blah",
             "authors": [],
-            "editions": [],
-            "price": 1
+            "editions": []
           },
           {
             "title": "Book 2",