Refactor Cps Delta code from CpsDataService to CpsDeltaService 43/140343/22
authorArpit Singh <AS00745003@techmahindra.com>
Wed, 12 Mar 2025 07:03:35 +0000 (12:33 +0530)
committerArpit Singh <AS00745003@techmahindra.com>
Thu, 3 Apr 2025 04:53:29 +0000 (10:23 +0530)
- added new open api yaml file defining CPS delta endpoints
- Added new REST controller for CPS Delta
- Moved CPS Delta endpoints to new java interface, out from CPS Data
- Added integration tests for CPS Delta
- CPS Delta rest controller and java interface have the two delta
  endpoints: Delta between anchors and Delta between anchor and payload.

Issue-ID: CPS-2320
Change-Id: I556e70623a8c18f8cde3cd28bac890296019c0e2
Signed-off-by: Arpit Singh <AS00745003@techmahindra.com>
18 files changed:
cps-rest/docs/openapi/cpsDataV2.yml
cps-rest/docs/openapi/cpsDelta.yml [new file with mode: 0644]
cps-rest/docs/openapi/openapi.yml
cps-rest/src/main/java/org/onap/cps/rest/controller/DataRestController.java
cps-rest/src/main/java/org/onap/cps/rest/controller/DeltaRestController.java [new file with mode: 0644]
cps-rest/src/test/groovy/org/onap/cps/rest/controller/DataRestControllerSpec.groovy
cps-rest/src/test/groovy/org/onap/cps/rest/controller/DeltaRestControllerSpec.groovy [new file with mode: 0644]
cps-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy
cps-service/src/main/java/org/onap/cps/api/CpsDataService.java
cps-service/src/main/java/org/onap/cps/api/CpsDeltaService.java
cps-service/src/main/java/org/onap/cps/impl/CpsDataServiceImpl.java
cps-service/src/main/java/org/onap/cps/impl/CpsDeltaServiceImpl.java
cps-service/src/test/groovy/org/onap/cps/impl/CpsDataServiceImplSpec.groovy
cps-service/src/test/groovy/org/onap/cps/impl/CpsDeltaServiceImplSpec.groovy
cps-service/src/test/groovy/org/onap/cps/impl/E2ENetworkSliceSpec.groovy
integration-test/src/test/groovy/org/onap/cps/integration/base/CpsIntegrationSpecBase.groovy
integration-test/src/test/groovy/org/onap/cps/integration/functional/cps/DataServiceIntegrationSpec.groovy
integration-test/src/test/groovy/org/onap/cps/integration/functional/cps/DeltaServiceIntegrationSpec.groovy [new file with mode: 0644]

index 999c5b2..7afda70 100644 (file)
@@ -1,5 +1,5 @@
 # ============LICENSE_START=======================================================
-# Copyright (c) 2022-2024 TechMahindra Ltd.
+# Copyright (c) 2022-2025 TechMahindra Ltd.
 # ================================================================================
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -54,83 +54,3 @@ nodeByDataspaceAndAnchor:
       '500':
         $ref: 'components.yml#/components/responses/InternalServerError'
     x-codegen-request-body-name: xpath
-
-delta:
-  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/sourceAnchorNameInPath'
-      - $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'
-      '403':
-        $ref: 'components.yml#/components/responses/Forbidden'
-      '500':
-        $ref: 'components.yml#/components/responses/InternalServerError'
-    x-codegen-request-body-name: xpath
-  post:
-    description: Get delta between an anchor in a dataspace and JSON payload
-    tags:
-      - cps-data
-    summary: Get delta between an anchor and JSON payload
-    operationId: getDeltaByDataspaceAnchorAndPayload
-    parameters:
-      - $ref: 'components.yml#/components/parameters/dataspaceNameInPath'
-      - $ref: 'components.yml#/components/parameters/sourceAnchorNameInPath'
-      - $ref: 'components.yml#/components/parameters/xpathInQuery'
-    requestBody:
-      content:
-        multipart/form-data:
-          schema:
-            type: object
-            properties:
-              json:
-                type: object
-                example:
-                  test:bookstore:
-                    bookstore-name: Chapters
-                    categories:
-                      - code: 01
-                        name: SciFi
-                      - code: 02
-                        name: kids
-              file:
-                type: string
-                format: binary
-            required:
-              - json
-    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'
\ No newline at end of file
diff --git a/cps-rest/docs/openapi/cpsDelta.yml b/cps-rest/docs/openapi/cpsDelta.yml
new file mode 100644 (file)
index 0000000..67535ce
--- /dev/null
@@ -0,0 +1,97 @@
+# ============LICENSE_START=======================================================
+# Copyright (c) 2025 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=========================================================
+
+delta:
+  get:
+    description: Get delta between two anchors within a given dataspace
+    tags:
+      - cps-delta
+    summary: Get delta between anchors in the same dataspace
+    operationId: getDeltaByDataspaceAndAnchors
+    parameters:
+      - $ref: 'components.yml#/components/parameters/dataspaceNameInPath'
+      - $ref: 'components.yml#/components/parameters/sourceAnchorNameInPath'
+      - $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'
+      '403':
+        $ref: 'components.yml#/components/responses/Forbidden'
+      '500':
+        $ref: 'components.yml#/components/responses/InternalServerError'
+    x-codegen-request-body-name: xpath
+  post:
+    description: Get delta between an anchor in a dataspace and JSON payload
+    tags:
+      - cps-delta
+    summary: Get delta between an anchor and JSON payload
+    operationId: getDeltaByDataspaceAnchorAndPayload
+    parameters:
+      - $ref: 'components.yml#/components/parameters/dataspaceNameInPath'
+      - $ref: 'components.yml#/components/parameters/sourceAnchorNameInPath'
+      - $ref: 'components.yml#/components/parameters/xpathInQuery'
+    requestBody:
+      content:
+        multipart/form-data:
+          schema:
+            type: object
+            properties:
+              json:
+                type: object
+                example:
+                  test:bookstore:
+                    bookstore-name: Chapters
+                    categories:
+                      - code: 01
+                        name: SciFi
+                      - code: 02
+                        name: kids
+              file:
+                type: string
+                format: binary
+            required:
+              - json
+    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'
index 09c454b..c3f3a02 100644 (file)
@@ -44,6 +44,8 @@ tags:
     description: cps Admin
   - name: cps-data
     description: cps Data
+  - name: cps-delta
+    description: CPS Delta
 paths:
 
   /v1/dataspaces:
@@ -104,7 +106,7 @@ paths:
     $ref: 'cpsData.yml#/listElementByDataspaceAndAnchor'
 
   /v2/dataspaces/{dataspace-name}/anchors/{source-anchor-name}/delta:
-    $ref: 'cpsDataV2.yml#/delta'
+    $ref: 'cpsDelta.yml#/delta'
 
   /v1/dataspaces/{dataspace-name}/anchors/{anchor-name}/nodes/query:
     $ref: 'cpsQueryV1Deprecated.yml#/nodesByDataspaceAndAnchorAndCpsPath'
index b6a2e42..90500f3 100755 (executable)
@@ -3,7 +3,7 @@
  *  Copyright (C) 2020-2022 Bell Canada.
  *  Modifications Copyright (C) 2021 Pantheon.tech
  *  Modifications Copyright (C) 2021-2025 Nordix Foundation
- *  Modifications Copyright (C) 2022-2024 TechMahindra Ltd.
+ *  Modifications Copyright (C) 2022-2025 TechMahindra Ltd.
  *  Modifications Copyright (C) 2022 Deutsche Telekom AG
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
 
 package org.onap.cps.rest.controller;
 
-import static org.onap.cps.rest.utils.MultipartFileUtil.extractYangResourcesMap;
-
 import io.micrometer.core.annotation.Timed;
 import jakarta.validation.ValidationException;
 import java.time.OffsetDateTime;
 import java.time.format.DateTimeFormatter;
-import java.util.Collection;
-import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import lombok.RequiredArgsConstructor;
 import org.apache.commons.lang3.StringUtils;
 import org.onap.cps.api.CpsDataService;
 import org.onap.cps.api.CpsFacade;
-import org.onap.cps.api.model.DeltaReport;
 import org.onap.cps.api.parameters.FetchDescendantsOption;
 import org.onap.cps.rest.api.CpsDataApi;
 import org.onap.cps.utils.ContentType;
@@ -48,7 +43,6 @@ import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
-import org.springframework.web.multipart.MultipartFile;
 
 @RestController
 @RequestMapping("${rest.api.cps-base-path}")
@@ -193,44 +187,6 @@ public class DataRestController implements CpsDataApi {
         return new ResponseEntity<>(HttpStatus.NO_CONTENT);
     }
 
