# ============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.
'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
--- /dev/null
+# ============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'
description: cps Admin
- name: cps-data
description: cps Data
+ - name: cps-delta
+ description: CPS Delta
paths:
/v1/dataspaces:
$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'
* 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;
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}")
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;
--- /dev/null
+/*
+ * ============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);
+ }
+
+}
* 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.
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
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
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
@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"
'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"
--- /dev/null
+/*
+ * ============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\"}]')
+ }
+}
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
@SpringBean
CpsNotificationService mockCpsNotificationService = Stub()
+ @SpringBean
+ CpsDeltaService cpsDeltaService = Stub()
+
@Autowired
MockMvc mvc
* 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.
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;
*/
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.
/*
* ============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);
+
+
}
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;
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;
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,
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);
}
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")
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()));
/*
* ============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 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;
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<>();
final Map<String, DataNode> xpathToTargetDataNodes = convertToXPathToDataNodesMap(targetDataNodes);
deltaReport.addAll(getRemovedAndUpdatedDeltaReports(xpathToSourceDataNodes, xpathToTargetDataNodes));
-
deltaReport.addAll(getAddedDeltaReports(xpathToSourceDataNodes, xpathToTargetDataNodes));
return Collections.unmodifiableList(deltaReport);
}
} else if (sourceLeaf == null) {
targetDataInDeltaReport.put(key, targetLeaf);
-
} else {
sourceDataInDeltaReport.put(key, sourceLeaf);
}
}
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);
+ }
+ }
}
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
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
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
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')
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
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
- }
-
}
/*
* ============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')]
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
}
}
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
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'
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
@Autowired
CpsDataService cpsDataService
+ @Autowired
+ CpsDeltaService cpsDeltaService
+
@Autowired
CpsModuleService cpsModuleService
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
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))
}
--- /dev/null
+/*
+ * ============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))
+ }
+
+}