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:
- $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
- $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:
final String sourceAnchorName,
final String targetAnchorName,
final String xpath,
- final String descendants) {
+ final String descendants,
+ final Boolean groupDataNodes) {
final FetchDescendantsOption fetchDescendantsOption =
FetchDescendantsOption.getFetchDescendantsOption(descendants);
final List<DeltaReport> deltaBetweenAnchors =
cpsDeltaService.getDeltaByDataspaceAndAnchors(dataspaceName, sourceAnchorName,
- targetAnchorName, xpath, fetchDescendantsOption);
+ targetAnchorName, xpath, fetchDescendantsOption, groupDataNodes);
return new ResponseEntity<>(jsonObjectMapper.asJsonString(deltaBetweenAnchors), HttpStatus.OK);
}
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);
}
final Collection<DeltaReport> deltaReports = Collections.unmodifiableList(
cpsDeltaService.getDeltaByDataspaceAnchorAndPayload(dataspaceName, sourceAnchorName,
- xpath, yangResourceMap, targetData, fetchDescendantsOption));
+ xpath, yangResourceMap, targetData, fetchDescendantsOption, groupDataNodes));
return new ResponseEntity<>(jsonObjectMapper.asJsonString(deltaReports), HttpStatus.OK);
}
}
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"]}]}]}'
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)
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)
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)
* @param xpath xpath
* @param fetchDescendantsOption defines the scope of data to fetch: either single node or all the descendant
* nodes (recursively) as well
+ * @param groupDataNodes boolean flag to enable or disable grouping of data nodes in delta report.
+ * If enabled, data nodes are grouped based on parent-child relationship, providing a
+ * condensed version of delta report.
* @return list containing {@link DeltaReport} objects
*/
List<DeltaReport> getDeltaByDataspaceAndAnchors(String dataspaceName, String sourceAnchorName,
String targetAnchorName, String xpath,
- FetchDescendantsOption fetchDescendantsOption);
+ FetchDescendantsOption fetchDescendantsOption,
+ boolean groupDataNodes);
/**
* Retrieves the delta between an anchor and JSON payload by xpath, using dataspace name and anchor name.
* @param yangResourceContentPerName YANG resources (files) map where key is a name and value is content
* @param targetData target data to be compared in JSON string format
* @param fetchDescendantsOption defines the scope of data to fetch: defaulted to INCLUDE_ALL_DESCENDANTS
+ * @param groupDataNodes boolean flag to enable or disable grouping of data nodes in delta report.
+ * If enabled, data nodes are grouped based on parent-child relationship,
+ * providing a condensed version of delta report.
*
* @return list containing {@link DeltaReport} objects
*/
List<DeltaReport> getDeltaByDataspaceAnchorAndPayload(String dataspaceName, String sourceAnchorName, String xpath,
Map<String, String> yangResourceContentPerName,
String targetData,
- FetchDescendantsOption fetchDescendantsOption);
+ FetchDescendantsOption fetchDescendantsOption,
+ boolean groupDataNodes);
}
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;
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;
final String sourceAnchorName,
final String targetAnchorName,
final String xpath,
- final FetchDescendantsOption fetchDescendantsOption) {
+ final FetchDescendantsOption fetchDescendantsOption,
+ final boolean groupDataNodes) {
final Collection<DataNode> sourceDataNodes = cpsDataService.getDataNodesForMultipleXpaths(dataspaceName,
sourceAnchorName, Collections.singletonList(xpath), fetchDescendantsOption);
final Collection<DataNode> targetDataNodes = cpsDataService.getDataNodesForMultipleXpaths(dataspaceName,
targetAnchorName, Collections.singletonList(xpath), fetchDescendantsOption);
- return getDeltaReports(sourceDataNodes, targetDataNodes);
+ return getDeltaReports(sourceDataNodes, targetDataNodes, groupDataNodes);
}
@Timed(value = "cps.delta.service.get.delta",
final String xpath,
final Map<String, String> yangResourceContentPerName,
final String targetData,
- final FetchDescendantsOption fetchDescendantsOption) {
+ final FetchDescendantsOption fetchDescendantsOption,
+ final boolean groupDataNodes) {
final Anchor sourceAnchor = cpsAnchorService.getAnchor(dataspaceName, sourceAnchorName);
final Collection<DataNode> sourceDataNodes = cpsDataService.getDataNodesForMultipleXpaths(dataspaceName,
rebuildSourceDataNodes(xpath, sourceAnchor, sourceDataNodes);
final Collection<DataNode> targetDataNodes = new ArrayList<>(
buildTargetDataNodes(sourceAnchor, xpath, yangResourceContentPerName, targetData));
- return getDeltaReports(sourceDataNodesRebuilt, targetDataNodes);
+ return getDeltaReports(sourceDataNodesRebuilt, targetDataNodes, groupDataNodes);
}
- private List<DeltaReport> getDeltaReports(final Collection<DataNode> sourceDataNodes,
- final Collection<DataNode> targetDataNodes) {
+ private static List<DeltaReport> getDeltaReports(final Collection<DataNode> sourceDataNodes,
+ final Collection<DataNode> targetDataNodes,
+ final boolean groupDataNodes) {
final List<DeltaReport> deltaReport = new ArrayList<>();
-
- final Map<String, DataNode> xpathToSourceDataNodes = convertToXPathToDataNodesMap(sourceDataNodes);
- final Map<String, DataNode> xpathToTargetDataNodes = convertToXPathToDataNodesMap(targetDataNodes);
-
- deltaReport.addAll(getRemovedAndUpdatedDeltaReports(xpathToSourceDataNodes, xpathToTargetDataNodes));
- deltaReport.addAll(getAddedDeltaReports(xpathToSourceDataNodes, xpathToTargetDataNodes));
-
+ if (groupDataNodes) {
+ deltaReport.addAll(getCondensedAddedDeltaReports(sourceDataNodes, targetDataNodes));
+ } else {
+ final Map<String, DataNode> xpathToSourceDataNodes = convertToXPathToDataNodesMap(sourceDataNodes);
+ final Map<String, DataNode> xpathToTargetDataNodes = convertToXPathToDataNodesMap(targetDataNodes);
+ deltaReport.addAll(getRemovedAndUpdatedDeltaReports(xpathToSourceDataNodes, xpathToTargetDataNodes));
+ deltaReport.addAll(getAddedDeltaReports(xpathToSourceDataNodes, xpathToTargetDataNodes));
+ }
return Collections.unmodifiableList(deltaReport);
}
- private static Map<String, DataNode> convertToXPathToDataNodesMap(
- final Collection<DataNode> dataNodes) {
+ private static Map<String, DataNode> convertToXPathToDataNodesMap(final Collection<DataNode> dataNodes) {
final Map<String, DataNode> xpathToDataNode = new LinkedHashMap<>();
for (final DataNode dataNode : dataNodes) {
xpathToDataNode.put(dataNode.getXpath(), dataNode);
.withXpath(xpath).withSourceData(entry.getKey()).withTargetData(entry.getValue()).build();
updatedDeltaReportEntries.add(updatedDataForDeltaReport);
}
-
}
private static List<DeltaReport> getAddedDeltaReports(final Map<String, DataNode> xpathToSourceDataNodes,
.createDataNodesWithYangResourceXpathAndNodeData(yangResourceContentPerName, xpath, targetData, JSON);
}
}
+
+ private static List<DeltaReport> getCondensedAddedDeltaReports(final Collection<DataNode> sourceDataNodes,
+ final Collection<DataNode> targetDataNodes) {
+
+ final List<DeltaReport> addedDeltaReportEntries = new ArrayList<>();
+ final Collection<DataNode> addedDataNodes =
+ getDataNodesForDeltaReport(targetDataNodes, flattenToXpathToFirstLevelDataNodeMap(sourceDataNodes));
+ if (!addedDataNodes.isEmpty()) {
+ final String xpath = getXpathForDeltaReport(addedDataNodes);
+ addedDeltaReportEntries.add(new DeltaReportBuilder().actionCreate().withXpath(xpath)
+ .withTargetData(getCondensedDataForDeltaReport(addedDataNodes)).build());
+ }
+ return addedDeltaReportEntries;
+ }
+
+ private static String getXpathForDeltaReport(final Collection<DataNode> dataNodes) {
+ final String firstNodeXpath = dataNodes.iterator().next().getXpath();
+ final String parentNodeXpath = CpsPathUtil.getNormalizedParentXpath(firstNodeXpath);
+ return parentNodeXpath.isEmpty() ? firstNodeXpath : parentNodeXpath;
+ }
+
+ private static Collection<DataNode> getDataNodesForDeltaReport(final Collection<DataNode> dataNodes,
+ final Map<String, DataNode> xpathToDataNodes) {
+ return dataNodes.stream().filter(dataNode -> !xpathToDataNodes.containsKey(dataNode.getXpath())).toList();
+ }
+
+ private static Map<String, Serializable> getCondensedDataForDeltaReport(final Collection<DataNode> dataNodes) {
+ final DataNode containerNode = new DataNodeBuilder().withChildDataNodes(dataNodes).build();
+ final Map<String, Object> condensedData = DataMapUtils.toDataMap(containerNode);
+ return condensedData.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey,
+ entry -> (Serializable) entry.getValue()));
+ }
+
+ private static Map<String, DataNode> flattenToXpathToFirstLevelDataNodeMap(final Collection<DataNode> dataNodes) {
+ return dataNodes.stream().collect(Collectors.toMap(DataNode::getXpath, dataNode -> dataNode));
+ }
}
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
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)
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'])]
@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
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)
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'
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'
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)'
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)'
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'
'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
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:
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:
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:
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
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'
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'
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'
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'
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'
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'
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'
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'
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'
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'