-    @Override
-    public ResponseEntity<Object> getDeltaByDataspaceAnchorAndPayload(final String dataspaceName,
-                                                                      final String sourceAnchorName,
-                                                                      final Object jsonPayload,
-                                                                      final String xpath,
-                                                                      final MultipartFile multipartFile) {
-        final FetchDescendantsOption fetchDescendantsOption = FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS;
-
-        final Map<String, String> yangResourceMap;
-        if (multipartFile == null) {
-            yangResourceMap = Collections.emptyMap();
-        } else {
-            yangResourceMap = extractYangResourcesMap(multipartFile);
-        }
-        final Collection<DeltaReport> deltaReports = Collections.unmodifiableList(
-                cpsDataService.getDeltaByDataspaceAnchorAndPayload(dataspaceName, sourceAnchorName,
-                xpath, yangResourceMap, jsonPayload.toString(), fetchDescendantsOption));
-
-        return new ResponseEntity<>(jsonObjectMapper.asJsonString(deltaReports), HttpStatus.OK);
-    }
-
-    @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 ResponseEntity<Object> buildResponseEntity(final List<Map<String, Object>> dataMaps,
                                                final ContentType contentType) {
         final String responseData;
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
new file mode 100644 (file)
index 0000000..f27346c
--- /dev/null
@@ -0,0 +1,89 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2025 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.rest.controller;
+
+import static org.onap.cps.rest.utils.MultipartFileUtil.extractYangResourcesMap;
+
+import io.micrometer.core.annotation.Timed;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import lombok.RequiredArgsConstructor;
+import org.onap.cps.api.CpsDeltaService;
+import org.onap.cps.api.model.DeltaReport;
+import org.onap.cps.api.parameters.FetchDescendantsOption;
+import org.onap.cps.rest.api.CpsDeltaApi;
+import org.onap.cps.utils.JsonObjectMapper;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+@RestController
+@RequestMapping("${rest.api.cps-base-path}")
+@RequiredArgsConstructor
+public class DeltaRestController implements CpsDeltaApi {
+
+    private final CpsDeltaService cpsDeltaService;
+    private final JsonObjectMapper jsonObjectMapper;
+
+
+    @Timed(value = "cps.delta.controller.get.delta",
+        description = "Time taken to get delta between anchors")
+    @Override
+    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 =
+            cpsDeltaService.getDeltaByDataspaceAndAnchors(dataspaceName, sourceAnchorName,
+                targetAnchorName, xpath, fetchDescendantsOption);
+        return new ResponseEntity<>(jsonObjectMapper.asJsonString(deltaBetweenAnchors), HttpStatus.OK);
+    }
+
+    @Timed(value = "cps.delta.controller.get.delta",
+        description = "Time taken to get delta between anchors")
+    @Override
+    public ResponseEntity<Object> getDeltaByDataspaceAnchorAndPayload(final String dataspaceName,
+                                                                      final String sourceAnchorName,
+                                                                      final Object jsonPayload,
+                                                                      final String xpath,
+                                                                      final MultipartFile multipartFile) {
+        final FetchDescendantsOption fetchDescendantsOption = FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS;
+
+        final Map<String, String> yangResourceMap;
+        if (multipartFile == null) {
+            yangResourceMap = Collections.emptyMap();
+        } else {
+            yangResourceMap = extractYangResourcesMap(multipartFile);
+        }
+        final Collection<DeltaReport> deltaReports = Collections.unmodifiableList(
+            cpsDeltaService.getDeltaByDataspaceAnchorAndPayload(dataspaceName, sourceAnchorName,
+                xpath, yangResourceMap, jsonPayload.toString(), fetchDescendantsOption));
+        return new ResponseEntity<>(jsonObjectMapper.asJsonString(deltaReports), HttpStatus.OK);
+    }
+
+}
index e4cd8c4..ba5104a 100755 (executable)
@@ -4,7 +4,7 @@
  *  Modifications Copyright (C) 2021 Pantheon.tech
  *  Modifications Copyright (C) 2021-2022 Bell Canada.
  *  Modifications Copyright (C) 2022 Deutsche Telekom AG
- *  Modifications Copyright (C) 2022-2024 TechMahindra Ltd.
+ *  Modifications Copyright (C) 2022-2025 TechMahindra Ltd.
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -27,7 +27,6 @@ package org.onap.cps.rest.controller
 import com.fasterxml.jackson.databind.ObjectMapper
 import org.onap.cps.api.CpsDataService
 import org.onap.cps.api.CpsFacade
-import org.onap.cps.impl.DeltaReportBuilder
 import org.onap.cps.utils.ContentType
 import org.onap.cps.utils.DateTimeUtility
 import org.onap.cps.utils.JsonObjectMapper
@@ -37,7 +36,6 @@ import org.springframework.beans.factory.annotation.Value
 import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
 import org.springframework.http.HttpStatus
 import org.springframework.http.MediaType
-import org.springframework.mock.web.MockMultipartFile
 import org.springframework.test.web.servlet.MockMvc
 import spock.lang.Shared
 import spock.lang.Specification
@@ -46,7 +44,6 @@ import static org.onap.cps.api.parameters.FetchDescendantsOption.INCLUDE_ALL_DES
 import static org.onap.cps.api.parameters.FetchDescendantsOption.OMIT_DESCENDANTS
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
-import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put
@@ -87,9 +84,6 @@ class DataRestControllerSpec extends Specification {
     @Shared
     def expectedXmlData = '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n<bookstore xmlns="org:onap:ccsdk:sample">\n</bookstore>'
 
-    @Shared
-    def multipartYangFile = new MockMultipartFile("file", 'filename.yang', "text/plain", 'content'.getBytes())
-
     def setup() {
         dataNodeBaseEndpointV1 = "$basePath/v1/dataspaces/$dataspaceName"
         dataNodeBaseEndpointV2 = "$basePath/v2/dataspaces/$dataspaceName"
@@ -308,65 +302,6 @@ class DataRestControllerSpec extends Specification {
             'JSON'   | MediaType.APPLICATION_JSON || '[{"mocked":"result1"},{"mocked":"result2"}]'
     }
 
-    def 'Get delta between two anchors.'() {
-        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'
-            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\":\"replace\",\"xpath\":\"some xpath\",\"sourceData\":{\"some key\":\"some value\"},\"targetData\":{\"some key\":\"some value\"}}]")
-    }
-
-    def 'Get delta between anchor and JSON payload with multipart file'() {
-        given: 'sample delta report, xpath, yang model file and json payload'
-            def deltaReports = new DeltaReportBuilder().actionCreate().withXpath('some xpath').build()
-            def xpath = 'some xpath'
-            def endpoint = "$dataNodeBaseEndpointV2/anchors/$anchorName/delta"
-        and: 'the service layer returns a list containing delta reports'
-            mockCpsDataService.getDeltaByDataspaceAnchorAndPayload(dataspaceName, anchorName, xpath, ['filename.yang':'content'], expectedJsonData, INCLUDE_ALL_DESCENDANTS) >> [deltaReports]
-        when: 'get delta request is performed using REST API'
-            def response =
-                    mvc.perform(multipart(endpoint)
-                            .file(multipartYangFile)
-                            .param("json", requestBodyJson)
-                            .param('xpath', xpath)
-                            .contentType(MediaType.MULTIPART_FORM_DATA))
-                            .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\":\"create\",\"xpath\":\"some xpath\"}]")
-    }
-
-    def 'Get delta between anchor and JSON payload without multipart file.'() {
-        given: 'sample delta report, xpath, and json payload'
-            def deltaReports = new DeltaReportBuilder().actionRemove().withXpath('some xpath').build()
-            def xpath = 'some xpath'
-            def endpoint = "$dataNodeBaseEndpointV2/anchors/$anchorName/delta"
-        and: 'the service layer returns a list containing delta reports'
-            mockCpsDataService.getDeltaByDataspaceAnchorAndPayload(dataspaceName, anchorName, xpath, [:], expectedJsonData, INCLUDE_ALL_DESCENDANTS) >> [deltaReports]
-        when: 'get delta request is performed using REST API'
-            def response =
-                    mvc.perform(multipart(endpoint)
-                            .param("json", requestBodyJson)
-                            .param('xpath', xpath)
-                            .contentType(MediaType.MULTIPART_FORM_DATA))
-                            .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\":\"remove\",\"xpath\":\"some xpath\"}]")
-    }
-
     def 'Update data node leaves: #scenario.'() {
         given: 'endpoint to update a node '
             def endpoint = "$dataNodeBaseEndpointV1/anchors/$anchorName/nodes"
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
new file mode 100644 (file)
index 0000000..18c0f13
--- /dev/null
@@ -0,0 +1,129 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2025 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.rest.controller
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import org.onap.cps.api.CpsDeltaService
+import org.onap.cps.impl.DeltaReportBuilder
+import org.onap.cps.utils.JsonObjectMapper
+import org.spockframework.spring.SpringBean
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
+import org.springframework.http.HttpStatus
+import org.springframework.http.MediaType
+import org.springframework.mock.web.MockMultipartFile
+import org.springframework.test.web.servlet.MockMvc
+import org.springframework.web.multipart.MultipartFile
+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
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart
+
+@WebMvcTest(DeltaRestController)
+class DeltaRestControllerSpec extends Specification {
+
+    @SpringBean
+    CpsDeltaService mockCpsDeltaService = Mock()
+
+    @SpringBean
+    JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
+
+    @Autowired
+    MockMvc mvc
+
+    @Value('${rest.api.cps-base-path}')
+    def basePath
+
+    def dataNodeBaseEndpointV2
+    def dataspaceName = 'my_dataspace'
+    def anchorName = 'my_anchor'
+
+    @Shared
+    def requestBodyJson = '{"some-key":"some-value","categories":[{"books":[{"authors":["Iain M. Banks"]}]}]}'
+    @Shared
+    def expectedJsonData = '{"some-key":"some-value","categories":[{"books":[{"authors":["Iain M. Banks"]}]}]}'
+    @Shared
+    static MultipartFile multipartYangFile = new MockMultipartFile('file', 'filename.yang', 'text/plain', 'content'.getBytes())
+
+    def setup() {
+        dataNodeBaseEndpointV2 = "$basePath/v2/dataspaces/$dataspaceName/anchors/$anchorName/delta"
+    }
+
+    def 'Get delta between two anchors'() {
+        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]
+        when: 'get delta request is performed using REST API'
+            def response =
+                mvc.perform(get(dataNodeBaseEndpointV2)
+                    .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\":\"replace\",\"xpath\":\"some xpath\",\"sourceData\":{\"some key\":\"some value\"},\"targetData\":{\"some key\":\"some value\"}}]')
+    }
+
+    def 'Get delta between anchor and JSON payload with multipart file'() {
+        given: 'sample delta report, xpath, yang model file and json payload'
+            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]
+        when: 'get delta request is performed using REST API'
+            def response =
+                mvc.perform(multipart(dataNodeBaseEndpointV2)
+                    .file(multipartYangFile)
+                    .param('json', requestBodyJson)
+                    .param('xpath', xpath)
+                    .contentType(MediaType.MULTIPART_FORM_DATA))
+                    .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\":\"create\",\"xpath\":\"some xpath\"}]')
+    }
+
+    def 'Get delta between anchor and JSON payload without multipart file'() {
+        given: 'sample delta report, xpath, and json payload'
+            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]
+        when: 'get delta request is performed using REST API'
+            def response =
+                mvc.perform(multipart(dataNodeBaseEndpointV2)
+                    .param('json', requestBodyJson)
+                    .param('xpath', xpath)
+                    .contentType(MediaType.MULTIPART_FORM_DATA))
+                    .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\":\"remove\",\"xpath\":\"some xpath\"}]')
+    }
+}
index 0cbdffb..1d58197 100644 (file)
@@ -29,6 +29,7 @@ import groovy.json.JsonSlurper
 import org.onap.cps.api.CpsAnchorService
 import org.onap.cps.api.CpsDataService
 import org.onap.cps.api.CpsDataspaceService
+import org.onap.cps.api.CpsDeltaService
 import org.onap.cps.api.CpsFacade
 import org.onap.cps.api.CpsModuleService
 import org.onap.cps.api.CpsNotificationService
