aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorArpit Singh <as00745003@techmahindra.com>2023-08-02 18:35:31 +0530
committerArpit Singh <as00745003@techmahindra.com>2024-02-08 13:26:53 +0000
commite140c60532de6487225ee7860ade0825cb350648 (patch)
tree65b326595091936131744f61cd8203f49c95f212
parent60c73123ba15404816282dd36b370a3fec10c91d (diff)
CPS Delta API 1: Delta between 2 anchors
- CPS Delta Feature Part 1: To find delta between two anchors - created new endpoint deltaByDataspaceAndAnchors - endpoint to take dataspaceName, source anchor, target anchor, xpath, descendants as input - added new service CpsDeltaService - added method to find delta between DataNodes: getDeltaReport - added method to find removed data nodes: getRemovedDeltaReports - added method to get Added DataNodes: getAddedDeltaReports - added method to get Map of xpath to DataNode: convertToXPathToDataNodesMap - added a POJO for delta report - Added new JSON data for delta feature testing - Added groovy test files CpsDeltaServiceImplSpec and DeltaReportBuilderSpec - code related to update operation, will be added in separate commit Issue-ID: CPS-1824 Signed-off-by: Arpit Singh <as00745003@techmahindra.com> Change-Id: I313f0f71d04b03878be7643f709d8af1aa6df6ba
-rw-r--r--cps-rest/docs/openapi/components.yml26
-rw-r--r--cps-rest/docs/openapi/cpsDataV2.yml33
-rw-r--r--cps-rest/docs/openapi/openapi.yml3
-rwxr-xr-xcps-rest/src/main/java/org/onap/cps/rest/controller/DataRestController.java18
-rwxr-xr-xcps-rest/src/test/groovy/org/onap/cps/rest/controller/DataRestControllerSpec.groovy21
-rw-r--r--cps-service/src/main/java/org/onap/cps/api/CpsDataService.java17
-rw-r--r--cps-service/src/main/java/org/onap/cps/api/CpsDeltaService.java42
-rwxr-xr-xcps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java20
-rw-r--r--cps-service/src/main/java/org/onap/cps/api/impl/CpsDeltaServiceImpl.java108
-rw-r--r--cps-service/src/main/java/org/onap/cps/spi/model/DeltaReport.java42
-rw-r--r--cps-service/src/main/java/org/onap/cps/spi/model/DeltaReportBuilder.java79
-rw-r--r--cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy33
-rw-r--r--cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDeltaServiceImplSpec.groovy66
-rwxr-xr-xcps-service/src/test/groovy/org/onap/cps/api/impl/E2ENetworkSliceSpec.groovy6
-rw-r--r--cps-service/src/test/groovy/org/onap/cps/spi/model/DeltaReportBuilderSpec.groovy52
-rw-r--r--integration-test/src/test/groovy/org/onap/cps/integration/base/FunctionalSpecBase.groovy17
-rw-r--r--integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsDataServiceIntegrationSpec.groovy97
-rw-r--r--integration-test/src/test/resources/data/bookstore/bookstore.yang11
-rw-r--r--integration-test/src/test/resources/data/bookstore/bookstoreDataForDeltaReport.json192
19 files changed, 878 insertions, 5 deletions
diff --git a/cps-rest/docs/openapi/components.yml b/cps-rest/docs/openapi/components.yml
index a3016ce76..c1b111bfa 100644
--- a/cps-rest/docs/openapi/components.yml
+++ b/cps-rest/docs/openapi/components.yml
@@ -137,6 +137,24 @@ components:
name: SciFi
- code: 02
name: kids
+ deltaReportSample:
+ value:
+ - action: "ADD"
+ xpath: "/bookstore/categories/[@code=3]"
+ target-data:
+ code: 3,
+ name: "kidz"
+ - action: "REMOVE"
+ xpath: "/bookstore/categories/[@code=1]"
+ source-data:
+ code: 1,
+ name: "Fiction"
+ - action: "UPDATE"
+ xpath: "/bookstore/categories/[@code=2]"
+ source-data:
+ name: "Funny"
+ target-data:
+ name: "Comic"
parameters:
dataspaceNameInQuery:
@@ -187,6 +205,14 @@ components:
schema:
type: string
example: my-anchor
+ targetAnchorNameInQuery:
+ name: target-anchor-name
+ in: query
+ description: target-anchor-name
+ required: true
+ schema:
+ type: string
+ example: my-anchor
xpathInQuery:
name: xpath
in: query
diff --git a/cps-rest/docs/openapi/cpsDataV2.yml b/cps-rest/docs/openapi/cpsDataV2.yml
index ad0c299d7..c7629b70e 100644
--- a/cps-rest/docs/openapi/cpsDataV2.yml
+++ b/cps-rest/docs/openapi/cpsDataV2.yml
@@ -46,4 +46,37 @@ nodeByDataspaceAndAnchor:
$ref: 'components.yml#/components/responses/Forbidden'
'500':
$ref: 'components.yml#/components/responses/InternalServerError'
+ x-codegen-request-body-name: xpath
+
+deltaByDataspaceAndAnchors:
+ get:
+ description: Get delta between two anchors within a given dataspace
+ tags:
+ - cps-data
+ summary: Get delta between anchors in the same dataspace
+ operationId: getDeltaByDataspaceAndAnchors
+ parameters:
+ - $ref: 'components.yml#/components/parameters/dataspaceNameInPath'
+ - $ref: 'components.yml#/components/parameters/anchorNameInPath'
+ - $ref: 'components.yml#/components/parameters/targetAnchorNameInQuery'
+ - $ref: 'components.yml#/components/parameters/xpathInQuery'
+ - $ref: 'components.yml#/components/parameters/descendantsInQuery'
+ responses:
+ '200':
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ examples:
+ dataSample:
+ $ref: 'components.yml#/components/examples/deltaReportSample'
+ '400':
+ $ref: 'components.yml#/components/responses/BadRequest'
+ '401':
+ $ref: 'components.yml#/components/responses/Unauthorized'
+ '403':
+ $ref: 'components.yml#/components/responses/Forbidden'
+ '500':
+ $ref: 'components.yml#/components/responses/InternalServerError'
x-codegen-request-body-name: xpath \ No newline at end of file
diff --git a/cps-rest/docs/openapi/openapi.yml b/cps-rest/docs/openapi/openapi.yml
index 4bbf9f0fb..f29335a0a 100644
--- a/cps-rest/docs/openapi/openapi.yml
+++ b/cps-rest/docs/openapi/openapi.yml
@@ -104,6 +104,9 @@ paths:
/{apiVersion}/dataspaces/{dataspace-name}/anchors/{anchor-name}/list-nodes:
$ref: 'cpsData.yml#/listElementByDataspaceAndAnchor'
+ /v2/dataspaces/{dataspace-name}/anchors/{anchor-name}/delta:
+ $ref: 'cpsDataV2.yml#/deltaByDataspaceAndAnchors'
+
/v1/dataspaces/{dataspace-name}/anchors/{anchor-name}/nodes/query:
$ref: 'cpsQueryV1Deprecated.yml#/nodesByDataspaceAndAnchorAndCpsPath'
diff --git a/cps-rest/src/main/java/org/onap/cps/rest/controller/DataRestController.java b/cps-rest/src/main/java/org/onap/cps/rest/controller/DataRestController.java
index 60e7fb6d2..4f9328b6c 100755
--- a/cps-rest/src/main/java/org/onap/cps/rest/controller/DataRestController.java
+++ b/cps-rest/src/main/java/org/onap/cps/rest/controller/DataRestController.java
@@ -38,6 +38,7 @@ import org.onap.cps.api.CpsDataService;
import org.onap.cps.rest.api.CpsDataApi;
import org.onap.cps.spi.FetchDescendantsOption;
import org.onap.cps.spi.model.DataNode;
+import org.onap.cps.spi.model.DeltaReport;
import org.onap.cps.utils.ContentType;
import org.onap.cps.utils.DataMapUtils;
import org.onap.cps.utils.JsonObjectMapper;
@@ -166,6 +167,23 @@ public class DataRestController implements CpsDataApi {
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
+ @Override
+ @Timed(value = "cps.data.controller.get.delta",
+ description = "Time taken to get delta between anchors")
+ public ResponseEntity<Object> getDeltaByDataspaceAndAnchors(final String dataspaceName,
+ final String sourceAnchorName,
+ final String targetAnchorName,
+ final String xpath,
+ final String descendants) {
+ final FetchDescendantsOption fetchDescendantsOption =
+ FetchDescendantsOption.getFetchDescendantsOption(descendants);
+
+ final List<DeltaReport> deltaBetweenAnchors =
+ cpsDataService.getDeltaByDataspaceAndAnchors(dataspaceName, sourceAnchorName,
+ targetAnchorName, xpath, fetchDescendantsOption);
+ return new ResponseEntity<>(jsonObjectMapper.asJsonString(deltaBetweenAnchors), HttpStatus.OK);
+ }
+
private static boolean isRootXpath(final String xpath) {
return ROOT_XPATH.equals(xpath);
}
diff --git a/cps-rest/src/test/groovy/org/onap/cps/rest/controller/DataRestControllerSpec.groovy b/cps-rest/src/test/groovy/org/onap/cps/rest/controller/DataRestControllerSpec.groovy
index 81262c80c..12c9c4c60 100755
--- a/cps-rest/src/test/groovy/org/onap/cps/rest/controller/DataRestControllerSpec.groovy
+++ b/cps-rest/src/test/groovy/org/onap/cps/rest/controller/DataRestControllerSpec.groovy
@@ -30,6 +30,7 @@ import org.onap.cps.api.CpsDataService
import org.onap.cps.spi.FetchDescendantsOption
import org.onap.cps.spi.model.DataNode
import org.onap.cps.spi.model.DataNodeBuilder
+import org.onap.cps.spi.model.DeltaReportBuilder
import org.onap.cps.utils.ContentType
import org.onap.cps.utils.DateTimeUtility
import org.onap.cps.utils.JsonObjectMapper
@@ -331,7 +332,25 @@ class DataRestControllerSpec extends Specification {
and: 'the response contains the root node identifier'
assert response.contentAsString.contains('parent')
and: 'the response contains child is true'
- assert response.contentAsString.contains('"child"') == true
+ assert response.contentAsString.contains('"child"')
+ }
+
+ def 'Get delta between two anchors'() {
+ given: 'the service returns a list containing delta reports'
+ def deltaReports = new DeltaReportBuilder().actionAdd().withXpath('/bookstore').withSourceData('bookstore-name': 'Easons').withTargetData('bookstore-name': 'Easons').build()
+ def xpath = 'some xpath'
+ def endpoint = "$dataNodeBaseEndpointV2/anchors/sourceAnchor/delta"
+ mockCpsDataService.getDeltaByDataspaceAndAnchors(dataspaceName, 'sourceAnchor', 'targetAnchor', xpath, OMIT_DESCENDANTS) >> [deltaReports]
+ when: 'get delta request is performed using REST API'
+ def response =
+ mvc.perform(get(endpoint)
+ .param('target-anchor-name', 'targetAnchor')
+ .param('xpath', xpath))
+ .andReturn().response
+ then: 'expected response code is returned'
+ assert response.status == HttpStatus.OK.value()
+ and: 'the response contains expected value'
+ assert response.contentAsString.contains("[{\"action\":\"add\",\"xpath\":\"/bookstore\",\"sourceData\":{\"bookstore-name\":\"Easons\"},\"targetData\":{\"bookstore-name\":\"Easons\"}}]")
}
def 'Update data node leaves: #scenario.'() {
diff --git a/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java b/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java
index 6a2cac467..c9879595a 100644
--- a/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java
+++ b/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java
@@ -26,9 +26,11 @@ package org.onap.cps.api;
import java.time.OffsetDateTime;
import java.util.Collection;
+import java.util.List;
import java.util.Map;
import org.onap.cps.spi.FetchDescendantsOption;
import org.onap.cps.spi.model.DataNode;
+import org.onap.cps.spi.model.DeltaReport;
import org.onap.cps.utils.ContentType;
/*
@@ -298,4 +300,19 @@ public interface CpsDataService {
* @param timeoutInMilliseconds lock attempt timeout in milliseconds
*/
void lockAnchor(String sessionID, String dataspaceName, String anchorName, Long timeoutInMilliseconds);
+
+ /**
+ * Retrieves the delta between two anchors by xpath within a dataspace.
+ *
+ * @param dataspaceName dataspace name
+ * @param sourceAnchorName source anchor name
+ * @param targetAnchorName target anchor name
+ * @param xpath xpath
+ * @param fetchDescendantsOption defines the scope of data to fetch: either single node or all the descendant
+ * nodes (recursively) as well
+ * @return list containing {@link DeltaReport} objects
+ */
+ List<DeltaReport> getDeltaByDataspaceAndAnchors(String dataspaceName, String sourceAnchorName,
+ String targetAnchorName, String xpath,
+ FetchDescendantsOption fetchDescendantsOption);
}
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
new file mode 100644
index 000000000..d806c208a
--- /dev/null
+++ b/cps-service/src/main/java/org/onap/cps/api/CpsDeltaService.java
@@ -0,0 +1,42 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2023 TechMahindra Ltd.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.api;
+
+import java.util.Collection;
+import java.util.List;
+import org.onap.cps.spi.model.DataNode;
+import org.onap.cps.spi.model.DeltaReport;
+
+public interface CpsDeltaService {
+
+ /**
+ * Retrieves delta between source data nodes and target data nodes. Source data nodes contain the data which acts as
+ * the point of reference for delta report, whereas target data nodes contain the data being compared against
+ * source data node. List of {@link DeltaReport}. Each Delta Report contains information such as action, xpath,
+ * source-payload and target-payload.
+ *
+ * @param sourceDataNodes collection of {@link DataNode} as source/reference for delta generation
+ * @param targetDataNodes collection of {@link DataNode} as target data for delta generation
+ * @return list of {@link DeltaReport} containing delta information
+ */
+ List<DeltaReport> getDeltaReports(Collection<DataNode> sourceDataNodes,
+ Collection<DataNode> targetDataNodes);
+}
diff --git a/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java b/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java
index 1d68450f8..e74e0ad24 100755
--- a/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java
+++ b/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java
@@ -34,12 +34,14 @@ import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
+import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.onap.cps.api.CpsAdminService;
import org.onap.cps.api.CpsDataService;
+import org.onap.cps.api.CpsDeltaService;
import org.onap.cps.cpspath.parser.CpsPathUtil;
import org.onap.cps.notification.NotificationService;
import org.onap.cps.notification.Operation;
@@ -49,6 +51,7 @@ import org.onap.cps.spi.exceptions.DataValidationException;
import org.onap.cps.spi.model.Anchor;
import org.onap.cps.spi.model.DataNode;
import org.onap.cps.spi.model.DataNodeBuilder;
+import org.onap.cps.spi.model.DeltaReport;
import org.onap.cps.spi.utils.CpsValidator;
import org.onap.cps.utils.ContentType;
import org.onap.cps.utils.TimedYangParser;
@@ -70,6 +73,7 @@ public class CpsDataServiceImpl implements CpsDataService {
private final NotificationService notificationService;
private final CpsValidator cpsValidator;
private final TimedYangParser timedYangParser;
+ private final CpsDeltaService cpsDeltaService;
@Override
public void saveData(final String dataspaceName, final String anchorName, final String nodeData,
@@ -215,6 +219,22 @@ public class CpsDataServiceImpl implements CpsDataService {
}
@Override
+ @Timed(value = "cps.data.service.get.delta",
+ description = "Time taken to get delta between anchors")
+ public List<DeltaReport> getDeltaByDataspaceAndAnchors(final String dataspaceName,
+ final String sourceAnchorName,
+ final String targetAnchorName, final String xpath,
+ final FetchDescendantsOption fetchDescendantsOption) {
+
+ final Collection<DataNode> sourceDataNodes = getDataNodesForMultipleXpaths(dataspaceName,
+ sourceAnchorName, Collections.singletonList(xpath), fetchDescendantsOption);
+ final Collection<DataNode> targetDataNodes = getDataNodesForMultipleXpaths(dataspaceName,
+ targetAnchorName, Collections.singletonList(xpath), fetchDescendantsOption);
+
+ return cpsDeltaService.getDeltaReports(sourceDataNodes, targetDataNodes);
+ }
+
+ @Override
@Timed(value = "cps.data.service.datanode.descendants.update",
description = "Time taken to update a data node and descendants")
public void updateDataNodeAndDescendants(final String dataspaceName, final String anchorName,
diff --git a/cps-service/src/main/java/org/onap/cps/api/impl/CpsDeltaServiceImpl.java b/cps-service/src/main/java/org/onap/cps/api/impl/CpsDeltaServiceImpl.java
new file mode 100644
index 000000000..683ddce3d
--- /dev/null
+++ b/cps-service/src/main/java/org/onap/cps/api/impl/CpsDeltaServiceImpl.java
@@ -0,0 +1,108 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2023 TechMahindra Ltd.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.api.impl;
+
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import lombok.NoArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.onap.cps.api.CpsDeltaService;
+import org.onap.cps.spi.model.DataNode;
+import org.onap.cps.spi.model.DeltaReport;
+import org.onap.cps.spi.model.DeltaReportBuilder;
+import org.springframework.stereotype.Service;
+
+@Slf4j
+@Service
+@NoArgsConstructor
+public class CpsDeltaServiceImpl implements CpsDeltaService {
+
+ @Override
+ public List<DeltaReport> getDeltaReports(final Collection<DataNode> sourceDataNodes,
+ final Collection<DataNode> targetDataNodes) {
+
+ final List<DeltaReport> deltaReport = new ArrayList<>();
+
+ final Map<String, DataNode> xpathToSourceDataNodes = convertToXPathToDataNodesMap(sourceDataNodes);
+ final Map<String, DataNode> xpathToTargetDataNodes = convertToXPathToDataNodesMap(targetDataNodes);
+
+ deltaReport.addAll(getRemovedDeltaReports(xpathToSourceDataNodes, xpathToTargetDataNodes));
+
+ deltaReport.addAll(getAddedDeltaReports(xpathToSourceDataNodes, xpathToTargetDataNodes));
+
+ return Collections.unmodifiableList(deltaReport);
+ }
+
+ 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);
+ final Collection<DataNode> childDataNodes = dataNode.getChildDataNodes();
+ if (!childDataNodes.isEmpty()) {
+ xpathToDataNode.putAll(convertToXPathToDataNodesMap(childDataNodes));
+ }
+ }
+ return xpathToDataNode;
+ }
+
+ private static List<DeltaReport> getRemovedDeltaReports(
+ final Map<String, DataNode> xpathToSourceDataNodes,
+ final Map<String, DataNode> xpathToTargetDataNodes) {
+
+ final List<DeltaReport> removedDeltaReportEntries = new ArrayList<>();
+ for (final Map.Entry<String, DataNode> entry: xpathToSourceDataNodes.entrySet()) {
+ final String xpath = entry.getKey();
+ final DataNode sourceDataNode = entry.getValue();
+ final DataNode targetDataNode = xpathToTargetDataNodes.get(xpath);
+
+ if (targetDataNode == null) {
+ final Map<String, Serializable> sourceDataNodeLeaves = sourceDataNode.getLeaves();
+ final DeltaReport removedData = new DeltaReportBuilder().actionRemove().withXpath(xpath)
+ .withSourceData(sourceDataNodeLeaves).build();
+ removedDeltaReportEntries.add(removedData);
+ }
+ }
+ return removedDeltaReportEntries;
+ }
+
+ private static List<DeltaReport> getAddedDeltaReports(final Map<String, DataNode> xpathToSourceDataNodes,
+ final Map<String, DataNode> xpathToTargetDataNodes) {
+
+ final List<DeltaReport> addedDeltaReportEntries = new ArrayList<>();
+ final Map<String, DataNode> xpathToAddedNodes = new LinkedHashMap<>(xpathToTargetDataNodes);
+ xpathToAddedNodes.keySet().removeAll(xpathToSourceDataNodes.keySet());
+ for (final Map.Entry<String, DataNode> entry: xpathToAddedNodes.entrySet()) {
+ final String xpath = entry.getKey();
+ final DataNode dataNode = entry.getValue();
+ final DeltaReport addedDataForDeltaReport = new DeltaReportBuilder().actionAdd().withXpath(xpath)
+ .withTargetData(dataNode.getLeaves()).build();
+ addedDeltaReportEntries.add(addedDataForDeltaReport);
+ }
+ return addedDeltaReportEntries;
+ }
+}
diff --git a/cps-service/src/main/java/org/onap/cps/spi/model/DeltaReport.java b/cps-service/src/main/java/org/onap/cps/spi/model/DeltaReport.java
new file mode 100644
index 000000000..b9c05dcf0
--- /dev/null
+++ b/cps-service/src/main/java/org/onap/cps/spi/model/DeltaReport.java
@@ -0,0 +1,42 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2023 TechMahindra Ltd.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.spi.model;
+
+import java.io.Serializable;
+import java.util.Map;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.Setter;
+
+@Setter(AccessLevel.PROTECTED)
+@Getter
+public class DeltaReport {
+
+ public static final String ADD_ACTION = "add";
+ public static final String REMOVE_ACTION = "remove";
+
+ DeltaReport() {}
+
+ private String action;
+ private String xpath;
+ private Map<String, Serializable> sourceData;
+ private Map<String, Serializable> targetData;
+}
diff --git a/cps-service/src/main/java/org/onap/cps/spi/model/DeltaReportBuilder.java b/cps-service/src/main/java/org/onap/cps/spi/model/DeltaReportBuilder.java
new file mode 100644
index 000000000..cef6ca3fa
--- /dev/null
+++ b/cps-service/src/main/java/org/onap/cps/spi/model/DeltaReportBuilder.java
@@ -0,0 +1,79 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2023 TechMahindra Ltd.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.spi.model;
+
+import java.io.Serializable;
+import java.util.Map;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+public class DeltaReportBuilder {
+
+
+ private String action;
+ private String xpath;
+ private Map<String, Serializable> sourceData;
+ private Map<String, Serializable> targetData;
+
+ public DeltaReportBuilder withXpath(final String xpath) {
+ this.xpath = xpath;
+ return this;
+ }
+
+ public DeltaReportBuilder withSourceData(final Map<String, Serializable> sourceData) {
+ this.sourceData = sourceData;
+ return this;
+ }
+
+ public DeltaReportBuilder withTargetData(final Map<String, Serializable> targetData) {
+ this.targetData = targetData;
+ return this;
+ }
+
+ public DeltaReportBuilder actionAdd() {
+ this.action = DeltaReport.ADD_ACTION;
+ return this;
+ }
+
+ public DeltaReportBuilder actionRemove() {
+ this.action = DeltaReport.REMOVE_ACTION;
+ return this;
+ }
+
+ /**
+ * To create a single entry of {@link DeltaReport}.
+ *
+ * @return {@link DeltaReport}
+ */
+ public DeltaReport build() {
+ final DeltaReport deltaReport = new DeltaReport();
+ deltaReport.setAction(action);
+ deltaReport.setXpath(xpath);
+ if (sourceData != null && !sourceData.isEmpty()) {
+ deltaReport.setSourceData(sourceData);
+ }
+
+ if (targetData != null && !targetData.isEmpty()) {
+ deltaReport.setTargetData(targetData);
+ }
+ return deltaReport;
+ }
+}
diff --git a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy
index e1d15d68a..a91459852 100644
--- a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy
+++ b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy
@@ -25,6 +25,7 @@ package org.onap.cps.api.impl
import org.onap.cps.TestUtils
import org.onap.cps.api.CpsAdminService
+import org.onap.cps.api.CpsDeltaService
import org.onap.cps.notification.NotificationService
import org.onap.cps.notification.Operation
import org.onap.cps.spi.CpsDataPersistenceService
@@ -37,12 +38,14 @@ import org.onap.cps.spi.exceptions.SessionTimeoutException
import org.onap.cps.spi.model.Anchor
import org.onap.cps.spi.model.DataNode
import org.onap.cps.spi.model.DataNodeBuilder
+import org.onap.cps.spi.model.DeltaReportBuilder
+import org.onap.cps.spi.utils.CpsValidator
import org.onap.cps.utils.ContentType
import org.onap.cps.utils.TimedYangParser
import org.onap.cps.yang.YangTextSchemaSourceSet
import org.onap.cps.yang.YangTextSchemaSourceSetBuilder
+import spock.lang.Shared
import spock.lang.Specification
-import org.onap.cps.spi.utils.CpsValidator
import java.time.OffsetDateTime
import java.util.stream.Collectors
@@ -54,18 +57,28 @@ class CpsDataServiceImplSpec extends Specification {
def mockNotificationService = Mock(NotificationService)
def mockCpsValidator = Mock(CpsValidator)
def timedYangParser = new TimedYangParser()
+ def mockCpsDeltaService = Mock(CpsDeltaService);
def objectUnderTest = new CpsDataServiceImpl(mockCpsDataPersistenceService, mockCpsAdminService,
- mockYangTextSchemaSourceSetCache, mockNotificationService, mockCpsValidator, timedYangParser)
+ mockYangTextSchemaSourceSetCache, mockNotificationService, mockCpsValidator, timedYangParser, mockCpsDeltaService)
def setup() {
+
mockCpsAdminService.getAnchor(dataspaceName, anchorName) >> anchor
+ mockCpsAdminService.getAnchor(dataspaceName, ANCHOR_NAME_1) >> anchor1
+ mockCpsAdminService.getAnchor(dataspaceName, ANCHOR_NAME_2) >> anchor2
}
+ @Shared
+ static def ANCHOR_NAME_1 = 'some-anchor-1'
+ @Shared
+ static def ANCHOR_NAME_2 = 'some-anchor-2'
def dataspaceName = 'some-dataspace'
def anchorName = 'some-anchor'
def schemaSetName = 'some-schema-set'
def anchor = Anchor.builder().name(anchorName).dataspaceName(dataspaceName).schemaSetName(schemaSetName).build()
+ 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 observedTimestamp = OffsetDateTime.now()
def 'Saving #scenario data.'() {
@@ -228,6 +241,22 @@ class CpsDataServiceImplSpec extends Specification {
fetchDescendantsOption << [FetchDescendantsOption.OMIT_DESCENDANTS, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS]
}
+ def 'Get delta between 2 anchors'() {
+ given: 'some xpath, source and target data nodes'
+ def xpath = '/xpath'
+ def sourceDataNodes = [new DataNodeBuilder().withXpath(xpath).build()]
+ def targetDataNodes = [new DataNodeBuilder().withXpath(xpath).build()]
+ when: 'attempt to get delta between 2 anchors'
+ objectUnderTest.getDeltaByDataspaceAndAnchors(dataspaceName, ANCHOR_NAME_1, ANCHOR_NAME_2, xpath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS)
+ then: 'the dataspace and anchor names are validated'
+ 2 * mockCpsValidator.validateNameCharacters(_)
+ and: 'data nodes are fetched using appropriate persistence layer method'
+ mockCpsDataPersistenceService.getDataNodesForMultipleXpaths(dataspaceName, ANCHOR_NAME_1, [xpath], FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> sourceDataNodes
+ mockCpsDataPersistenceService.getDataNodesForMultipleXpaths(dataspaceName, ANCHOR_NAME_2, [xpath], FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> targetDataNodes
+ and: 'appropriate delta service method is invoked once with correct source and target data nodes'
+ 1 * mockCpsDeltaService.getDeltaReports(sourceDataNodes, targetDataNodes)
+ }
+
def 'Update data node leaves: #scenario.'() {
given: 'schema set for given anchor and dataspace references test-tree model'
setupSchemaSetMocks('test-tree.yang')
diff --git a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDeltaServiceImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDeltaServiceImplSpec.groovy
new file mode 100644
index 000000000..a4f433973
--- /dev/null
+++ b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDeltaServiceImplSpec.groovy
@@ -0,0 +1,66 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2023 TechMahindra Ltd.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.api.impl
+
+import org.onap.cps.spi.model.DataNode
+import org.onap.cps.spi.model.DataNodeBuilder
+import spock.lang.Shared
+import spock.lang.Specification
+
+class CpsDeltaServiceImplSpec extends Specification{
+
+ def objectUnderTest = new CpsDeltaServiceImpl()
+
+ @Shared
+ def dataNodeWithLeafAndChildDataNode = [new DataNodeBuilder().withXpath('/parent').withLeaves(['parent-leaf': 'parent-payload'])
+ .withChildDataNodes([new DataNodeBuilder().withXpath("/parent/child").withLeaves('child-leaf': 'child-payload').build()]).build()]
+ @Shared
+ def dataNodeWithChildDataNode = [new DataNodeBuilder().withXpath('/parent').withLeaves(['parent-leaf': 'parent-payload'])
+ .withChildDataNodes([new DataNodeBuilder().withXpath("/parent/child").build()]).build()]
+ @Shared
+ def emptyDataNode = [new DataNodeBuilder().withXpath('/parent').build()]
+
+ def 'Get delta between data nodes for removed data where source data node has #scenario'() {
+ when: 'attempt to get delta between 2 data nodes'
+ def result = objectUnderTest.getDeltaReports(sourceDataNode as Collection<DataNode>, emptyDataNode)
+ then: 'the delta report contains "remove" action with right data'
+ assert result.first().action.equals("remove")
+ assert result.first().xpath == "/parent/child"
+ assert result.first().sourceData == expectedSourceData
+ where: 'following data was used'
+ scenario | sourceDataNode || expectedSourceData
+ 'leaf data' | dataNodeWithLeafAndChildDataNode || ['child-leaf': 'child-payload']
+ 'no leaf data' | dataNodeWithChildDataNode || null
+ }
+
+ def 'Get delta between data nodes with new data where target data node has #scenario'() {
+ when: 'attempt to get delta between 2 data nodes'
+ def result = objectUnderTest.getDeltaReports(emptyDataNode, targetDataNode)
+ then: 'the delta report contains "add" action with right data'
+ assert result.first().action.equals("add")
+ assert result.first().xpath == "/parent/child"
+ assert result.first().targetData == expectedTargetData
+ where: 'following data was used'
+ scenario | targetDataNode || expectedTargetData
+ 'leaf data' | dataNodeWithLeafAndChildDataNode || ['child-leaf': 'child-payload']
+ 'no leaf data' | dataNodeWithChildDataNode || null
+ }
+}
diff --git a/cps-service/src/test/groovy/org/onap/cps/api/impl/E2ENetworkSliceSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/api/impl/E2ENetworkSliceSpec.groovy
index 75f29746d..1b873ec12 100755
--- a/cps-service/src/test/groovy/org/onap/cps/api/impl/E2ENetworkSliceSpec.groovy
+++ b/cps-service/src/test/groovy/org/onap/cps/api/impl/E2ENetworkSliceSpec.groovy
@@ -3,7 +3,7 @@
* Copyright (C) 2021-2023 Nordix Foundation.
* Modifications Copyright (C) 2021-2022 Bell Canada.
* Modifications Copyright (C) 2021 Pantheon.tech
- * Modifications Copyright (C) 2022 TechMahindra Ltd.
+ * Modifications Copyright (C) 2022-2023 TechMahindra Ltd.
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -25,6 +25,7 @@ package org.onap.cps.api.impl
import org.onap.cps.TestUtils
import org.onap.cps.api.CpsAdminService
+import org.onap.cps.api.CpsDeltaService
import org.onap.cps.notification.NotificationService
import org.onap.cps.spi.CpsDataPersistenceService
import org.onap.cps.spi.CpsModulePersistenceService
@@ -45,12 +46,13 @@ class E2ENetworkSliceSpec extends Specification {
def mockCpsValidator = Mock(CpsValidator)
def timedYangTextSchemaSourceSetBuilder = new TimedYangTextSchemaSourceSetBuilder()
def timedYangParser = new TimedYangParser()
+ def mockCpsDeltaService = Mock(CpsDeltaService)
def cpsModuleServiceImpl = new CpsModuleServiceImpl(mockModuleStoreService,
mockYangTextSchemaSourceSetCache, mockCpsAdminService, mockCpsValidator,timedYangTextSchemaSourceSetBuilder)
def cpsDataServiceImpl = new CpsDataServiceImpl(mockDataStoreService, mockCpsAdminService,
- mockYangTextSchemaSourceSetCache, mockNotificationService, mockCpsValidator, timedYangParser)
+ mockYangTextSchemaSourceSetCache, mockNotificationService, mockCpsValidator, timedYangParser, mockCpsDeltaService)
def dataspaceName = 'someDataspace'
def anchorName = 'someAnchor'
diff --git a/cps-service/src/test/groovy/org/onap/cps/spi/model/DeltaReportBuilderSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/spi/model/DeltaReportBuilderSpec.groovy
new file mode 100644
index 000000000..e19d12042
--- /dev/null
+++ b/cps-service/src/test/groovy/org/onap/cps/spi/model/DeltaReportBuilderSpec.groovy
@@ -0,0 +1,52 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2023 TechMahindra Ltd.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.spi.model
+
+import spock.lang.Specification
+
+class DeltaReportBuilderSpec extends Specification{
+
+ def 'Generating delta report with for add action'() {
+ when: 'delta report is generated'
+ def result = new DeltaReportBuilder()
+ .actionAdd()
+ .withXpath('/xpath')
+ .withTargetData(['data':'leaf-data'])
+ .build()
+ then: 'the delta report contains the "add" action with expected target data'
+ assert result.action == 'add'
+ assert result.xpath == '/xpath'
+ assert result.targetData == ['data': 'leaf-data']
+ }
+
+ def 'Generating delta report with attributes for remove action'() {
+ when: 'delta report is generated'
+ def result = new DeltaReportBuilder()
+ .actionRemove()
+ .withXpath('/xpath')
+ .withSourceData(['data':'leaf-data'])
+ .build()
+ then: 'the delta report contains the "remove" action with expected source data'
+ assert result.action == 'remove'
+ assert result.xpath == '/xpath'
+ assert result.sourceData == ['data': 'leaf-data']
+ }
+}
diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/base/FunctionalSpecBase.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/base/FunctionalSpecBase.groovy
index 327a39ee4..14612d6c1 100644
--- a/integration-test/src/test/groovy/org/onap/cps/integration/base/FunctionalSpecBase.groovy
+++ b/integration-test/src/test/groovy/org/onap/cps/integration/base/FunctionalSpecBase.groovy
@@ -1,6 +1,7 @@
/*
* ============LICENSE_START=======================================================
* Copyright (C) 2023 Nordix Foundation
+ * Modifications Copyright (C) 2022-2023 TechMahindra Ltd.
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
@@ -26,17 +27,24 @@ class FunctionalSpecBase extends CpsIntegrationSpecBase {
def static FUNCTIONAL_TEST_DATASPACE_1 = 'functionalTestDataspace1'
def static FUNCTIONAL_TEST_DATASPACE_2 = 'functionalTestDataspace2'
+ def static FUNCTIONAL_TEST_DATASPACE_3 = 'functionalTestDataspace3'
def static NUMBER_OF_ANCHORS_PER_DATASPACE_WITH_BOOKSTORE_DATA = 2
+ def static NUMBER_OF_ANCHORS_PER_DATASPACE_WITH_BOOKSTORE_DELTA_DATA = 1
def static BOOKSTORE_ANCHOR_1 = 'bookstoreAnchor1'
def static BOOKSTORE_ANCHOR_2 = 'bookstoreAnchor2'
+ def static BOOKSTORE_ANCHOR_3 = 'bookstoreSourceAnchor1'
+ def static BOOKSTORE_ANCHOR_4 = 'copyOfSourceAnchor1'
+ def static BOOKSTORE_ANCHOR_5 = 'bookstoreAnchorForDeltaReport1'
def static initialized = false
def static bookstoreJsonData = readResourceDataFile('bookstore/bookstoreData.json')
+ def static bookstoreJsonDataForDeltaReport = readResourceDataFile('bookstore/bookstoreDataForDeltaReport.json')
def setup() {
if (!initialized) {
setupBookstoreInfraStructure()
addBookstoreData()
+ addDeltaData()
initialized = true
}
}
@@ -44,9 +52,12 @@ class FunctionalSpecBase extends CpsIntegrationSpecBase {
def setupBookstoreInfraStructure() {
cpsAdminService.createDataspace(FUNCTIONAL_TEST_DATASPACE_1)
cpsAdminService.createDataspace(FUNCTIONAL_TEST_DATASPACE_2)
+ cpsAdminService.createDataspace(FUNCTIONAL_TEST_DATASPACE_3)
def bookstoreYangModelAsString = readResourceDataFile('bookstore/bookstore.yang')
cpsModuleService.createSchemaSet(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_SCHEMA_SET, [bookstore: bookstoreYangModelAsString])
cpsModuleService.createSchemaSet(FUNCTIONAL_TEST_DATASPACE_2, BOOKSTORE_SCHEMA_SET, [bookstore: bookstoreYangModelAsString])
+ cpsModuleService.createSchemaSet(FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_SCHEMA_SET, [bookstore: bookstoreYangModelAsString])
+
}
def addBookstoreData() {
@@ -54,6 +65,12 @@ class FunctionalSpecBase extends CpsIntegrationSpecBase {
addAnchorsWithData(NUMBER_OF_ANCHORS_PER_DATASPACE_WITH_BOOKSTORE_DATA, FUNCTIONAL_TEST_DATASPACE_2, BOOKSTORE_SCHEMA_SET, 'bookstoreAnchor', bookstoreJsonData)
}
+ def addDeltaData() {
+ addAnchorsWithData(NUMBER_OF_ANCHORS_PER_DATASPACE_WITH_BOOKSTORE_DELTA_DATA, FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_SCHEMA_SET, 'bookstoreSourceAnchor', bookstoreJsonData)
+ addAnchorsWithData(NUMBER_OF_ANCHORS_PER_DATASPACE_WITH_BOOKSTORE_DELTA_DATA, FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_SCHEMA_SET, 'copyOfSourceAnchor', bookstoreJsonData)
+ addAnchorsWithData(NUMBER_OF_ANCHORS_PER_DATASPACE_WITH_BOOKSTORE_DELTA_DATA, FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_SCHEMA_SET, 'bookstoreAnchorForDeltaReport', bookstoreJsonDataForDeltaReport)
+ }
+
def restoreBookstoreDataAnchor(anchorNumber) {
def anchorName = 'bookstoreAnchor' + anchorNumber
cpsAdminService.deleteAnchor(FUNCTIONAL_TEST_DATASPACE_1, anchorName)
diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsDataServiceIntegrationSpec.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsDataServiceIntegrationSpec.groovy
index 12c97ed40..017ede7de 100644
--- a/integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsDataServiceIntegrationSpec.groovy
+++ b/integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsDataServiceIntegrationSpec.groovy
@@ -32,6 +32,7 @@ import org.onap.cps.spi.exceptions.DataNodeNotFoundException
import org.onap.cps.spi.exceptions.DataNodeNotFoundExceptionBatch
import org.onap.cps.spi.exceptions.DataValidationException
import org.onap.cps.spi.exceptions.DataspaceNotFoundException
+import org.onap.cps.spi.model.DeltaReport
import java.time.OffsetDateTime
@@ -432,6 +433,102 @@ class CpsDataServiceIntegrationSpec extends FunctionalSpecBase {
restoreBookstoreDataAnchor(2)
}
+ def 'Get delta between 2 anchors for when #scenario'() {
+ when: 'attempt to get delta report between anchors'
+ def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_ANCHOR_3, BOOKSTORE_ANCHOR_5, xpath, fetchDescendantOption)
+ then: 'delta report contains expected number of changes'
+ result.size() == 2
+ and: 'delta report contains expected action'
+ assert result.get(index).getAction() == expectedActions
+ and: 'delta report contains expected xpath'
+ assert result.get(index).getXpath() == expectedXpath
+ where: 'following data was used'
+ scenario | index | xpath || expectedActions || expectedXpath | fetchDescendantOption
+ 'a node is removed' | 0 | '/' || 'remove' || "/bookstore-address[@bookstore-name='Easons-1']" | OMIT_DESCENDANTS
+ 'a node is added' | 1 | '/' || 'add' || "/bookstore-address[@bookstore-name='Crossword Bookstores']" | OMIT_DESCENDANTS
+ }
+
+ def 'Get delta between 2 anchors where child nodes are added/removed but parent node remains unchanged'() {
+ def parentNodeXpath = "/bookstore"
+ when: 'attempt to get delta report between anchors'
+ def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, BOOKSTORE_ANCHOR_3, BOOKSTORE_ANCHOR_5, parentNodeXpath, INCLUDE_ALL_DESCENDANTS)
+ then: 'delta report contains expected number of changes'
+ result.size() == 11
+ and: 'the delta report does not contain parent node xpath'
+ def xpaths = getDeltaReportEntities(result).get('xpaths')
+ assert !(xpaths.contains(parentNodeXpath))
+ }
+
+ def 'Get delta between 2 anchors returns empty response when #scenario'() {
+ when: 'attempt to get delta report between anchors'
+ def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, sourceAnchor, targetAnchor, xpath, INCLUDE_ALL_DESCENDANTS)
+ then: 'delta report is empty'
+ assert result.isEmpty()
+ where: 'following data was used'
+ scenario | sourceAnchor | targetAnchor | xpath
+ 'anchors with identical data are queried' | BOOKSTORE_ANCHOR_3 | BOOKSTORE_ANCHOR_4 | '/'
+ 'same anchor name is passed as parameter' | BOOKSTORE_ANCHOR_3 | BOOKSTORE_ANCHOR_3 | '/'
+ 'non existing xpath' | BOOKSTORE_ANCHOR_3 | BOOKSTORE_ANCHOR_5 | '/non-existing-xpath'
+ }
+
+ 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)
+ then: 'expected exception is thrown'
+ thrown(expectedException)
+ where: 'following data was used'
+ scenario | dataspaceName | sourceAnchor | targetAnchor || expectedException
+ 'invalid dataspace name' | 'Invalid dataspace' | 'not-relevant' | 'not-relevant' || DataValidationException
+ 'invalid anchor 1 name' | FUNCTIONAL_TEST_DATASPACE_3 | 'invalid anchor' | 'not-relevant' || DataValidationException
+ 'invalid anchor 2 name' | FUNCTIONAL_TEST_DATASPACE_3 | BOOKSTORE_ANCHOR_3 | 'invalid anchor' || DataValidationException
+ 'non-existing dataspace' | 'non-existing' | 'not-relevant1' | 'not-relevant2' || DataspaceNotFoundException
+ 'non-existing dataspace with same anchor name' | 'non-existing' | 'not-relevant' | 'not-relevant' || DataspaceNotFoundException
+ 'non-existing anchor 1' | FUNCTIONAL_TEST_DATASPACE_3 | 'non-existing-anchor' | 'not-relevant' || AnchorNotFoundException
+ 'non-existing anchor 2' | FUNCTIONAL_TEST_DATASPACE_3 | BOOKSTORE_ANCHOR_3 | 'non-existing-anchor' || AnchorNotFoundException
+ }
+
+ 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)
+ then: 'expected action is present in delta report'
+ assert result.get(0).getAction() == 'remove'
+ where: 'following data was used'
+ scenario | parentNodeXpath
+ 'has leaves and child nodes' | "/bookstore/categories[@code='6']"
+ 'has leaves only' | "/bookstore/categories[@code='5']/books[@title='Book 11']"
+ 'has child data node only' | "/bookstore/support-info/contact-emails"
+ 'is empty' | "/bookstore/container-without-leaves"
+ }
+
+ def 'Get delta between anchors for add 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)
+ then: 'the expected action is present in delta report'
+ result.get(0).getAction() == 'add'
+ and: 'the expected xapth is present in delta report'
+ result.get(0).getXpath() == parentNodeXpath
+ where: 'following data was used'
+ scenario | parentNodeXpath
+ 'has leaves and child nodes' | "/bookstore/categories[@code='6']"
+ 'has leaves only' | "/bookstore/categories[@code='5']/books[@title='Book 11']"
+ 'has child data node only' | "/bookstore/support-info/contact-emails"
+ 'is empty' | "/bookstore/container-without-leaves"
+ }
+
+ def getDeltaReportEntities(List<DeltaReport> deltaReport) {
+ def xpaths = []
+ def action = []
+ def sourcePayload = []
+ def targetPayload = []
+ deltaReport.each {
+ delta -> xpaths.add(delta.getXpath())
+ action.add(delta.getAction())
+ sourcePayload.add(delta.getSourceData())
+ targetPayload.add(delta.getTargetData())
+ }
+ return ['xpaths':xpaths, 'action':action, 'sourcePayload':sourcePayload, 'targetPayload':targetPayload]
+ }
+
def countDataNodesInBookstore() {
return countDataNodesInTree(objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/bookstore', INCLUDE_ALL_DESCENDANTS))
}
diff --git a/integration-test/src/test/resources/data/bookstore/bookstore.yang b/integration-test/src/test/resources/data/bookstore/bookstore.yang
index 6f60f1981..9c6c42e28 100644
--- a/integration-test/src/test/resources/data/bookstore/bookstore.yang
+++ b/integration-test/src/test/resources/data/bookstore/bookstore.yang
@@ -49,6 +49,17 @@ module stores {
}
}
+ container support-info {
+ leaf support-office {
+ type string;
+ }
+ container contact-emails {
+ leaf email {
+ type string;
+ }
+ }
+ }
+
container container-without-leaves { }
container premises {
diff --git a/integration-test/src/test/resources/data/bookstore/bookstoreDataForDeltaReport.json b/integration-test/src/test/resources/data/bookstore/bookstoreDataForDeltaReport.json
new file mode 100644
index 000000000..73b84fc98
--- /dev/null
+++ b/integration-test/src/test/resources/data/bookstore/bookstoreDataForDeltaReport.json
@@ -0,0 +1,192 @@
+{
+ "bookstore-address": [
+ {
+ "bookstore-name": "Crossword Bookstores",
+ "address": "Bangalore, India",
+ "postal-code": "560062"
+ }
+ ],
+ "bookstore": {
+ "bookstore-name": "Easons",
+ "premises": {
+ "addresses": [
+ {
+ "house-number": 2,
+ "street": "Main Street",
+ "town": "Killarney",
+ "county": "Kerry"
+ },
+ {
+ "house-number": 24,
+ "street": "Grafton Street",
+ "town": "Dublin",
+ "county": "Dublin"
+ }
+ ]
+ },
+ "support-info": {
+ "contact-emails": {
+ }
+ },
+ "container-without-leaves": { },
+ "categories": [
+ {
+ "code": 1,
+ "name": "Kids",
+ "books" : [
+ {
+ "title": "Matilda",
+ "lang": "English",
+ "authors": ["Roald Dahl"],
+ "editions": [1988, 2000, 2023],
+ "price": 200
+ },
+ {
+ "title": "The Gruffalo",
+ "lang": "English/German",
+ "authors": ["Julia Donaldson"],
+ "editions": [1999],
+ "price": 15
+ }
+ ]
+ },
+ {
+ "code": 2,
+ "name": "Suspense"
+ },
+ {
+ "code": 3,
+ "name": "Comedy",
+ "books" : [
+ {
+ "title": "Good Omens",
+ "lang": "English",
+ "authors": ["Neil Gaiman", "Terry Pratchett"],
+ "editions": [2006],
+ "price": 13
+ },
+ {
+ "title": "The Colour of Magic",
+ "lang": "English",
+ "authors": ["Terry Pratchett"],
+ "editions": [1983],
+ "price": 12
+ },
+ {
+ "title": "The Light Fantastic",
+ "lang": "English",
+ "authors": ["Terry Pratchett"],
+ "editions": [1986],
+ "price": 14
+ },
+ {
+ "title": "A Book with No Language",
+ "lang": "",
+ "authors": ["Joe Bloggs"],
+ "editions": [2023],
+ "price": 20
+ }
+ ]
+ },
+ {
+ "code": 5,
+ "name": "Discount books",
+ "books" : [
+ {
+ "title": "Book 1",
+ "lang": "blah",
+ "authors": [],
+ "editions": [],
+ "price": 1
+ },
+ {
+ "title": "Book 2",
+ "lang": "blah",
+ "authors": [],
+ "editions": [],
+ "price": 2
+ },
+ {
+ "title": "Book 3",
+ "lang": "blah",
+ "authors": [],
+ "editions": [],
+ "price": 3
+ },
+ {
+ "title": "Book 4",
+ "lang": "blah",
+ "authors": [],
+ "editions": [],
+ "price": 4
+ },
+ {
+ "title": "Book 5",
+ "lang": "blah",
+ "authors": [],
+ "editions": [],
+ "price": 5
+ },
+ {
+ "title": "Book 6",
+ "lang": "blah",
+ "authors": [],
+ "editions": [],
+ "price": 6
+ },
+ {
+ "title": "Book 7",
+ "lang": "blah",
+ "authors": [],
+ "editions": [],
+ "price": 7
+ },
+ {
+ "title": "Book 8",
+ "lang": "blahh",
+ "authors": [],
+ "editions": [],
+ "price": 8
+ },
+ {
+ "title": "Book 9",
+ "lang": "blah",
+ "authors": [],
+ "editions": [],
+ "price": 9
+ },
+ {
+ "title": "Book 10",
+ "lang": "blah",
+ "authors": [],
+ "editions": [],
+ "price": 10
+ },
+ {
+ "title": "Book 11",
+ "lang": "blah",
+ "authors": [],
+ "editions": [],
+ "price": 10
+ }
+ ]
+ },
+ {
+ "code": 6,
+ "name": "Random books",
+ "books": [
+ {
+ "title": "Book 1",
+ "lang": "blah",
+ "authors": [],
+ "editions": [],
+ "price": 1
+ }
+ ]
+ },
+ {
+ "code": 7
+ }
+ ]
+ }
+}