From 99274b453a0b6919f35113eba93a905f6c21f3cf Mon Sep 17 00:00:00 2001 From: Arpit Singh Date: Tue, 18 Feb 2025 12:19:21 +0530 Subject: [PATCH] Part-2: Grouping of Data Nodes in Delta Report - 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 --- .../org/onap/cps/impl/CpsDeltaServiceImpl.java | 182 +++++++++++++++------ .../onap/cps/impl/CpsDeltaServiceImplSpec.groovy | 141 ++++++++++------ .../cps/DataServiceIntegrationSpec.groovy | 4 +- .../cps/DeltaServiceIntegrationSpec.groovy | 20 +-- 4 files changed, 236 insertions(+), 111 deletions(-) diff --git a/cps-service/src/main/java/org/onap/cps/impl/CpsDeltaServiceImpl.java b/cps-service/src/main/java/org/onap/cps/impl/CpsDeltaServiceImpl.java index 727c6b7302..c902c00848 100644 --- a/cps-service/src/main/java/org/onap/cps/impl/CpsDeltaServiceImpl.java +++ b/cps-service/src/main/java/org/onap/cps/impl/CpsDeltaServiceImpl.java @@ -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 = new ArrayList<>(); if (groupDataNodes) { - deltaReport.addAll(getCondensedAddedDeltaReports(sourceDataNodes, targetDataNodes)); + deltaReport.addAll(getCondensedDeltaReports(sourceDataNodes, targetDataNodes)); } else { final Map xpathToSourceDataNodes = convertToXPathToDataNodesMap(sourceDataNodes); final Map xpathToTargetDataNodes = convertToXPathToDataNodesMap(targetDataNodes); @@ -135,58 +137,61 @@ public class CpsDeltaServiceImpl implements CpsDeltaService { final DataNode targetDataNode = xpathToTargetDataNodes.get(xpath); final List 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 getRemovedDeltaReports(final String xpath, final DataNode sourceDataNode) { - final List removedDeltaReportEntries = new ArrayList<>(); + private static List getDeltaReportsForRemove(final String xpath, final DataNode sourceDataNode) { + final List deltaReportEntriesForRemove = new ArrayList<>(); final Map 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 getUpdatedDeltaReports(final String xpath, final DataNode sourceDataNode, - final DataNode targetDataNode) { - final List updatedDeltaReportEntries = new ArrayList<>(); - final Map, Map> updatedLeavesAsSourceDataToTargetData = - getUpdatedLeavesBetweenSourceAndTargetDataNode(sourceDataNode.getLeaves(), targetDataNode.getLeaves()); - addUpdatedLeavesToDeltaReport(xpath, updatedLeavesAsSourceDataToTargetData, updatedDeltaReportEntries); - return updatedDeltaReportEntries; + private static List getDeltaReportsForUpdates(final String xpath, final DataNode sourceDataNode, + final DataNode targetDataNode) { + final List deltaReportEntriesForUpdates = new ArrayList<>(); + final Map, Map> updatedSourceDataToTargetData = + getUpdatedSourceAndTargetDataNode(sourceDataNode, targetDataNode); + if (!updatedSourceDataToTargetData.isEmpty()) { + addUpdatedDataToDeltaReport(xpath, updatedSourceDataToTargetData, deltaReportEntriesForUpdates); + } + return deltaReportEntriesForUpdates; } - private static Map, - Map> getUpdatedLeavesBetweenSourceAndTargetDataNode( - final Map leavesOfSourceDataNode, - final Map leavesOfTargetDataNode) { - final Map, Map> updatedLeavesAsSourceDataToTargetData = - new LinkedHashMap<>(); - final Map sourceDataInDeltaReport = new LinkedHashMap<>(); - final Map 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> getUpdatedSourceAndTargetDataNode( + final DataNode sourceDataNode, + final DataNode targetDataNode) { + final Map updatedLeavesInSourceData = new HashMap<>(); + final Map updatedLeavesInTargetData = new HashMap<>(); + processSourceAndTargetDataNode(sourceDataNode, targetDataNode, + updatedLeavesInSourceData, updatedLeavesInTargetData); + processUniqueDataInTargetDataNode(sourceDataNode, targetDataNode, + updatedLeavesInSourceData, updatedLeavesInTargetData); + final Map updatedSourceData = + getUpdatedNodeData(sourceDataNode, updatedLeavesInSourceData); + final Map 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 leavesOfSourceDataNode, - final Map leavesOfTargetDataNode, + private static void processSourceAndTargetDataNode( + final DataNode sourceDataNode, + final DataNode targetDataNode, final Map sourceDataInDeltaReport, final Map targetDataInDeltaReport) { + final Map leavesOfSourceDataNode = sourceDataNode.getLeaves(); + final Map leavesOfTargetDataNode = targetDataNode.getLeaves(); for (final Map.Entry 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 leavesOfSourceDataNode, - final Map leavesOfTargetDataNode, + private static void processUniqueDataInTargetDataNode( + final DataNode sourceDataNode, + final DataNode targetDataNode, final Map sourceDataInDeltaReport, final Map targetDataInDeltaReport) { + final Map leavesOfSourceDataNode = sourceDataNode.getLeaves(); final Map uniqueLeavesOfTargetDataNode = - new LinkedHashMap<>(leavesOfTargetDataNode); + new HashMap<>(targetDataNode.getLeaves()); uniqueLeavesOfTargetDataNode.keySet().removeAll(leavesOfSourceDataNode.keySet()); for (final Map.Entry 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> updatedLeavesAsSourceDataToTargetData, - final List updatedDeltaReportEntries) { + private static Map getUpdatedNodeData(final DataNode dataNode, + final Map updatedLeaves) { + final Map updatedSourceData = new HashMap<>(); + if (!updatedLeaves.isEmpty()) { + final String xpath = dataNode.getXpath(); + if (CpsPathUtil.isPathToListElement(xpath)) { + addKeyLeavesToUpdatedData(xpath, updatedLeaves); + } + final Collection updatedDataNode = buildUpdatedDataNode(dataNode, updatedLeaves); + updatedSourceData.putAll(getCondensedDataForDeltaReport(updatedDataNode)); + } + return updatedSourceData; + } + + private static void addKeyLeavesToUpdatedData(final String xpath, + final Map updatedLeaves) { + final Map keyLeaves = new HashMap<>(); + final List 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 buildUpdatedDataNode(final DataNode dataNode, + final Map 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> updatedSourceDataToTargetData, + final List deltaReportEntriesForUpdates) { for (final Map.Entry, Map> 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 getCondensedDeltaReports(final Collection sourceDataNodes, + final Collection targetDataNodes) { + + final List deltaReportEntries = new ArrayList<>(); + final Map xpathToTargetDataNodes = flattenToXpathToFirstLevelDataNodeMap(targetDataNodes); + deltaReportEntries.addAll(getCondensedRemovedDeltaReports(sourceDataNodes, xpathToTargetDataNodes)); + deltaReportEntries.addAll(getCondensedUpdatedDeltaReports(sourceDataNodes, xpathToTargetDataNodes)); + deltaReportEntries.addAll(getCondensedAddedDeltaReports(sourceDataNodes, targetDataNodes)); + return deltaReportEntries; + } + + private static List getCondensedRemovedDeltaReports(final Collection sourceDataNodes, + final Map xpathToTargetDataNodes) { + + final List deltaReportEntriesForRemove = new ArrayList<>(); + final Collection 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 getCondensedUpdatedDeltaReports(final Collection sourceDataNodes, + final Map xpathToTargetDataNodes) { + final List 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 deltaReportEntries) { + final Collection childrenOfSourceDataNodes = sourceDataNode.getChildDataNodes(); + final Collection childrenOfTargetDataNodes = targetDataNode.getChildDataNodes(); + if (!childrenOfSourceDataNodes.isEmpty() || !childrenOfTargetDataNodes.isEmpty()) { + deltaReportEntries.addAll(getCondensedDeltaReports(childrenOfSourceDataNodes, childrenOfTargetDataNodes)); + } + } + private static List getCondensedAddedDeltaReports(final Collection sourceDataNodes, final Collection targetDataNodes) { diff --git a/cps-service/src/test/groovy/org/onap/cps/impl/CpsDeltaServiceImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/impl/CpsDeltaServiceImplSpec.groovy index d979be482b..82fc893abb 100644 --- a/cps-service/src/test/groovy/org/onap/cps/impl/CpsDeltaServiceImplSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/impl/CpsDeltaServiceImplSpec.groovy @@ -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) { diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/functional/cps/DataServiceIntegrationSpec.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/functional/cps/DataServiceIntegrationSpec.groovy index 6ecc3a509c..c7f7b7d3df 100644 --- a/integration-test/src/test/groovy/org/onap/cps/integration/functional/cps/DataServiceIntegrationSpec.groovy +++ b/integration-test/src/test/groovy/org/onap/cps/integration/functional/cps/DataServiceIntegrationSpec.groovy @@ -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 diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/functional/cps/DeltaServiceIntegrationSpec.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/functional/cps/DeltaServiceIntegrationSpec.groovy index 5890450a4e..9f30b5a5d1 100644 --- a/integration-test/src/test/groovy/org/onap/cps/integration/functional/cps/DeltaServiceIntegrationSpec.groovy +++ b/integration-test/src/test/groovy/org/onap/cps/integration/functional/cps/DeltaServiceIntegrationSpec.groovy @@ -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) -- 2.16.6