@@ -95,6 +96,9 @@ class CpsRestExceptionHandlerSpec extends Specification {
     @SpringBean
     CpsNotificationService mockCpsNotificationService = Stub()
 
+    @SpringBean
+    CpsDeltaService cpsDeltaService = Stub()
+
     @Autowired
     MockMvc mvc
 
index 4171374..5d48812 100644 (file)
@@ -4,7 +4,7 @@
  *  Modifications Copyright (C) 2021 Pantheon.tech
  *  Modifications Copyright (C) 2021-2022 Bell Canada
  *  Modifications Copyright (C) 2022 Deutsche Telekom AG
- *  Modifications Copyright (C) 2023-2024 TechMahindra Ltd.
+ *  Modifications Copyright (C) 2023-2025 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,10 +26,8 @@ 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.api.model.DataNode;
-import org.onap.cps.api.model.DeltaReport;
 import org.onap.cps.api.parameters.FetchDescendantsOption;
 import org.onap.cps.utils.ContentType;
 
@@ -293,38 +291,6 @@ public interface CpsDataService {
      */
     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);
-
-    /**
-     * Retrieves the delta between an anchor and JSON payload by xpath, using dataspace name and anchor name.
-     *
-     * @param dataspaceName                     source dataspace name
-     * @param sourceAnchorName                  source anchor name
-     * @param xpath                             xpath
-     * @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
-     * @return                                  list containing {@link DeltaReport} objects
-     */
-    List<DeltaReport> getDeltaByDataspaceAnchorAndPayload(String dataspaceName, String sourceAnchorName, String xpath,
-                                                          Map<String, String> yangResourceContentPerName,
-                                                          String targetData,
-                                                          FetchDescendantsOption fetchDescendantsOption);
-
-
     /**
      * Validates JSON or XML data by parsing it using the schema associated to an anchor within the given dataspace.
      * Validation is performed without persisting the data.
index 67c4da6..671b1d6 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2023 TechMahindra Ltd.
+ *  Copyright (C) 2023-2025 TechMahindra Ltd.
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
 
 package org.onap.cps.api;
 
-import java.util.Collection;
 import java.util.List;
-import org.onap.cps.api.model.DataNode;
+import java.util.Map;
 import org.onap.cps.api.model.DeltaReport;
+import org.onap.cps.api.parameters.FetchDescendantsOption;
 
 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.
+     * Retrieves the delta between two anchors by xpath within a dataspace. Returns a List of {@link DeltaReport}.
+     * Each Delta Report contains following information: action, xpath, source-payload and/or 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
+     * @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> getDeltaReports(Collection<DataNode> sourceDataNodes,
-                                      Collection<DataNode> targetDataNodes);
+    List<DeltaReport> getDeltaByDataspaceAndAnchors(String dataspaceName, String sourceAnchorName,
+                                                    String targetAnchorName, String xpath,
+                                                    FetchDescendantsOption fetchDescendantsOption);
+
+    /**
+     * Retrieves the delta between an anchor and JSON payload by xpath, using dataspace name and anchor name.
+     * Returns a List of {@link DeltaReport}. Each Delta Report contains following information: action, xpath,
+     * source-payload and/or target-payload.
+     *
+     * @param dataspaceName                source dataspace name
+     * @param sourceAnchorName             source anchor name
+     * @param xpath                        xpath
+     * @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
+     *
+     * @return                             list containing {@link DeltaReport} objects
+     */
+    List<DeltaReport> getDeltaByDataspaceAnchorAndPayload(String dataspaceName, String sourceAnchorName, String xpath,
+                                                          Map<String, String> yangResourceContentPerName,
+                                                          String targetData,
+                                                          FetchDescendantsOption fetchDescendantsOption);
+
+
 }
index 586941a..16dbc7b 100644 (file)
@@ -31,21 +31,17 @@ import static org.onap.cps.utils.ContentType.JSON;
 import io.micrometer.core.annotation.Timed;
 import java.io.Serializable;
 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.CpsAnchorService;
 import org.onap.cps.api.CpsDataService;
-import org.onap.cps.api.CpsDeltaService;
 import org.onap.cps.api.DataNodeFactory;
 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.events.CpsDataUpdateEventsProducer;
@@ -53,8 +49,6 @@ import org.onap.cps.events.model.Data.Operation;
 import org.onap.cps.spi.CpsDataPersistenceService;
 import org.onap.cps.utils.ContentType;
 import org.onap.cps.utils.CpsValidator;
-import org.onap.cps.utils.DataMapper;
-import org.onap.cps.utils.JsonObjectMapper;
 import org.onap.cps.utils.YangParser;
 import org.springframework.stereotype.Service;
 
