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