- Feature that uses the delta report to perform REST operations.
- The code uses underlying CPS data java interface logic to perform the
REST operations.
- The REST operations are executed based on the 'action' identified in the
delta report.
- The supported operations are: create, delete and update.
- Fixed indentation error in CpsDataServiceImpl
Issue-ID: CPS-2614
Change-Id: Ic1c9f5d2320afd8e4fb49b638e6636291124ea75
Signed-off-by: Arpit Singh <AS00745003@techmahindra.com>
$ref: 'components.yml#/components/responses/Forbidden'
'500':
$ref: 'components.yml#/components/responses/InternalServerError'
+
+applyChangesInDeltaReport:
+ post:
+ description: Use the delta report to perform batch operations on an anchor in a dataspace.
+ tags:
+ - cps-delta
+ summary: Apply delta to an anchor
+ operationId: applyChangesInDeltaReport
+ parameters:
+ - $ref: 'components.yml#/components/parameters/dataspaceNameInPath'
+ - $ref: 'components.yml#/components/parameters/sourceAnchorNameInPath'
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: string
+ examples:
+ dataSample:
+ $ref: 'components.yml#/components/examples/deltaReportSample'
+ responses:
+ '201':
+ $ref: 'components.yml#/components/responses/Created'
+ '400':
+ $ref: 'components.yml#/components/responses/BadRequest'
+ '403':
+ $ref: 'components.yml#/components/responses/Forbidden'
+ '500':
+ $ref: 'components.yml#/components/responses/InternalServerError'
/v2/dataspaces/{dataspace-name}/anchors/{source-anchor-name}/delta:
$ref: 'cpsDelta.yml#/delta'
+ /v2/dataspaces/{dataspace-name}/anchors/{source-anchor-name}/applyChangesInDeltaReport:
+ $ref: 'cpsDelta.yml#/applyChangesInDeltaReport'
+
/v1/dataspaces/{dataspace-name}/anchors/{anchor-name}/nodes/query:
$ref: 'cpsQueryV1Deprecated.yml#/nodesByDataspaceAndAnchorAndCpsPath'
private final CpsDeltaService cpsDeltaService;
private final JsonObjectMapper jsonObjectMapper;
-
@Timed(value = "cps.delta.controller.get.delta",
description = "Time taken to get delta between anchors")
@Override
xpath, yangResourceMap, targetData, fetchDescendantsOption, groupDataNodes));
return new ResponseEntity<>(jsonObjectMapper.asJsonString(deltaReports), HttpStatus.OK);
}
+
+ public ResponseEntity<String> applyChangesInDeltaReport(final String dataspaceName,
+ final String anchorName,
+ final String deltaReport) {
+ cpsDeltaService.applyChangesInDeltaReport(dataspaceName, anchorName, deltaReport);
+ return ResponseEntity.status(HttpStatus.CREATED).build();
+ }
+
}
import org.springframework.web.bind.annotation.RestControllerAdvice;
@Slf4j
-@RestControllerAdvice(assignableTypes = {AdminRestController.class, DataRestController.class,
+@RestControllerAdvice(assignableTypes = {AdminRestController.class, DataRestController.class, DeltaRestController.class,
DeltaRestController.class, QueryRestController.class})
@NoArgsConstructor(access = AccessLevel.PACKAGE)
public class CpsRestExceptionHandler {
import org.springframework.web.multipart.MultipartFile
import spock.lang.Shared
import spock.lang.Specification
+
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.nio.file.Path
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
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
@WebMvcTest(DeltaRestController)
class DeltaRestControllerSpec extends Specification {
then: 'the response contains expected error message'
assert response.contentAsString.contains("Parsing error occurred while converting JSON content to Json Node")
}
+
+ def 'Apply changes from a delta report, in JSON format, on an anchor'() {
+ given: 'sample delta report, xpath, and json payload'
+ def deltaReports = 'some delta report'
+ def applyDeltaEndpointV2 = "$basePath/v2/dataspaces/$dataspaceName/anchors/$anchorName/applyChangesInDeltaReport"
+ when: 'request to apply delta report to an anchor is performed using REST API'
+ def response =
+ mvc.perform(post(applyDeltaEndpointV2)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(deltaReports)
+ ).andReturn().response
+ then: 'expected response code is returned'
+ assert response.status == HttpStatus.CREATED.value()
+ }
}
FetchDescendantsOption fetchDescendantsOption,
boolean groupDataNodes);
-
+ /**
+ * Apply the changes in the given delta report to an anchor. The delta report contains the difference between two
+ * anchors or an anchor and a configuration.
+ *
+ * @param dataspaceName dataspace name
+ * @param anchorName anchor name where the delta report is to be applied
+ * @param deltaReportAsJsonString delta report in JSON string format
+ */
+ void applyChangesInDeltaReport(String dataspaceName, String anchorName, String deltaReportAsJsonString);
}
@Override
@Timed(value = "cps.data.service.datanode.leaves.descendants.leaves.update",
description = "Time taken to update data node leaves and existing descendants leaves")
- public void updateNodeLeavesAndExistingDescendantLeaves(final String dataspaceName, final String anchorName,
- final String parentNodeXpath,
- final String dataNodeUpdatesAsJson,
- final OffsetDateTime observedTimestamp) {
+ public void updateNodeLeavesAndExistingDescendantLeaves(final String dataspaceName,
+ final String anchorName,
+ final String parentNodeXpath,
+ final String dataNodeUpdatesAsJson,
+ final OffsetDateTime observedTimestamp) {
cpsValidator.validateNameCharacters(dataspaceName, anchorName);
final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName);
final Collection<DataNode> dataNodeUpdates = dataNodeFactory
import org.onap.cps.api.parameters.FetchDescendantsOption;
import org.onap.cps.utils.DataMapper;
import org.onap.cps.utils.JsonObjectMapper;
+import org.onap.cps.utils.deltareport.DeltaReportExecutor;
import org.onap.cps.utils.deltareport.DeltaReportGenerator;
import org.onap.cps.utils.deltareport.GroupedDeltaReportGenerator;
import org.springframework.stereotype.Service;
@RequiredArgsConstructor
public class CpsDeltaServiceImpl implements CpsDeltaService {
+ private final DeltaReportExecutor deltaReportExecutor;
private final CpsAnchorService cpsAnchorService;
private final CpsDataService cpsDataService;
private final DataNodeFactory dataNodeFactory;
return getDeltaReports(sourceDataNodesRebuilt, targetDataNodes, groupDataNodes);
}
+ /**
+ * Apply the delta report to the data nodes.
+ *
+ * @param dataspaceName name of the dataspace
+ * @param anchorName name of the anchor
+ * @param deltaReportAsJsonString JSON string representing the delta report
+ */
+ @Override
+ public void applyChangesInDeltaReport(final String dataspaceName, final String anchorName,
+ final String deltaReportAsJsonString) {
+ deltaReportExecutor.applyChangesInDeltaReport(dataspaceName, anchorName, deltaReportAsJsonString);
+ }
+
private List<DeltaReport> getDeltaReports(final Collection<DataNode> sourceDataNodes,
final Collection<DataNode> targetDataNodes,
final boolean groupDataNodes) {
.createDataNodesWithYangResourceXpathAndNodeData(yangResourceContentPerName, xpath, targetData, JSON);
}
}
+
}
/*
* ============LICENSE_START=======================================================
* Copyright (C) 2022 Nordix Foundation
+ * Modifications 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.
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.type.CollectionType;
+import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.onap.cps.api.exceptions.DataValidationException;
+ "JSON content to Json Node.", e.getMessage());
}
}
+
+ /**
+ * Deserialize JSON content from given JSON content String to List of specific class type.
+ *
+ * @param jsonContent JSON content
+ * @param collectionEntryType compatible Object class type
+ * @param <T> type parameter
+ * @return a list of specific class type 'T'
+ */
+ public <T> List<T> convertToJsonArray(final String jsonContent, final Class<T> collectionEntryType) {
+ try {
+ final CollectionType collectionType =
+ objectMapper.getTypeFactory().constructCollectionType(List.class, collectionEntryType);
+ return objectMapper.readValue(jsonContent, collectionType);
+ } catch (final JsonProcessingException e) {
+ log.error("Parsing error occurred while converting JSON content to specific class type.");
+ throw new DataValidationException("Parsing error occurred while converting "
+ + "JSON content to specific class type.", e.getMessage());
+ }
+ }
}
--- /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.utils.deltareport;
+
+import static org.onap.cps.utils.ContentType.JSON;
+
+import java.time.OffsetDateTime;
+import java.util.Collection;
+import java.util.List;
+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.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.cpspath.parser.CpsPathUtil;
+import org.onap.cps.utils.JsonObjectMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class DeltaReportExecutor {
+
+ private static final OffsetDateTime NO_TIMESTAMP = null;
+
+ private final CpsAnchorService cpsAnchorService;
+ private final CpsDataService cpsDataService;
+ private final DataNodeFactory dataNodeFactory;
+ private final JsonObjectMapper jsonObjectMapper;
+
+ /**
+ * Applies the delta report to the specified dataspace and anchor.
+ *
+ * @param dataspaceName the name of the dataspace
+ * @param anchorName the name of the anchor
+ * @param deltaReportAsJsonString the delta report as a JSON string
+ */
+ @Transactional
+ public void applyChangesInDeltaReport(final String dataspaceName, final String anchorName,
+ final String deltaReportAsJsonString) {
+ final List<DeltaReport> deltaReports =
+ jsonObjectMapper.convertToJsonArray(deltaReportAsJsonString, DeltaReport.class);
+ for (final DeltaReport deltaReport: deltaReports) {
+ final String action = deltaReport.getAction();
+ final String xpath = deltaReport.getXpath();
+ if (action.equals(DeltaReport.REPLACE_ACTION)) {
+ final String dataForUpdate = jsonObjectMapper.asJsonString(deltaReport.getTargetData());
+ updateDataNodes(dataspaceName, anchorName, xpath, dataForUpdate);
+ } else if (action.equals(DeltaReport.REMOVE_ACTION)) {
+ final String dataForDelete = jsonObjectMapper.asJsonString(deltaReport.getSourceData());
+ deleteDataNodesUsingDelta(dataspaceName, anchorName, xpath, dataForDelete);
+ } else {
+ final String dataForCreate = jsonObjectMapper.asJsonString(deltaReport.getTargetData());
+ addDataNodesUsingDelta(dataspaceName, anchorName, xpath, dataForCreate);
+ }
+ }
+ }
+
+ private void updateDataNodes(final String dataspaceName, final String anchorName, final String xpath,
+ final String updatedData) {
+ cpsDataService.updateNodeLeavesAndExistingDescendantLeaves(dataspaceName, anchorName,
+ CpsPathUtil.getNormalizedParentXpath(xpath), updatedData, NO_TIMESTAMP);
+ }
+
+ private void deleteDataNodesUsingDelta(final String dataspaceName, final String anchorName, final String xpath,
+ final String dataToDelete) {
+ final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName);
+ final Collection<DataNode> dataNodesToDelete =
+ dataNodeFactory.createDataNodesWithAnchorParentXpathAndNodeData(anchor, xpath, dataToDelete, JSON);
+ final Collection<String> xpathsToDelete = dataNodesToDelete.stream().map(DataNode::getXpath).toList();
+ cpsDataService.deleteDataNodes(dataspaceName, anchorName, xpathsToDelete, NO_TIMESTAMP);
+ }
+
+ private void addDataNodesUsingDelta(final String dataspaceName, final String anchorName, final String xpath,
+ final String dataToAdd) {
+ final String xpathToAdd = isRootListNodeXpath(xpath) ? CpsPathUtil.ROOT_NODE_XPATH : xpath;
+ cpsDataService.saveListElements(dataspaceName, anchorName, xpathToAdd, dataToAdd, NO_TIMESTAMP, JSON);
+ }
+
+ private boolean isRootListNodeXpath(final String xpath) {
+ return CpsPathUtil.getNormalizedParentXpath(xpath).isEmpty() && CpsPathUtil.isPathToListElement(xpath);
+ }
+}
import org.onap.cps.utils.PrefixResolver
import org.onap.cps.utils.YangParser
import org.onap.cps.utils.YangParserHelper
+import org.onap.cps.utils.deltareport.DeltaReportExecutor
import org.onap.cps.utils.deltareport.DeltaReportGenerator
import org.onap.cps.utils.deltareport.DeltaReportHelper
import org.onap.cps.utils.deltareport.GroupedDeltaReportGenerator
def mockCpsAnchorService = Mock(CpsAnchorService)
def mockCpsDataService = Mock(CpsDataService)
+ def mockDeltaReportExecutor = Mock(DeltaReportExecutor)
def mockYangTextSchemaSourceSetCache = Mock(YangTextSchemaSourceSetCache)
def mockTimedYangTextSchemaSourceSetBuilder = Mock(TimedYangTextSchemaSourceSetBuilder)
def yangParser = new YangParser(new YangParserHelper(), mockYangTextSchemaSourceSetCache, mockTimedYangTextSchemaSourceSetBuilder)
def deltaReportHelper = new DeltaReportHelper()
def deltaReportGenerator = new DeltaReportGenerator(deltaReportHelper)
def groupedDeltaReportGenerator = new GroupedDeltaReportGenerator(deltaReportHelper)
- def objectUnderTest = new CpsDeltaServiceImpl(mockCpsAnchorService, mockCpsDataService, dataNodeFactory, dataMapper, jsonObjectMapper, deltaReportGenerator, groupedDeltaReportGenerator)
+ def objectUnderTest = new CpsDeltaServiceImpl(mockDeltaReportExecutor, mockCpsAnchorService, mockCpsDataService, dataNodeFactory, dataMapper, jsonObjectMapper, deltaReportGenerator, groupedDeltaReportGenerator)
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'])]
'removed' | bookstoreDataNodesWithChildXpathAndNoLeaves | bookstoreDataNodeWithChildXpath || null | ['categories': [['code': '02', 'name': 'Kids']]]
}
- def setupSchemaSetMocks(String... yangResources) {
+ def 'Apply changes from a delta report to an anchor'() {
+ given: 'delta report as JSON string'
+ def deltaReportJson = '[{"action":"replace","xpath":"/bookstore","sourceData":{"bookstore":{"bookstore-name":"Easons"}},"targetData":{"bookstore":{"bookstore-name":"My Store"}}}]'
+ when: 'an attempt to apply the delta report to the anchor'
+ objectUnderTest.applyChangesInDeltaReport(dataspaceName, ANCHOR_NAME_1, deltaReportJson)
+ then: 'utility class to apply the delta report is invoked with expected parameters'
+ 1 * mockDeltaReportExecutor.applyChangesInDeltaReport(dataspaceName, ANCHOR_NAME_1, deltaReportJson)
+ }
+
+ def setupSchemaSetMocks(yangResources) {
def mockYangTextSchemaSourceSet = Mock(YangTextSchemaSourceSet)
mockYangTextSchemaSourceSetCache.get(dataspaceName, schemaSetName) >> mockYangTextSchemaSourceSet
def yangResourceNameToContent = TestUtils.getYangResourcesAsMap(yangResources)
mockYangTextSchemaSourceSet.getSchemaContext() >> schemaContext
}
- def setupSchemaSetMocksForDelta(Map<String, String> yangResourceContentPerName) {
+ def setupSchemaSetMocksForDelta(yangResourceContentPerName) {
def mockYangTextSchemaSourceSet = Mock(YangTextSchemaSourceSet)
mockTimedYangTextSchemaSourceSetBuilder.getYangTextSchemaSourceSet(yangResourceContentPerName) >> mockYangTextSchemaSourceSet
mockYangTextSchemaSourceSetCache.get(_, _) >> mockYangTextSchemaSourceSet
/*
* ============LICENSE_START=======================================================
* Copyright (C) 2022 Nordix Foundation
+ * Modifications 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.
then: 'a data validation exception is thrown'
thrown(DataValidationException)
}
+
+ def 'Convert JSON content to a list of specific class type'() {
+ given: 'a JSON array string'
+ def jsonContent = '[{"key":"value1"}, {"key":"value2"}]'
+ when: 'The JSON content is converted to a list of the target class type'
+ def result = jsonObjectMapper.convertToJsonArray(jsonContent, Map)
+ then: 'The result is a list of the target class type and has expected content'
+ assert result == [[key: 'value1'], [key: 'value2']]
+ }
+
+ def 'Throw exception when JSON content is invalid for list conversion'() {
+ given: 'An invalid JSON array string'
+ def jsonContent = '[{"key":"value1", {"key":"value2"}'
+ when: 'an attempt to convert JSON content to a list of the target class type'
+ jsonObjectMapper.convertToJsonArray(jsonContent, Map)
+ then: 'a DataValidationException is thrown'
+ def thrown = thrown(DataValidationException)
+ thrown.message.contains('Parsing error occurred while converting JSON content to specific class type.')
+ }
}
--- /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.utils.deltareport
+
+
+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.model.DeltaReport
+import org.onap.cps.cpspath.parser.CpsPathUtil
+import org.onap.cps.impl.DataNodeFactoryImpl
+import org.onap.cps.impl.DeltaReportBuilder
+import org.onap.cps.impl.YangTextSchemaSourceSetCache
+import org.onap.cps.utils.ContentType
+import org.onap.cps.utils.JsonObjectMapper
+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 spock.lang.Shared
+import spock.lang.Specification
+
+class DeltaReportExecutorSpec extends Specification {
+
+ 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 jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
+ def objectUnderTest = new DeltaReportExecutor(mockCpsAnchorService, mockCpsDataService, dataNodeFactory, jsonObjectMapper)
+
+ @Shared
+ static def ANCHOR_NAME_1 = 'some-anchor-1'
+ static def ANCHOR_NAME_2 = 'some-anchor-2'
+ static def NO_TIMESTAMP = null
+ 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
+ }
+
+ def 'Perform delete operation on existing data under an anchor using delta report'() {
+ given: 'schema mocks and delta report in JSON format'
+ setupSchemaSetMocks('bookstore.yang')
+ def deltaReportJson = '[{"action":"remove","xpath":"/bookstore","sourceData":{"categories":[{"code":"1","name":"Children","books":[{"title":"Matilda"}]}]}}]'
+ and: 'delta report constructed from JSON'
+ def deltaReport = new DeltaReportBuilder().actionRemove().withXpath('/bookstore').withSourceData('categories': [['code': '1', 'name': 'Children', 'books': [['title': 'Matilda']]]]).build()
+ and: 'source data as JSON string from delta report'
+ def sourceData = jsonObjectMapper.asJsonString(deltaReport.getSourceData())
+ and: 'expected data nodes with child nodes to delete'
+ def dataNodes = [new DataNode(xpath: '/bookstore/categories[@code=\'1\']', childDataNodes: [new DataNode(xpath: '/bookstore/categories[@code=\'1\']/books[@title=\'Matilda\']')])]
+ def xpathsToDelete = dataNodes*.xpath
+ when: 'attempt to apply delta using the delta report'
+ objectUnderTest.applyChangesInDeltaReport(dataspaceName, ANCHOR_NAME_1, deltaReportJson)
+ then: 'the delta report in JSON format is converted to a list of DeltaReport objects'
+ jsonObjectMapper.convertToJsonArray(deltaReportJson, DeltaReport) >> [deltaReport]
+ and: 'data nodes are built from the source data of delta report'
+ dataNodeFactory.createDataNodesWithAnchorParentXpathAndNodeData(anchor1, deltaReport.getXpath(), sourceData, ContentType.JSON) >> [dataNodes]
+ and: 'appropriate cps data service method is invoked with expected parameters to delete data nodes'
+ 1 * mockCpsDataService.deleteDataNodes(dataspaceName, ANCHOR_NAME_1, xpathsToDelete, NO_TIMESTAMP)
+ }
+
+ def 'Perform create operation on existing data under an anchor using delta report to add a node with #scenario'() {
+ given: 'schema mocks and delta report in JSON format'
+ setupSchemaSetMocks('bookstore.yang')
+ and: 'target data as JSON string from delta report'
+ def targetData = jsonObjectMapper.asJsonString(deltaReport.getTargetData())
+ when: 'attempt to apply delta using the delta report'
+ objectUnderTest.applyChangesInDeltaReport(dataspaceName, ANCHOR_NAME_1, deltaReportJson)
+ then: 'the delta report in JSON format is converted to a list of DeltaReport objects'
+ jsonObjectMapper.convertToJsonArray(deltaReportJson, DeltaReport) >> [deltaReport]
+ and: 'appropriate cps data service method is invoked with expected parameters to create data nodes'
+ 1 * mockCpsDataService.saveListElements(dataspaceName, ANCHOR_NAME_1, expectedXpath, targetData, NO_TIMESTAMP, ContentType.JSON)
+ where:
+ scenario | deltaReportJson | deltaReport || expectedXpath
+ 'xpath' | '[{"action":"create","xpath":"/bookstore","targetData":{"categories":[{"code":"1","name":"Children"}]}}]' | new DeltaReportBuilder().actionCreate().withXpath('/bookstore').withTargetData(['categories': [['code': '1', 'name': 'Children']]]).build() || '/bookstore'
+ 'list node xpath' | '[{"action":"create","xpath":"/bookstore/categories[@code=\'1\']","targetData":{"books":[{"price":20,"title":"Matilda"}]}}]' | new DeltaReportBuilder().actionCreate().withXpath('/bookstore/categories[@code=\'1\']').withTargetData(['books': [['price': 20, 'title': 'Matilda']]]).build() || '/bookstore/categories[@code=\'1\']'
+ 'parent list node xpath' | '[{"action":"create","xpath":"/bookstore-address[@bookstore-name=\'Easons\']","targetData":{"address":{"street":"Main Street"}}}]' | new DeltaReportBuilder().actionCreate().withXpath('/bookstore-address[@bookstore-name=\'Easons\']').withTargetData(['address': ['street': 'Main Street']]).build() || '/'
+ }
+
+ def 'Perform replace operation on existing data under an anchor using delta report'() {
+ given: 'schema mocks and delta report in JSON format'
+ setupSchemaSetMocks('bookstore.yang')
+ def deltaReportJson = '[{"action":"replace","xpath":"/bookstore/categories[@code=\'1\']","sourceData":{"books":[{"price":20,"title":"Matilda"}]},"targetData":{"books":[{"price":30,"title":"Matilda"}]}}]'
+ and: 'delta report constructed from JSON'
+ def deltaReport = new DeltaReportBuilder().actionReplace().withXpath('/bookstore/categories[@code=\'1\']').withSourceData(['books': [['price': 20, 'title': 'Matilda']]]).withTargetData(['books': [['price': 30, 'title': 'Matilda']]]).build()
+ and: 'the parent node xpath is fetched from delta report'
+ def parentNodeXpath = CpsPathUtil.getNormalizedParentXpath(deltaReport.getXpath())
+ and: 'target data as JSON is fetched from delta report'
+ def targetData = jsonObjectMapper.asJsonString(deltaReport.getTargetData())
+ when: 'attempt to apply delta using the delta report'
+ objectUnderTest.applyChangesInDeltaReport(dataspaceName, ANCHOR_NAME_1, deltaReportJson)
+ then: 'the delta report in JSON format is converted to a list of DeltaReport objects'
+ jsonObjectMapper.convertToJsonArray(deltaReportJson, DeltaReport) >> [deltaReport]
+ and: 'cps data service is invoked with expected parameters to delete data nodes by using their xpaths'
+ 1 * mockCpsDataService.updateNodeLeavesAndExistingDescendantLeaves(dataspaceName, ANCHOR_NAME_1, parentNodeXpath, targetData, NO_TIMESTAMP)
+ }
+
+ def 'Batch operation using delta report rolls back in case of a semantically invalid Delta Report'() {
+ given: 'schema mocks and a semantically invalid delta report in JSON format'
+ setupSchemaSetMocks('bookstore.yang')
+ def deltaReportJson = '[{"action":"create","xpath":"/bookstore/categories[@code=\'100\']","targetData":{"categories":[{"code":"100","name":"Funny"}]}},{"action":"remove","xpath":"/bookstore/categories[@code=\'4\']","sourceData":{"categorie":[{"code":"4","name":"Computing"}]}}]'
+ and: 'delta report object with invalid data for remove operation'
+ def deltaReport = [new DeltaReportBuilder().actionCreate().withXpath('/bookstore/categories[@code=\'100\']').withTargetData(['categories': [['code': '100', 'name': 'Funny']]]).build(), new DeltaReportBuilder().actionRemove().withXpath('/bookstore/categories[@code=\'4\'').withSourceData(['categorie': ['code': '4', 'name': 'Computing']]).build()]
+ and: 'appropriate data is fetched from delta report'
+ def xpathForCreateOperation = deltaReport[0].xpath
+ def targetDataForCreateOperation = jsonObjectMapper.asJsonString(deltaReport[0].targetData)
+ def xpathForDeleteOperation = deltaReport[1].xpath
+ def sourceDataForDeleteOperation = jsonObjectMapper.asJsonString(deltaReport[1].sourceData)
+ when: 'attempt to apply delta using the delta report'
+ objectUnderTest.applyChangesInDeltaReport(dataspaceName, ANCHOR_NAME_1, deltaReportJson)
+ then: 'the delta report in JSON format is converted to DeltaReport objects'
+ jsonObjectMapper.convertToJsonArray(deltaReportJson, DeltaReport) >> [deltaReport]
+ and: 'the create operation is attempted and succeeds'
+ 1 * mockCpsDataService.saveListElements(dataspaceName, ANCHOR_NAME_1, xpathForCreateOperation, targetDataForCreateOperation, NO_TIMESTAMP, ContentType.JSON)
+ and: 'the remove operation fails due to invalid data, causing rollback'
+ dataNodeFactory.createDataNodesWithAnchorParentXpathAndNodeData(anchor1, xpathForDeleteOperation, sourceDataForDeleteOperation, ContentType.JSON) >> {throw new DataValidationException('Data Validation Failed')}
+ then: 'a DataValidationException is thrown'
+ thrown(DataValidationException)
+ }
+
+ def setupSchemaSetMocks(yangResources) {
+ def mockYangTextSchemaSourceSet = Mock(YangTextSchemaSourceSet)
+ mockYangTextSchemaSourceSetCache.get(dataspaceName, schemaSetName) >> mockYangTextSchemaSourceSet
+ def yangResourceNameToContent = TestUtils.getYangResourcesAsMap(yangResources)
+ def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext()
+ mockYangTextSchemaSourceSet.getSchemaContext() >> schemaContext
+ }
+}