@@ -69,12 +63,8 @@ public class CpsDataServiceImpl implements CpsDataService {
     private final CpsDataUpdateEventsProducer cpsDataUpdateEventsProducer;
     private final CpsAnchorService cpsAnchorService;
     private final DataNodeFactory dataNodeFactory;
-
     private final CpsValidator cpsValidator;
     private final YangParser yangParser;
-    private final CpsDeltaService cpsDeltaService;
-    private final DataMapper dataMapper;
-    private final JsonObjectMapper jsonObjectMapper;
 
     @Override
     public void saveData(final String dataspaceName, final String anchorName, final String nodeData,
@@ -175,8 +165,7 @@ public class CpsDataServiceImpl implements CpsDataService {
         cpsValidator.validateNameCharacters(dataspaceName, anchorName);
         final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName);
         final Collection<DataNode> dataNodeUpdates = dataNodeFactory
-                .createDataNodesWithAnchorParentXpathAndNodeData(anchor, parentNodeXpath, dataNodeUpdatesAsJson,
-                        JSON);
+                .createDataNodesWithAnchorParentXpathAndNodeData(anchor, parentNodeXpath, dataNodeUpdatesAsJson, JSON);
         for (final DataNode dataNodeUpdate : dataNodeUpdates) {
             processDataNodeUpdate(anchor, dataNodeUpdate);
         }
@@ -204,44 +193,6 @@ public class CpsDataServiceImpl implements CpsDataService {
         cpsDataPersistenceService.lockAnchor(sessionID, dataspaceName, anchorName, timeoutInMilliseconds);
     }
 
-    @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.get.deltaBetweenAnchorAndPayload",
-            description = "Time taken to get delta between anchor and a payload")
-    public List<DeltaReport> getDeltaByDataspaceAnchorAndPayload(final String dataspaceName,
-                                                                final String sourceAnchorName, final String xpath,
-                                                                final Map<String, String> yangResourceContentPerName,
-                                                                final String targetData,
-                                                                final FetchDescendantsOption fetchDescendantsOption) {
-
-        final Anchor sourceAnchor = cpsAnchorService.getAnchor(dataspaceName, sourceAnchorName);
-
-        final Collection<DataNode> sourceDataNodes = getDataNodes(dataspaceName,
-                sourceAnchorName, xpath, fetchDescendantsOption);
-
-        final Collection<DataNode> sourceDataNodesRebuilt =
-                new ArrayList<>(rebuildSourceDataNodes(xpath, sourceAnchor, sourceDataNodes));
-
-        final Collection<DataNode> targetDataNodes =
-                new ArrayList<>(buildTargetDataNodes(sourceAnchor, xpath, yangResourceContentPerName, targetData));
-
-        return cpsDeltaService.getDeltaReports(sourceDataNodesRebuilt, targetDataNodes);
-    }
-
     @Override
     @Timed(value = "cps.data.service.datanode.descendants.update",
         description = "Time taken to update a data node and descendants")
@@ -357,31 +308,6 @@ public class CpsDataServiceImpl implements CpsDataService {
         yangParser.validateData(contentType, nodeData, anchor, xpath);
     }
 
-    private Collection<DataNode> rebuildSourceDataNodes(final String xpath,
-                                                        final Anchor sourceAnchor,
-                                                        final Collection<DataNode> sourceDataNodes) {
-        final Collection<DataNode> sourceDataNodesRebuilt = new ArrayList<>();
-        if (sourceDataNodes != null) {
-            final Map<String, Object> sourceDataNodesAsMap = dataMapper.toFlatDataMap(sourceAnchor, sourceDataNodes);
-            final String sourceDataNodesAsJson = jsonObjectMapper.asJsonString(sourceDataNodesAsMap);
-            final Collection<DataNode> dataNodes = dataNodeFactory
-                    .createDataNodesWithAnchorXpathAndNodeData(sourceAnchor, xpath, sourceDataNodesAsJson, JSON);
-            sourceDataNodesRebuilt.addAll(dataNodes);
-        }
-        return sourceDataNodesRebuilt;
-    }
-
-    private Collection<DataNode> buildTargetDataNodes(final Anchor sourceAnchor,
-                                                      final String xpath,
-                                                      final Map<String, String> yangResourceContentPerName,
-                                                      final String targetData) {
-        if (yangResourceContentPerName.isEmpty()) {
-            return dataNodeFactory.createDataNodesWithAnchorXpathAndNodeData(sourceAnchor, xpath, targetData, JSON);
-        }
-        return dataNodeFactory
-            .createDataNodesWithYangResourceXpathAndNodeData(yangResourceContentPerName, xpath, targetData, JSON);
-    }
-
     private void processDataNodeUpdate(final Anchor anchor, final DataNode dataNodeUpdate) {
         cpsDataPersistenceService.batchUpdateDataLeaves(anchor.getDataspaceName(), anchor.getName(),
                 Collections.singletonMap(dataNodeUpdate.getXpath(), dataNodeUpdate.getLeaves()));
index d532001..650aa99 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2023 TechMahindra Ltd.
+ *  Copyright (C) 2023-2025 TechMahindra Ltd.
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
@@ -20,6 +20,9 @@
 
 package org.onap.cps.impl;
 
+import static org.onap.cps.utils.ContentType.JSON;
+
+import io.micrometer.core.annotation.Timed;
 import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -28,19 +31,69 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.onap.cps.api.CpsAnchorService;
+import org.onap.cps.api.CpsDataService;
 import org.onap.cps.api.CpsDeltaService;
+import org.onap.cps.api.DataNodeFactory;
+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.utils.DataMapper;
+import org.onap.cps.utils.JsonObjectMapper;
 import org.springframework.stereotype.Service;
 
 @Slf4j
 @Service
+@RequiredArgsConstructor
 public class CpsDeltaServiceImpl implements CpsDeltaService {
 
+    private final CpsAnchorService cpsAnchorService;
+    private final CpsDataService cpsDataService;
+    private final DataNodeFactory dataNodeFactory;
+    private final DataMapper dataMapper;
+    private final JsonObjectMapper jsonObjectMapper;
+
     @Override
-    public List<DeltaReport> getDeltaReports(final Collection<DataNode> sourceDataNodes,
-                                             final Collection<DataNode> targetDataNodes) {
+    @Timed(value = "cps.delta.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 = cpsDataService.getDataNodesForMultipleXpaths(dataspaceName,
+            sourceAnchorName, Collections.singletonList(xpath), fetchDescendantsOption);
+        final Collection<DataNode> targetDataNodes = cpsDataService.getDataNodesForMultipleXpaths(dataspaceName,
+            targetAnchorName, Collections.singletonList(xpath), fetchDescendantsOption);
+        return getDeltaReports(sourceDataNodes, targetDataNodes);
+    }
+
+    @Timed(value = "cps.delta.service.get.delta",
+        description = "Time taken to get delta between anchor and a payload")
+    @Override
+    public List<DeltaReport> getDeltaByDataspaceAnchorAndPayload(final String dataspaceName,
+                                                                 final String sourceAnchorName,
+                                                                 final String xpath,
+                                                                 final Map<String, String> yangResourceContentPerName,
+                                                                 final String targetData,
+                                                                 final FetchDescendantsOption fetchDescendantsOption) {
+
+        final Anchor sourceAnchor = cpsAnchorService.getAnchor(dataspaceName, sourceAnchorName);
+        final Collection<DataNode> sourceDataNodes = cpsDataService.getDataNodesForMultipleXpaths(dataspaceName,
+            sourceAnchorName, Collections.singletonList(xpath), fetchDescendantsOption);
+        final Collection<DataNode> sourceDataNodesRebuilt =
+            rebuildSourceDataNodes(xpath, sourceAnchor, sourceDataNodes);
+        final Collection<DataNode> targetDataNodes = new ArrayList<>(
+            buildTargetDataNodes(sourceAnchor, xpath, yangResourceContentPerName, targetData));
+        return getDeltaReports(sourceDataNodesRebuilt, targetDataNodes);
+    }
+
+    private List<DeltaReport> getDeltaReports(final Collection<DataNode> sourceDataNodes,
+                                              final Collection<DataNode> targetDataNodes) {
 
         final List<DeltaReport> deltaReport = new ArrayList<>();
 
@@ -48,7 +101,6 @@ public class CpsDeltaServiceImpl implements CpsDeltaService {
         final Map<String, DataNode> xpathToTargetDataNodes = convertToXPathToDataNodesMap(targetDataNodes);
 
         deltaReport.addAll(getRemovedAndUpdatedDeltaReports(xpathToSourceDataNodes, xpathToTargetDataNodes));
-
         deltaReport.addAll(getAddedDeltaReports(xpathToSourceDataNodes, xpathToTargetDataNodes));
 
         return Collections.unmodifiableList(deltaReport);
@@ -165,7 +217,6 @@ public class CpsDeltaServiceImpl implements CpsDeltaService {
             }
         } else if (sourceLeaf == null) {
             targetDataInDeltaReport.put(key, targetLeaf);
-
         } else {
             sourceDataInDeltaReport.put(key, sourceLeaf);
         }
@@ -199,4 +250,30 @@ public class CpsDeltaServiceImpl implements CpsDeltaService {
         }
         return addedDeltaReportEntries;
     }
+
+    private Collection<DataNode> rebuildSourceDataNodes(final String xpath,
+                                                        final Anchor sourceAnchor,
+                                                        final Collection<DataNode> sourceDataNodes) {
+        final Collection<DataNode> sourceDataNodesRebuilt = new ArrayList<>();
+        if (sourceDataNodes != null) {
+            final Map<String, Object> sourceDataNodesAsMap = dataMapper.toFlatDataMap(sourceAnchor, sourceDataNodes);
+            final String sourceDataNodesAsJson = jsonObjectMapper.asJsonString(sourceDataNodesAsMap);
+            final Collection<DataNode> dataNodes = dataNodeFactory
+                .createDataNodesWithAnchorXpathAndNodeData(sourceAnchor, xpath, sourceDataNodesAsJson, JSON);
+            sourceDataNodesRebuilt.addAll(dataNodes);
+        }
+        return sourceDataNodesRebuilt;
+    }
+
+    private Collection<DataNode> buildTargetDataNodes(final Anchor sourceAnchor, final String xpath,
+                                                      final Map<String, String> yangResourceContentPerName,
+                                                      final String targetData) {
+        if (yangResourceContentPerName.isEmpty()) {
+            return dataNodeFactory
+                .createDataNodesWithAnchorXpathAndNodeData(sourceAnchor, xpath, targetData, JSON);
+        } else {
+            return dataNodeFactory
+                .createDataNodesWithYangResourceXpathAndNodeData(yangResourceContentPerName, xpath, targetData, JSON);
+        }
+    }
 }
index a4bfd56..3984743 100644 (file)
@@ -26,10 +26,8 @@ package org.onap.cps.impl
 import ch.qos.logback.classic.Level
 import ch.qos.logback.classic.Logger
 import ch.qos.logback.core.read.ListAppender
-import com.fasterxml.jackson.databind.ObjectMapper
 import org.onap.cps.TestUtils
 import org.onap.cps.api.CpsAnchorService
-import org.onap.cps.api.CpsDeltaService
 import org.onap.cps.api.exceptions.ConcurrencyException
 import org.onap.cps.api.exceptions.DataNodeNotFoundExceptionBatch
 import org.onap.cps.api.exceptions.DataValidationException
@@ -41,9 +39,6 @@ import org.onap.cps.events.CpsDataUpdateEventsProducer
 import org.onap.cps.spi.CpsDataPersistenceService
 import org.onap.cps.utils.ContentType
 import org.onap.cps.utils.CpsValidator
-import org.onap.cps.utils.DataMapper
-import org.onap.cps.utils.JsonObjectMapper
-import org.onap.cps.utils.PrefixResolver
 import org.onap.cps.utils.YangParser
 import org.onap.cps.utils.YangParserHelper
 import org.onap.cps.yang.TimedYangTextSchemaSourceSetBuilder
@@ -65,15 +60,11 @@ class CpsDataServiceImplSpec extends Specification {
     def mockCpsValidator = Mock(CpsValidator)
     def mockTimedYangTextSchemaSourceSetBuilder = Mock(TimedYangTextSchemaSourceSetBuilder)
     def yangParser = new YangParser(new YangParserHelper(), mockYangTextSchemaSourceSetCache, mockTimedYangTextSchemaSourceSetBuilder)
-    def mockCpsDeltaService = Mock(CpsDeltaService);
     def mockCpsDataUpdateEventsProducer = Mock(CpsDataUpdateEventsProducer)
-    def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
-    def mockPrefixResolver = Mock(PrefixResolver)
-    def dataMapper = new DataMapper(mockCpsAnchorService, mockPrefixResolver)
     def dataNodeFactory = new DataNodeFactoryImpl(yangParser)
 
     def objectUnderTest = new CpsDataServiceImpl(mockCpsDataPersistenceService, mockCpsDataUpdateEventsProducer, mockCpsAnchorService,
-            dataNodeFactory, mockCpsValidator, yangParser, mockCpsDeltaService, dataMapper, jsonObjectMapper)
+            dataNodeFactory, mockCpsValidator, yangParser)
 
     def logger = (Logger) LoggerFactory.getLogger(objectUnderTest.class)
     def loggingListAppender
@@ -236,76 +227,6 @@ 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 'Get delta between anchor and payload with user provided schema #scenario'() {
-        given: 'user provided schema set '
-            def yangResourceContentPerName = TestUtils.getYangResourcesAsMap('bookstore.yang')
-            setupSchemaSetMocksForDelta(yangResourceContentPerName)
-        when: 'attempt to get delta between an anchor and a JSON payload'
-            objectUnderTest.getDeltaByDataspaceAnchorAndPayload(dataspaceName, anchorName, xpath, yangResourceContentPerName, jsonData, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS)
-        then: 'dataspacename and anchor names are validated'
-            1 * mockCpsValidator.validateNameCharacters(['some-dataspace', 'some-anchor'])
-        and: 'source data nodes are fetched using appropriate persistence layer method'
-            1 * mockCpsDataPersistenceService.getDataNodes(dataspaceName, anchorName, xpath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> sourceDataNodes
-        and: 'appropriate delta service method is invoked once with correct source and target data nodes'
-            1 * mockCpsDeltaService.getDeltaReports({sourceDataNodesRebuilt -> sourceDataNodesRebuilt.xpath[0] == expectedNodeXpath}, {targetDataNodes -> targetDataNodes.xpath[0] == expectedNodeXpath})
-        where: 'following data was used'
-            scenario             | xpath                               | sourceDataNodes                                                                                          | jsonData                                       || expectedNodeXpath
-            'root node xpath'    | '/'                                 | [new DataNodeBuilder().withXpath('/bookstore').build()]                                                  | '{"bookstore":{"bookstore-name":"Easons"}}'    || '/bookstore'
-            'parent xpath'       | '/bookstore'                        | [new DataNodeBuilder().withXpath('/bookstore').build()]                                                  | '{"bookstore":{"bookstore-name":"Easons"}}'    || '/bookstore'
-            'non-root xpath'     | '/bookstore/categories[@code="02"]' | [new DataNodeBuilder().withXpath('/bookstore/categories[@code="02"]').withLeaves(["code":"02"]).build()] | '{"categories":[{"name":"kids","code":"02"}]}' || '/bookstore/categories[@code=\'02\']'
-    }
-
-    def 'Get delta between anchor and payload by using schema from anchor #scenario'() {
-        given: 'schema set for a given dataspace and anchor'
-            setupSchemaSetMocks("bookstore.yang")
-        when: 'attempt to get delta between an anchor and a JSON payload'
-            objectUnderTest.getDeltaByDataspaceAnchorAndPayload(dataspaceName, anchorName, xpath, [:], jsonData, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS)
-        then: 'dataspacename and anchor names are validated'
-            1 * mockCpsValidator.validateNameCharacters(['some-dataspace', 'some-anchor'])
-        and: 'source data nodes are fetched using appropriate persistence layer method'
-            1 * mockCpsDataPersistenceService.getDataNodes(dataspaceName, anchorName, xpath, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS) >> sourceDataNodes
-        and: 'appropriate delta service method is invoked once with correct source and target data nodes'
-            1 * mockCpsDeltaService.getDeltaReports({sourceDataNodesRebuilt -> sourceDataNodesRebuilt.xpath[0] == expectedNodeXpath}, {targetDataNodes -> targetDataNodes.xpath[0] == expectedNodeXpath})
-        where: 'following data was used'
-            scenario          | xpath                               | sourceDataNodes                                                                                          | jsonData                                       || expectedNodeXpath
-            'root node xpath' | '/'                                 | [new DataNodeBuilder().withXpath('/bookstore').build()]                                                  | '{"bookstore":{"bookstore-name":"Easons"}}'    || '/bookstore'
-            'parent xpath'    | '/bookstore'                        | [new DataNodeBuilder().withXpath('/bookstore').build()]                                                  | '{"bookstore":{"bookstore-name":"Easons"}}'    || '/bookstore'
-            'non-root xpath'  | '/bookstore/categories[@code="02"]' | [new DataNodeBuilder().withXpath('/bookstore/categories[@code="02"]').withLeaves(["code":"02"]).build()] | '{"categories":[{"name":"kids","code":"02"}]}' || '/bookstore/categories[@code=\'02\']'
-    }
-
-    def 'Delta between anchor and payload error scenario #scenario'() {
-        given: 'schema set for given anchor and dataspace references bookstore model'
-            def yangResourceContentPerName = TestUtils.getYangResourcesAsMap('bookstore.yang')
-            setupSchemaSetMocksForDelta(yangResourceContentPerName)
-        when: 'attempt to get delta between anchor and payload'
-            objectUnderTest.getDeltaByDataspaceAnchorAndPayload(dataspaceName, anchorName, xpath, yangResourceContentPerName, jsonData, FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS)
-        then: 'expected exception is thrown'
-            thrown(DataValidationException)
-        where: 'following parameters were used'
-            scenario                                   | xpath                               | jsonData
-            'invalid json data with root node xpath'   | '/'                                 | '{"some-key": "some-value"'
-            'empty json data with root node xpath'     | '/'                                 | '{}'
-            'invalid json data with parent node xpath' | '/bookstore'                        | '{"some-key": "some-value"'
-            'empty json data with parent node xpath'   | '/bookstore'                        | '{}'
-            'empty json data with xpath'               | "/bookstore/categories[@code='02']" | '{}'
-    }
-
     def 'Update data node leaves: #scenario.'() {
         given: 'schema set for given anchor and dataspace references test-tree model'
             setupSchemaSetMocks('test-tree.yang')
@@ -652,6 +573,7 @@ class CpsDataServiceImplSpec extends Specification {
             def logs = loggingListAppender.list.toString()
             assert logs.contains('Failed to send message to notification service')
     }
+
     def setupSchemaSetMocks(String... yangResources) {
         def mockYangTextSchemaSourceSet = Mock(YangTextSchemaSourceSet)
         mockYangTextSchemaSourceSetCache.get(dataspaceName, schemaSetName) >> mockYangTextSchemaSourceSet
@@ -660,12 +582,4 @@ class CpsDataServiceImplSpec extends Specification {
         mockYangTextSchemaSourceSet.getSchemaContext() >> schemaContext
     }
 
-    def setupSchemaSetMocksForDelta(Map<String, String> yangResourceContentPerName) {
-        def mockYangTextSchemaSourceSet = Mock(YangTextSchemaSourceSet)
-        mockTimedYangTextSchemaSourceSetBuilder.getYangTextSchemaSourceSet(yangResourceContentPerName) >> mockYangTextSchemaSourceSet
-        mockYangTextSchemaSourceSetCache.get(_, _) >> mockYangTextSchemaSourceSet
-        def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceContentPerName).getSchemaContext()
-        mockYangTextSchemaSourceSet.getSchemaContext() >> schemaContext
-    }
-
 }
index d3dfcf8..a1bfbb0 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *  ============LICENSE_START=======================================================
- *  Copyright (C) 2023 TechMahindra Ltd.
+ *  Copyright (C) 2023-2025 TechMahindra Ltd.
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
  *  you may not use this file except in compliance with the License.
 
 package org.onap.cps.impl
 
+import ch.qos.logback.classic.Level
+import ch.qos.logback.classic.Logger
+import ch.qos.logback.core.read.ListAppender
+import com.fasterxml.jackson.databind.ObjectMapper
+import org.onap.cps.TestUtils
+import org.onap.cps.api.CpsAnchorService
+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
+import org.onap.cps.utils.PrefixResolver
+import org.onap.cps.utils.YangParser
+import org.onap.cps.utils.YangParserHelper
+import org.onap.cps.yang.TimedYangTextSchemaSourceSetBuilder
+import org.onap.cps.yang.YangTextSchemaSourceSet
+import org.onap.cps.yang.YangTextSchemaSourceSetBuilder
+import org.slf4j.LoggerFactory
+import org.springframework.context.annotation.AnnotationConfigApplicationContext
+import spock.lang.Shared
 import spock.lang.Specification
 
-class CpsDeltaServiceImplSpec extends Specification{
+class CpsDeltaServiceImplSpec extends Specification {
 
-    def objectUnderTest = new CpsDeltaServiceImpl()
+    def mockCpsAnchorService = Mock(CpsAnchorService)
+    def mockCpsDataService = Mock(CpsDataService)
+    def mockYangTextSchemaSourceSetCache = Mock(YangTextSchemaSourceSetCache)
+    def mockTimedYangTextSchemaSourceSetBuilder = Mock(TimedYangTextSchemaSourceSetBuilder)
+    def yangParser = new YangParser(new YangParserHelper(), mockYangTextSchemaSourceSetCache, mockTimedYangTextSchemaSourceSetBuilder)
+    def dataNodeFactory = new DataNodeFactoryImpl(yangParser)
+    def mockPrefixResolver = Mock(PrefixResolver)
+    def dataMapper = new DataMapper(mockCpsAnchorService, mockPrefixResolver)
+    def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
+    def objectUnderTest = new CpsDeltaServiceImpl(mockCpsAnchorService, mockCpsDataService, dataNodeFactory, dataMapper, jsonObjectMapper)
 
+    static def bookstoreDataNodeWithParentXpath = [new DataNode(xpath: '/bookstore', leaves: ['bookstore-name': 'Easons'])]
+    static def bookstoreDataNodeWithChildXpath = [new DataNode(xpath: '/bookstore/categories[@code=\'02\']', leaves: ['code': '02', 'name': 'Kids'])]
+    static def bookstoreDataAsMapForParentNode = [bookstore: ['bookstore-name': 'Easons']]
+    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 sourceDataNodeWithoutLeafData = [new DataNode(xpath: '/parent')]
@@ -35,72 +72,193 @@ class CpsDeltaServiceImplSpec extends Specification{
     static def sourceDataNodeWithMultipleLeaves = [new DataNode(xpath: '/parent', leaves: ['leaf-1': 'leaf-1-in-source', 'leaf-2': 'leaf-2-in-source'])]
     static def targetDataNodeWithMultipleLeaves = [new DataNode(xpath: '/parent', leaves: ['leaf-1': 'leaf-1-in-target', 'leaf-2': 'leaf-2-in-target'])]
 
-    def 'Get delta between data nodes for REMOVED data'() {
-        when: 'attempt to get delta between 2 data nodes'
-            def result = objectUnderTest.getDeltaReports(sourceDataNodeWithLeafData, [])
-        then: 'the delta report contains expected "remove" action'
-            assert result[0].action.equals('remove')
-        and : 'the delta report contains the expected xpath'
-            assert result[0].xpath == '/parent'
-        and: 'the delta report contains expected source data'
-            assert result[0].sourceData == ['parent-leaf': 'parent-payload-in-source']
-        and: 'the delta report contains no target data'
-            assert  result[0].targetData == null
+    def logger = (Logger) LoggerFactory.getLogger(objectUnderTest.class)
+    def loggingListAppender
+    def applicationContext = new AnnotationConfigApplicationContext()
+
+    @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 setup() {
+        mockCpsAnchorService.getAnchor(dataspaceName, ANCHOR_NAME_1) >> anchor1
+        mockCpsAnchorService.getAnchor(dataspaceName, ANCHOR_NAME_2) >> anchor2
+        logger.setLevel(Level.DEBUG)
+        loggingListAppender = new ListAppender()
+        logger.addAppender(loggingListAppender)
+        loggingListAppender.start()
+        applicationContext.refresh()
     }
 
-    def 'Get delta between data nodes for ADDED data'() {
-        when: 'attempt to get delta between 2 data nodes'
-            def result = objectUnderTest.getDeltaReports([], targetDataNodeWithLeafData)
-        then: 'the delta report contains expected "create" action'
-            assert result[0].action.equals('create')
-        and: 'the delta report contains expected xpath'
-            assert result[0].xpath == '/parent'
-        and: 'the delta report contains no source data'
-            assert result[0].sourceData == null
-        and: 'the delta report contains expected target data'
-            assert result[0].targetData == ['parent-leaf': 'parent-payload-in-target']
+    void cleanup() {
+        ((Logger) LoggerFactory.getLogger(CpsDataServiceImpl.class)).detachAndStopAllAppenders()
+        applicationContext.close()
     }
 
-    def 'Delta Report between leaves for parent and child nodes'() {
-        given: 'Two data nodes'
+    def 'Get Delta between 2 anchors for #scenario'() {
+        given: 'xpath to get delta'
+            def xpath = '/'
+        when: 'attempt to get delta between 2 anchors'
+            def deltaReport = objectUnderTest.getDeltaByDataspaceAndAnchors(dataspaceName, ANCHOR_NAME_1, ANCHOR_NAME_2, xpath, INCLUDE_ALL_DESCENDANTS)
+        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 the expected information'
+            deltaReport.size() == 1
+            deltaReport[0].action.equals(expectedAction)
+            deltaReport[0].xpath.equals('/parent')
+            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']
+    }
+
+    def 'Delta Report between parent nodes containing child nodes'() {
+        given: 'Two data nodes and 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 data nodes'
-            def result = objectUnderTest.getDeltaReports(sourceDataNode, targetDataNode)
-        then: 'the delta report contains expected details for parent node'
-            assert result[0].action.equals('replace')
-            assert result[0].xpath == '/parent'
-            assert result[0].sourceData == ['parent-leaf': 'parent-payload']
-            assert result[0].targetData == ['parent-leaf': 'parent-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)
+        then: 'cps data service is invoked and returns source data nodes'
+            mockCpsDataService.getDataNodesForMultipleXpaths(dataspaceName, ANCHOR_NAME_1, [xpath], INCLUDE_ALL_DESCENDANTS) >> sourceDataNode
+        and: 'cps data service is invoked again to return target data nodes'
+            mockCpsDataService.getDataNodesForMultipleXpaths(dataspaceName, ANCHOR_NAME_2, [xpath], INCLUDE_ALL_DESCENDANTS) >> targetDataNode
+        and: 'the delta report contains expected details for parent node'
+            assert deltaReport[0].action.equals('replace')
+            assert deltaReport[0].xpath == '/parent'
+            assert deltaReport[0].sourceData == ['parent-leaf': 'parent-payload']
+            assert deltaReport[0].targetData == ['parent-leaf': 'parent-payload-updated']
         and: 'the delta report contains expected details for child node'
-            assert result[1].action.equals('replace')
-            assert result[1].xpath == '/parent/child'
-            assert result[1].sourceData == ['child-leaf': 'child-payload']
-            assert result[1].targetData == ['child-leaf': 'child-payload-updated']
+            assert deltaReport[1].action.equals('replace')
+            assert deltaReport[1].xpath == '/parent/child'
+            assert deltaReport[1].sourceData == ['child-leaf': 'child-payload']
+            assert deltaReport[1].targetData == ['child-leaf': 'child-payload-updated']
     }
 
     def 'Delta report between leaves, #scenario'() {
-        when: 'attempt to get delta between 2 data nodes'
-            def result = objectUnderTest.getDeltaReports(sourceDataNode, targetDataNode)
-        then: 'the delta report contains expected "replace" action'
-            assert result[0].action.equals('replace')
-        and: 'the delta report contains expected xpath'
-            assert result[0].xpath == '/parent'
-        and: 'the delta report contains expected source and target data'
-            assert result[0].sourceData == expectedSourceData
-            assert result[0].targetData == expectedTargetData
-        where: 'the following data was used'
-            scenario                                           | sourceDataNode                   | targetDataNode                   || expectedSourceData                                           | expectedTargetData
-            'source and target data nodes have leaves'         | sourceDataNodeWithLeafData       | targetDataNodeWithLeafData       || ['parent-leaf': 'parent-payload-in-source']                  | ['parent-leaf': 'parent-payload-in-target']
-            'only source data node has leaves'                 | sourceDataNodeWithLeafData       | targetDataNodeWithoutLeafData    || ['parent-leaf': 'parent-payload-in-source']                  | null
-            'only target data node has leaves'                 | sourceDataNodeWithoutLeafData    | targetDataNodeWithLeafData       || null                                                         | ['parent-leaf': 'parent-payload-in-target']
-            'source and target dsta node with multiple leaves' | sourceDataNodeWithMultipleLeaves | targetDataNodeWithMultipleLeaves || ['leaf-1': 'leaf-1-in-source', 'leaf-2': 'leaf-2-in-source'] | ['leaf-1': 'leaf-1-in-target', 'leaf-2': 'leaf-2-in-target']
+    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)
+    then: 'cps data service is invoked and returns source data nodes'
+        mockCpsDataService.getDataNodesForMultipleXpaths(dataspaceName, ANCHOR_NAME_1, [xpath], INCLUDE_ALL_DESCENDANTS) >> sourceDataNode
+    and: 'cps data service is invoked again to return target data nodes'
+        mockCpsDataService.getDataNodesForMultipleXpaths(dataspaceName, ANCHOR_NAME_2, [xpath], INCLUDE_ALL_DESCENDANTS) >> targetDataNode
+    and: 'the delta report contains expected "replace" action'
+        assert deltaReport[0].action.equals('replace')
+    and: 'the delta report contains expected xpath'
+        assert deltaReport[0].xpath == '/parent'
+    and: 'the delta report contains expected source and target data'
+        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']
     }
 
     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 result = objectUnderTest.getDeltaReports(sourceDataNodeWithoutLeafData, targetDataNodeWithoutLeafData)
+            def deltaReport = objectUnderTest.getDeltaByDataspaceAndAnchors(dataspaceName, ANCHOR_NAME_1, ANCHOR_NAME_2, xpath, INCLUDE_ALL_DESCENDANTS)
+        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
         then: 'the delta report is empty'
-            assert result.isEmpty()
+            assert deltaReport.isEmpty()
+    }
+
+    def 'Get delta between anchor and payload with user provided schema #scenario'() {
+        given: 'user provided schema set '
+            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)
+        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)'
+            dataNodeFactory.createDataNodesWithAnchorXpathAndNodeData(anchor1, xpath, jsonObjectMapper.asJsonString(sourceDataNodesAsMap), ContentType.JSON)
+        and: 'data node factory method is invoked to build target data nodes using user provided schema'
+            dataNodeFactory.createDataNodesWithYangResourceXpathAndNodeData(yangResourceContentPerName, xpath, jsonData, ContentType.JSON)
+        and: 'delta report contains expected xpath, action, source and target data'
+            deltaReport[0].getXpath() == expectedNodeXpath
+            deltaReport[0].getAction().equals('replace')
+            deltaReport[0].getSourceData().equals(expectedSourceData)
+            deltaReport[0].getTargetData().equals(expectedTargetData)
+        where: 'following data was used'
+            scenario          | xpath                                 | sourceDataNodes                  | sourceDataNodesAsMap            | jsonData                   || expectedNodeXpath                    | expectedSourceData          | expectedTargetData
+            'root node xpath' | '/'                                   | bookstoreDataNodeWithParentXpath | bookstoreDataAsMapForParentNode | bookstoreJsonForParentNode || '/bookstore'                         | ['bookstore-name':'Easons'] | ['bookstore-name':'My Store']
+            'parent xpath'    | '/bookstore'                          | bookstoreDataNodeWithParentXpath | bookstoreDataAsMapForParentNode | bookstoreJsonForParentNode || '/bookstore'                         | ['bookstore-name':'Easons'] | ['bookstore-name':'My Store']
+            'non-root xpath'  | '/bookstore/categories[@code=\'02\']' | bookstoreDataNodeWithChildXpath  | bookstoreDataAsMapForChildNode  | bookstoreJsonForChildNode  || '/bookstore/categories[@code=\'02\']'| ['name':'Kids']             | ['name':'Child']
+    }
+
+    def 'Get delta between anchor and payload by using schema from anchor #scenario'() {
+        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)
+        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)'
+            dataNodeFactory.createDataNodesWithAnchorXpathAndNodeData(anchor1, xpath, jsonObjectMapper.asJsonString(sourceDataNodesAsMap), ContentType.JSON)
+        and: 'data node factory method is invoked to build target data nodes using schema details fetched from anchor name'
+            dataNodeFactory.createDataNodesWithAnchorXpathAndNodeData(anchor1, xpath, jsonData, ContentType.JSON)
+        and: 'delta report contains expected xpath, action, source and target data'
+            deltaReport[0].getXpath() == expectedNodeXpath
+            deltaReport[0].getAction().equals('replace')
+            deltaReport[0].getSourceData().equals(expectedSourceData)
+            deltaReport[0].getTargetData().equals(expectedTargetData)
+        where: 'following data was used'
+             scenario         | xpath                                 | sourceDataNodes                  | sourceDataNodesAsMap            | jsonData                   || expectedNodeXpath                     | expectedSourceData          | expectedTargetData
+            'root node xpath' | '/'                                   | bookstoreDataNodeWithParentXpath | bookstoreDataAsMapForParentNode | bookstoreJsonForParentNode || '/bookstore'                          | ['bookstore-name':'Easons'] | ['bookstore-name':'My Store']
+            'parent xpath'    | '/bookstore'                          | bookstoreDataNodeWithParentXpath | bookstoreDataAsMapForParentNode | bookstoreJsonForParentNode || '/bookstore'                          | ['bookstore-name':'Easons'] | ['bookstore-name':'My Store']
+            'non-root xpath'  | '/bookstore/categories[@code=\'02\']' | bookstoreDataNodeWithChildXpath  | bookstoreDataAsMapForChildNode  | bookstoreJsonForChildNode  || '/bookstore/categories[@code=\'02\']' | ['name':'Kids']             | ['name':'Child']
+    }
+
+    def 'Delta between anchor and payload error scenario #scenario'() {
+        given: 'schema set for given anchor and dataspace references bookstore model'
+            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)
+        then: 'expected exception is thrown'
+            thrown(DataValidationException)
+        where: 'following parameters were used'
+            scenario                                   | xpath                                 | jsonData
+            'invalid json data with root node xpath'   | '/'                                   | '{"some-key": "some-value"'
+            'empty json data with root node xpath'     | '/'                                   | '{}'
+            'invalid json data with parent node xpath' | '/bookstore'                          | '{"some-key": "some-value"'
+            'empty json data with parent node xpath'   | '/bookstore'                          | '{}'
+            'empty json data with xpath'               | '/bookstore/categories[@code=\'02\']' | '{}'
+    }
+
+    def setupSchemaSetMocks(String... yangResources) {
+        def mockYangTextSchemaSourceSet = Mock(YangTextSchemaSourceSet)
+        mockYangTextSchemaSourceSetCache.get(dataspaceName, schemaSetName) >> mockYangTextSchemaSourceSet
+        def yangResourceNameToContent = TestUtils.getYangResourcesAsMap(yangResources)
+        def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext()
+        mockYangTextSchemaSourceSet.getSchemaContext() >> schemaContext
+    }
+
+    def setupSchemaSetMocksForDelta(Map<String, String> yangResourceContentPerName) {
+        def mockYangTextSchemaSourceSet = Mock(YangTextSchemaSourceSet)
+        mockTimedYangTextSchemaSourceSetBuilder.getYangTextSchemaSourceSet(yangResourceContentPerName) >> mockYangTextSchemaSourceSet
+        mockYangTextSchemaSourceSetCache.get(_, _) >> mockYangTextSchemaSourceSet
+        def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceContentPerName).getSchemaContext()
+        mockYangTextSchemaSourceSet.getSchemaContext() >> schemaContext
     }
 }
index c9c1991..4713283 100755 (executable)
 
 package org.onap.cps.impl
 
-import com.fasterxml.jackson.databind.ObjectMapper
 import org.onap.cps.TestUtils
 import org.onap.cps.api.CpsAnchorService
-import org.onap.cps.api.CpsDeltaService
 import org.onap.cps.api.model.Anchor
 import org.onap.cps.events.CpsDataUpdateEventsProducer
 import org.onap.cps.spi.CpsDataPersistenceService
 import org.onap.cps.spi.CpsModulePersistenceService
 import org.onap.cps.utils.ContentType
 import org.onap.cps.utils.CpsValidator
-import org.onap.cps.utils.DataMapper
-import org.onap.cps.utils.JsonObjectMapper
-import org.onap.cps.utils.PrefixResolver
 import org.onap.cps.utils.YangParser
 import org.onap.cps.utils.YangParserHelper
 import org.onap.cps.yang.TimedYangTextSchemaSourceSetBuilder
@@ -50,15 +45,10 @@ class E2ENetworkSliceSpec extends Specification {
     def mockCpsValidator = Mock(CpsValidator)
     def timedYangTextSchemaSourceSetBuilder = new TimedYangTextSchemaSourceSetBuilder()
     def yangParser = new YangParser(new YangParserHelper(), mockYangTextSchemaSourceSetCache, timedYangTextSchemaSourceSetBuilder)
-    def mockCpsDeltaService = Mock(CpsDeltaService)
-    def dataMapper = new DataMapper(mockCpsAnchorService, Mock(PrefixResolver))
-    def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
-
     def cpsModuleServiceImpl = new CpsModuleServiceImpl(mockCpsModulePersistenceService, mockYangTextSchemaSourceSetCache, mockCpsAnchorService, mockCpsValidator,timedYangTextSchemaSourceSetBuilder)
-
     def mockCpsDataUpdateEventsProducer = Mock(CpsDataUpdateEventsProducer)
     def dataNodeFactory = new DataNodeFactoryImpl(yangParser)
-    def cpsDataServiceImpl = new CpsDataServiceImpl(mockCpsDataPersistenceService, mockCpsDataUpdateEventsProducer, mockCpsAnchorService, dataNodeFactory, mockCpsValidator, yangParser, mockCpsDeltaService, dataMapper, jsonObjectMapper)
+    def cpsDataServiceImpl = new CpsDataServiceImpl(mockCpsDataPersistenceService, mockCpsDataUpdateEventsProducer, mockCpsAnchorService, dataNodeFactory, mockCpsValidator, yangParser)
     def dataspaceName = 'someDataspace'
     def anchorName = 'someAnchor'
     def schemaSetName = 'someSchemaSet'
index bb69f2f..b432823 100644 (file)
@@ -28,6 +28,7 @@ import okhttp3.mockwebserver.MockWebServer
 import org.onap.cps.api.CpsAnchorService
 import org.onap.cps.api.CpsDataService
 import org.onap.cps.api.CpsDataspaceService
+import org.onap.cps.api.CpsDeltaService
 import org.onap.cps.api.CpsModuleService
 import org.onap.cps.api.CpsQueryService
 import org.onap.cps.api.exceptions.DataspaceNotFoundException
@@ -92,6 +93,9 @@ abstract class CpsIntegrationSpecBase extends Specification {
     @Autowired
     CpsDataService cpsDataService
 
+    @Autowired
+    CpsDeltaService cpsDeltaService
+
     @Autowired
     CpsModuleService cpsModuleService
 
index 4823d58..6ecc3a5 100644 (file)
@@ -26,13 +26,11 @@ import org.onap.cps.integration.base.FunctionalSpecBase
 import org.onap.cps.api.parameters.FetchDescendantsOption
 import org.onap.cps.api.exceptions.AlreadyDefinedException
 import org.onap.cps.api.exceptions.AnchorNotFoundException
-import org.onap.cps.api.exceptions.CpsAdminException
 import org.onap.cps.api.exceptions.CpsPathException
 import org.onap.cps.api.exceptions.DataNodeNotFoundException
 import org.onap.cps.api.exceptions.DataNodeNotFoundExceptionBatch
 import org.onap.cps.api.exceptions.DataValidationException
 import org.onap.cps.api.exceptions.DataspaceNotFoundException
-import org.onap.cps.api.model.DeltaReport
 import org.onap.cps.utils.ContentType
 
 import static org.onap.cps.api.parameters.FetchDescendantsOption.DIRECT_CHILDREN_ONLY
@@ -523,191 +521,6 @@ class DataServiceIntegrationSpec extends FunctionalSpecBase {
             restoreBookstoreDataAnchor(2)
     }
 
-    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)
-        and: 'report is ordered based on xpath'
-            result = result.toList().sort { it.xpath }
-        then: 'delta report contains expected number of changes'
-            result.size() == 3
-        and: 'delta report contains REPLACE action with expected xpath'
-            assert result[0].getAction() == 'replace'
-            assert result[0].getXpath() == '/bookstore'
-        and: 'delta report contains CREATE action with expected xpath'
-            assert result[1].getAction() == 'create'
-            assert result[1].getXpath() == "/bookstore-address[@bookstore-name='Crossword Bookstores']"
-        and: 'delta report contains REMOVE action with expected xpath'
-            assert result[2].getAction() == 'remove'
-            assert result[2].getXpath() == "/bookstore-address[@bookstore-name='Easons-1']"
-    }
-
-    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)
-        then: 'delta report is empty'
-            assert result.isEmpty()
-        where: 'following data was used'
-            scenario                              | targetAnchor       | xpath
-        'anchors with identical data are queried' | BOOKSTORE_ANCHOR_4 | '/'
-        'same anchor name is passed as parameter' | BOOKSTORE_ANCHOR_3 | '/'
-        'non existing xpath'                      | BOOKSTORE_ANCHOR_5 | '/non-existing-xpath'
-    }
-
-    def 'Get delta between anchors error scenario: #scenario'() {
-        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 "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)
-        then: 'the expected action is present in delta report'
-            result.get(0).getAction() == 'create'
-        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 'Get delta between anchors when leaves of existing data nodes are updated,: #scenario'() {
-        when: 'attempt to get delta between leaves of existing data nodes'
-            def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, sourceAnchor, targetAnchor, xpath, OMIT_DESCENDANTS)
-        then: 'expected action is "replace"'
-            assert result[0].getAction() == 'replace'
-        and: 'the payload has expected leaf values'
-            def sourceData = result[0].getSourceData()
-            def targetData = result[0].getTargetData()
-            assert sourceData == expectedSourceValue
-            assert targetData == expectedTargetValue
-        where: 'following data was used'
-            scenario                           | sourceAnchor       | targetAnchor       | xpath                                                     || expectedSourceValue            | expectedTargetValue
-            'leaf is updated in target anchor' | BOOKSTORE_ANCHOR_3 | BOOKSTORE_ANCHOR_5 | '/bookstore'                                              || ['bookstore-name': 'Easons-1'] | ['bookstore-name': 'Crossword Bookstores']
-            'leaf is removed in target anchor' | BOOKSTORE_ANCHOR_3 | BOOKSTORE_ANCHOR_5 | "/bookstore/categories[@code='5']/books[@title='Book 1']" || [price:1]                      | null
-            'leaf is added in target anchor'   | BOOKSTORE_ANCHOR_5 | BOOKSTORE_ANCHOR_3 | "/bookstore/categories[@code='5']/books[@title='Book 1']" || null                           | [price:1]
-    }
-
-    def 'Get delta between anchors when child data nodes under existing parent data nodes are updated: #scenario'() {
-        when: 'attempt to get delta between leaves of existing data nodes'
-            def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, sourceAnchor, targetAnchor, xpath, DIRECT_CHILDREN_ONLY)
-        then: 'expected action is "replace"'
-            assert result[0].getAction() == 'replace'
-        and: 'the delta report has expected child node xpaths'
-            def deltaReportEntities = getDeltaReportEntities(result)
-            def childNodeXpathsInDeltaReport = deltaReportEntities.get('xpaths')
-            assert childNodeXpathsInDeltaReport.contains(expectedChildNodeXpath)
-        where: 'following data was used'
-            scenario                                          | sourceAnchor       | targetAnchor       | xpath                 || expectedChildNodeXpath
-            'source and target anchors have child data nodes' | BOOKSTORE_ANCHOR_3 | BOOKSTORE_ANCHOR_5 | '/bookstore/premises' || '/bookstore/premises/addresses[@house-number=\'2\' and @street=\'Main Street\']'
-            'removed child data nodes in target anchor'       | BOOKSTORE_ANCHOR_5 | BOOKSTORE_ANCHOR_3 | '/bookstore'          || '/bookstore/support-info'
-            'added  child data nodes in target anchor'        | BOOKSTORE_ANCHOR_3 | BOOKSTORE_ANCHOR_5 | '/bookstore'          || '/bookstore/support-info'
-    }
-
-    def 'Get delta between anchors where source and target data nodes have leaves and child data nodes'() {
-        given: 'parent node xpath and expected data in delta report'
-            def parentNodeXpath = "/bookstore/categories[@code='1']"
-            def expectedSourceDataInParentNode = ['name':'Children']
-            def expectedTargetDataInParentNode = ['name':'Kids']
-            def expectedSourceDataInChildNode = [['lang' : 'English'],['price':20, 'editions':[1988, 2000]]]
-            def expectedTargetDataInChildNode = [['lang':'English/German'], ['price':200, 'editions':[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 deltaReportEntities = getDeltaReportEntities(result)
-        then: 'expected action is "replace"'
-            assert result[0].getAction() == 'replace'
-        and: 'the payload has expected parent node xpath'
-            assert deltaReportEntities.get('xpaths').contains(parentNodeXpath)
-        and: 'delta report has expected source and target data'
-            assert deltaReportEntities.get('sourcePayload').contains(expectedSourceDataInParentNode)
-            assert deltaReportEntities.get('targetPayload').contains(expectedTargetDataInParentNode)
-        and: 'the delta report also has expected child node xpaths'
-            assert deltaReportEntities.get('xpaths').containsAll(["/bookstore/categories[@code='1']/books[@title='The Gruffalo']", "/bookstore/categories[@code='1']/books[@title='Matilda']"])
-        and: 'the delta report also has expected source and target data of child nodes'
-            assert deltaReportEntities.get('sourcePayload').containsAll(expectedSourceDataInChildNode)
-            assert deltaReportEntities.get('targetPayload').containsAll(expectedTargetDataInChildNode)
-    }
-
-    def '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)
-        then: 'delta report contains expected number of changes'
-            result.size() == 3
-        and: 'delta report contains "replace" action with expected xpath'
-            assert result[0].getAction() == 'replace'
-            assert result[0].getXpath() == '/bookstore'
-        and: 'delta report contains "remove" action with expected xpath'
-            assert result[1].getAction() == 'remove'
-            assert result[1].getXpath() == "/bookstore-address[@bookstore-name='Easons-1']"
-        and: 'delta report contains "create" action with expected xpath'
-            assert result[2].getAction() == 'create'
-            assert result[2].getXpath() == "/bookstore-address[@bookstore-name='Crossword Bookstores']"
-    }
-
-    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)
-        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)
-        then: 'expected exception is thrown'
-            thrown(expectedException)
-        where: 'following data was used'
-                scenario                               | dataspaceName               | sourceAnchor          | xpath        | jsonPayload   || expectedException
-        'invalid dataspace name'                       | 'Invalid dataspace'         | 'not-relevant'        | '/'          | '{some-json}' || DataValidationException
-        'invalid anchor name'                          | FUNCTIONAL_TEST_DATASPACE_3 | 'invalid anchor'      | '/'          | '{some-json}' || DataValidationException
-        'non-existing dataspace'                       | 'non-existing'              | 'not-relevant'        | '/'          | '{some-json}' || DataspaceNotFoundException
-        'non-existing anchor'                          | FUNCTIONAL_TEST_DATASPACE_3 | 'non-existing-anchor' | '/'          | '{some-json}' || AnchorNotFoundException
-        'empty json payload with root node xpath'      | FUNCTIONAL_TEST_DATASPACE_3 | BOOKSTORE_ANCHOR_3    | '/'          | ''            || DataValidationException
-        'empty json payload with non-root node xpath'  | FUNCTIONAL_TEST_DATASPACE_3 | BOOKSTORE_ANCHOR_3    | '/bookstore' | ''            || DataValidationException
-    }
-
-    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/groovy/org/onap/cps/integration/functional/cps/DeltaServiceIntegrationSpec.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/functional/cps/DeltaServiceIntegrationSpec.groovy
new file mode 100644 (file)
index 0000000..691e714
--- /dev/null
@@ -0,0 +1,245 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2025 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.integration.functional.cps
+
+import org.onap.cps.api.CpsDeltaService
+import org.onap.cps.api.exceptions.AnchorNotFoundException
+import org.onap.cps.api.exceptions.DataValidationException
+import org.onap.cps.api.exceptions.DataspaceNotFoundException
+import org.onap.cps.api.model.DeltaReport
+import org.onap.cps.api.parameters.FetchDescendantsOption
+import org.onap.cps.integration.base.FunctionalSpecBase
+
+class DeltaServiceIntegrationSpec extends FunctionalSpecBase {
+    CpsDeltaService objectUnderTest
+    def originalCountBookstoreChildNodes
+    def originalCountXmlBookstoreChildNodes
+    def originalCountBookstoreTopLevelListNodes
+
+    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 setup() {
+        objectUnderTest = cpsDeltaService
+        originalCountBookstoreChildNodes = countDataNodesInBookstore()
+        originalCountBookstoreTopLevelListNodes = countTopLevelListDataNodesInBookstore()
+        originalCountXmlBookstoreChildNodes = countXmlDataNodesInBookstore()
+    }
+
+    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)
+        and: 'report is ordered based on xpath'
+            result = result.toList().sort { it.xpath }
+        then: 'delta report contains expected number of changes'
+            result.size() == 3
+        and: 'delta report contains REPLACE action with expected xpath'
+            assert result[0].getAction() == 'replace'
+            assert result[0].getXpath() == '/bookstore'
+        and: 'delta report contains CREATE action with expected xpath'
+            assert result[1].getAction() == 'create'
+            assert result[1].getXpath() == '/bookstore-address[@bookstore-name=\'Crossword Bookstores\']'
+        and: 'delta report contains REMOVE action with expected xpath'
+            assert result[2].getAction() == 'remove'
+            assert result[2].getXpath() == '/bookstore-address[@bookstore-name=\'Easons-1\']'
+    }
+
+    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)
+        then: 'delta report is empty'
+            assert result.isEmpty()
+        where: 'following data was used'
+            scenario                              | targetAnchor       | xpath
+        'anchors with identical data are queried' | BOOKSTORE_ANCHOR_4 | '/'
+        'same anchor name is passed as parameter' | BOOKSTORE_ANCHOR_3 | '/'
+        'non existing xpath'                      | BOOKSTORE_ANCHOR_5 | '/non-existing-xpath'
+    }
+
+    def 'Get delta between anchors error scenario: #scenario'() {
+        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 "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)
+        then: 'the expected action is present in delta report'
+            result.get(0).getAction() == 'create'
+        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 'Get delta between anchors when leaves of existing data nodes are updated,: #scenario'() {
+        when: 'attempt to get delta between leaves of existing data nodes'
+            def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, sourceAnchor, targetAnchor, xpath, OMIT_DESCENDANTS)
+        then: 'expected action is "replace"'
+            assert result[0].getAction() == 'replace'
+        and: 'the payload has expected leaf values'
+            def sourceData = result[0].getSourceData()
+            def targetData = result[0].getTargetData()
+            assert sourceData == expectedSourceValue
+            assert targetData == expectedTargetValue
+        where: 'following data was used'
+            scenario                           | sourceAnchor       | targetAnchor       | xpath                                                         || expectedSourceValue            | expectedTargetValue
+            'leaf is updated in target anchor' | BOOKSTORE_ANCHOR_3 | BOOKSTORE_ANCHOR_5 | '/bookstore'                                                  || ['bookstore-name': 'Easons-1'] | ['bookstore-name': 'Crossword Bookstores']
+            'leaf is removed in target anchor' | BOOKSTORE_ANCHOR_3 | BOOKSTORE_ANCHOR_5 | '/bookstore/categories[@code=\'5\']/books[@title=\'Book 1\']' || [price:1]                      | null
+            'leaf is added in target anchor'   | BOOKSTORE_ANCHOR_5 | BOOKSTORE_ANCHOR_3 | '/bookstore/categories[@code=\'5\']/books[@title=\'Book 1\']' || null                           | [price:1]
+    }
+
+    def 'Get delta between anchors when child data nodes under existing parent data nodes are updated: #scenario'() {
+        when: 'attempt to get delta between leaves of existing data nodes'
+            def result = objectUnderTest.getDeltaByDataspaceAndAnchors(FUNCTIONAL_TEST_DATASPACE_3, sourceAnchor, targetAnchor, xpath, DIRECT_CHILDREN_ONLY)
+        then: 'expected action is "replace"'
+            assert result[0].getAction() == 'replace'
+        and: 'the delta report has expected child node xpaths'
+            def deltaReportEntities = getDeltaReportEntities(result)
+            def childNodeXpathsInDeltaReport = deltaReportEntities.get('xpaths')
+            assert childNodeXpathsInDeltaReport.contains(expectedChildNodeXpath)
+        where: 'following data was used'
+            scenario                                          | sourceAnchor       | targetAnchor       | xpath                 || expectedChildNodeXpath
+            'source and target anchors have child data nodes' | BOOKSTORE_ANCHOR_3 | BOOKSTORE_ANCHOR_5 | '/bookstore/premises' || '/bookstore/premises/addresses[@house-number=\'2\' and @street=\'Main Street\']'
+            'removed child data nodes in target anchor'       | BOOKSTORE_ANCHOR_5 | BOOKSTORE_ANCHOR_3 | '/bookstore'          || '/bookstore/support-info'
+            'added  child data nodes in target anchor'        | BOOKSTORE_ANCHOR_3 | BOOKSTORE_ANCHOR_5 | '/bookstore'          || '/bookstore/support-info'
+    }
+
+    def 'Get delta between anchors where source and target data nodes have leaves and child data nodes'() {
+        given: 'parent node xpath and expected data in delta report'
+            def parentNodeXpath = '/bookstore/categories[@code=\'1\']'
+            def expectedSourceDataInParentNode = ['name':'Children']
+            def expectedTargetDataInParentNode = ['name':'Kids']
+            def expectedSourceDataInChildNode = [['lang' : 'English'],['price':20, 'editions':[1988, 2000]]]
+            def expectedTargetDataInChildNode = [['lang':'English/German'], ['price':200, 'editions':[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 deltaReportEntities = getDeltaReportEntities(result)
+        then: 'expected action is "replace"'
+            assert result[0].getAction() == 'replace'
+        and: 'the payload has expected parent node xpath'
+            assert deltaReportEntities.get('xpaths').contains(parentNodeXpath)
+        and: 'delta report has expected source and target data'
+            assert deltaReportEntities.get('sourcePayload').contains(expectedSourceDataInParentNode)
+            assert deltaReportEntities.get('targetPayload').contains(expectedTargetDataInParentNode)
+        and: 'the delta report also has expected child node xpaths'
+            assert deltaReportEntities.get('xpaths').containsAll(['/bookstore/categories[@code=\'1\']/books[@title=\'The Gruffalo\']', '/bookstore/categories[@code=\'1\']/books[@title=\'Matilda\']'])
+        and: 'the delta report also has expected source and target data of child nodes'
+            assert deltaReportEntities.get('sourcePayload').containsAll(expectedSourceDataInChildNode)
+            assert deltaReportEntities.get('targetPayload').containsAll(expectedTargetDataInChildNode)
+    }
+
+    def '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)
+        then: 'delta report contains expected number of changes'
+            result.size() == 3
+        and: 'delta report contains "replace" action with expected xpath'
+            assert result[0].getAction() == 'replace'
+            assert result[0].getXpath() == '/bookstore'
+        and: 'delta report contains "remove" action with expected xpath'
+            assert result[1].getAction() == 'remove'
+            assert result[1].getXpath() == '/bookstore-address[@bookstore-name=\'Easons-1\']'
+        and: 'delta report contains "create" action with expected xpath'
+            assert result[2].getAction() == 'create'
+            assert result[2].getXpath() == '/bookstore-address[@bookstore-name=\'Crossword Bookstores\']'
+    }
+
+    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)
+        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)
+        then: 'expected exception is thrown'
+            thrown(expectedException)
+        where: 'following data was used'
+                scenario                               | dataspaceName               | sourceAnchor          | xpath        | jsonPayload   || expectedException
+        'invalid dataspace name'                       | 'Invalid dataspace'         | 'not-relevant'        | '/'          | '{some-json}' || DataValidationException
+        'invalid anchor name'                          | FUNCTIONAL_TEST_DATASPACE_3 | 'invalid anchor'      | '/'          | '{some-json}' || DataValidationException
+        'non-existing dataspace'                       | 'non-existing'              | 'not-relevant'        | '/'          | '{some-json}' || DataspaceNotFoundException
+        'non-existing anchor'                          | FUNCTIONAL_TEST_DATASPACE_3 | 'non-existing-anchor' | '/'          | '{some-json}' || AnchorNotFoundException
+        'empty json payload with root node xpath'      | FUNCTIONAL_TEST_DATASPACE_3 | BOOKSTORE_ANCHOR_3    | '/'          | ''            || DataValidationException
+        'empty json payload with non-root node xpath'  | FUNCTIONAL_TEST_DATASPACE_3 | BOOKSTORE_ANCHOR_3    | '/bookstore' | ''            || DataValidationException
+    }
+
+    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(cpsDataService.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/bookstore', INCLUDE_ALL_DESCENDANTS))
+    }
+
+    def countTopLevelListDataNodesInBookstore() {
+        return countDataNodesInTree(cpsDataService.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/', INCLUDE_ALL_DESCENDANTS))
+    }
+
+    def countXmlDataNodesInBookstore() {
+        return countDataNodesInTree(cpsDataService.getDataNodes(FUNCTIONAL_TEST_DATASPACE_4, BOOKSTORE_ANCHOR_6, '/bookstore', INCLUDE_ALL_DESCENDANTS))
+    }
+
+}