aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorArpit Singh <as00745003@techmahindra.com>2023-09-07 17:05:37 +0530
committerArpit Singh <as00745003@techmahindra.com>2024-06-06 14:13:32 +0530
commitd7fa9601a1409ee3a156ac2f6a6ec11853989cd7 (patch)
tree1967c1bebdfca2e116bdd6acf630d87e39ccf3ae
parentf8ca09bf4e5d76fb95bd1eda17f15a7fd92d0f63 (diff)
CPS Delta API 2: Delta between anchor and payload
- Second API to get Delta between an anchor and JSON payload - added new API getDeltaByDataspaceAnchorAndPayload - added controller and service layer methods getDeltaByDataspaceAnchorAndPayload - Core Delta algorithm remains same as the first API. getDeltaByDataspaceAnchorAndPayload will call getDeltaBetweenDataNodes Issue-ID: CPS-1836 Signed-off-by: Arpit Singh <as00745003@techmahindra.com> Change-Id: Id74cd930ce48e5cb414aa62c5381b79675788a37
-rw-r--r--cps-rest/docs/openapi/cpsDataV2.yml55
-rw-r--r--cps-rest/docs/openapi/openapi.yml7
-rwxr-xr-xcps-rest/src/main/java/org/onap/cps/rest/controller/DataRestController.java25
-rwxr-xr-xcps-rest/src/test/groovy/org/onap/cps/rest/controller/DataRestControllerSpec.groovy55
-rw-r--r--cps-service/src/main/java/org/onap/cps/api/CpsDataService.java18
-rw-r--r--cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java167
-rw-r--r--cps-service/src/main/java/org/onap/cps/spi/model/DeltaReport.java2
-rw-r--r--cps-service/src/main/java/org/onap/cps/utils/YangParser.java28
-rw-r--r--cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy74
-rwxr-xr-xcps-service/src/test/groovy/org/onap/cps/api/impl/E2ENetworkSliceSpec.groovy11
-rw-r--r--cps-service/src/test/groovy/org/onap/cps/utils/YangParserSpec.groovy19
-rw-r--r--docs/release-notes.rst2
-rw-r--r--integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsDataServiceIntegrationSpec.groovy44
13 files changed, 478 insertions, 29 deletions
diff --git a/cps-rest/docs/openapi/cpsDataV2.yml b/cps-rest/docs/openapi/cpsDataV2.yml
index cbb5ce4104..a1433badf6 100644
--- a/cps-rest/docs/openapi/cpsDataV2.yml
+++ b/cps-rest/docs/openapi/cpsDataV2.yml
@@ -1,5 +1,5 @@
# ============LICENSE_START=======================================================
-# Copyright (c) 2022-2023 TechMahindra Ltd.
+# Copyright (c) 2022-2024 TechMahindra Ltd.
# ================================================================================
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -75,4 +75,55 @@ deltaByDataspaceAndAnchors:
$ref: 'components.yml#/components/responses/Forbidden'
'500':
$ref: 'components.yml#/components/responses/InternalServerError'
- x-codegen-request-body-name: xpath \ No newline at end of file
+ x-codegen-request-body-name: xpath
+
+deltaByDataspaceAnchorAndPayload:
+ 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/anchorNameInPath'
+ - $ref: 'components.yml#/components/parameters/xpathInQuery'
+ requestBody:
+ content:
+ multipart/form-data:
+ schema:
+ type: object
+ properties:
+ json:
+ type: object
+ example:
+ test:bookstore:
+ bookstore-name: Chapters
+ categories:
+ - code: 01
+ name: SciFi
+ - code: 02
+ name: kids
+ file:
+ type: string
+ format: binary
+ required:
+ - json
+ responses:
+ '200':
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: object
+ examples:
+ dataSample:
+ $ref: 'components.yml#/components/examples/deltaReportSample'
+ '400':
+ $ref: 'components.yml#/components/responses/BadRequest'
+ '401':
+ $ref: 'components.yml#/components/responses/Unauthorized'
+ '403':
+ $ref: 'components.yml#/components/responses/Forbidden'
+ '500':
+ $ref: 'components.yml#/components/responses/InternalServerError' \ No newline at end of file
diff --git a/cps-rest/docs/openapi/openapi.yml b/cps-rest/docs/openapi/openapi.yml
index f29335a0a9..b4e0b70408 100644
--- a/cps-rest/docs/openapi/openapi.yml
+++ b/cps-rest/docs/openapi/openapi.yml
@@ -2,7 +2,7 @@
# Copyright (C) 2021-2023 Nordix Foundation
# Modifications Copyright (C) 2021 Pantheon.tech
# Modifications Copyright (C) 2021 Bell Canada.
-# Modifications Copyright (C) 2022-2023 TechMahindra Ltd.
+# Modifications Copyright (C) 2022-2024 TechMahindra Ltd.
# ================================================================================
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -104,9 +104,12 @@ paths:
/{apiVersion}/dataspaces/{dataspace-name}/anchors/{anchor-name}/list-nodes:
$ref: 'cpsData.yml#/listElementByDataspaceAndAnchor'
- /v2/dataspaces/{dataspace-name}/anchors/{anchor-name}/delta:
+ /v2/dataspaces/{dataspace-name}/anchors/{anchor-name}/deltaAnchors:
$ref: 'cpsDataV2.yml#/deltaByDataspaceAndAnchors'
+ /v2/dataspaces/{dataspace-name}/anchors/{anchor-name}/deltaPayload:
+ $ref: 'cpsDataV2.yml#/deltaByDataspaceAnchorAndPayload'
+
/v1/dataspaces/{dataspace-name}/anchors/{anchor-name}/nodes/query:
$ref: 'cpsQueryV1Deprecated.yml#/nodesByDataspaceAndAnchorAndCpsPath'
diff --git a/cps-rest/src/main/java/org/onap/cps/rest/controller/DataRestController.java b/cps-rest/src/main/java/org/onap/cps/rest/controller/DataRestController.java
index 310171b309..f579c82d25 100755
--- a/cps-rest/src/main/java/org/onap/cps/rest/controller/DataRestController.java
+++ b/cps-rest/src/main/java/org/onap/cps/rest/controller/DataRestController.java
@@ -24,12 +24,15 @@
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.ArrayList;
import java.util.Collection;
+import java.util.Collections;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
@@ -49,6 +52,7 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestHeader;
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}")
@@ -172,6 +176,27 @@ public class DataRestController implements CpsDataApi {
}
@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,
diff --git a/cps-rest/src/test/groovy/org/onap/cps/rest/controller/DataRestControllerSpec.groovy b/cps-rest/src/test/groovy/org/onap/cps/rest/controller/DataRestControllerSpec.groovy
index 3f5dcf2633..317b9c5b7c 100755
--- a/cps-rest/src/test/groovy/org/onap/cps/rest/controller/DataRestControllerSpec.groovy
+++ b/cps-rest/src/test/groovy/org/onap/cps/rest/controller/DataRestControllerSpec.groovy
@@ -41,7 +41,9 @@ 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
@@ -49,6 +51,7 @@ import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
import static org.onap.cps.spi.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
@@ -101,6 +104,10 @@ class DataRestControllerSpec extends Specification {
static DataNode dataNodeWithChild = new DataNodeBuilder().withXpath('/parent')
.withChildDataNodes([new DataNodeBuilder().withXpath("/parent/child").build()]).build()
+ @Shared
+ static MultipartFile multipartYangFile = new MockMultipartFile("file", 'filename.yang', "text/plain", 'content'.getBytes())
+
+
def setup() {
dataNodeBaseEndpointV1 = "$basePath/v1/dataspaces/$dataspaceName"
dataNodeBaseEndpointV2 = "$basePath/v2/dataspaces/$dataspaceName"
@@ -337,9 +344,9 @@ class DataRestControllerSpec extends Specification {
def 'Get delta between two anchors'() {
given: 'the service returns a list containing delta reports'
- def deltaReports = new DeltaReportBuilder().actionAdd().withXpath('/bookstore').withSourceData('bookstore-name': 'Easons').withTargetData('bookstore-name': 'Easons').build()
+ def deltaReports = new DeltaReportBuilder().actionUpdate().withXpath('some xpath').withSourceData('some key': 'some value').withTargetData('some key': 'some value').build()
def xpath = 'some xpath'
- def endpoint = "$dataNodeBaseEndpointV2/anchors/sourceAnchor/delta"
+ def endpoint = "$dataNodeBaseEndpointV2/anchors/sourceAnchor/deltaAnchors"
mockCpsDataService.getDeltaByDataspaceAndAnchors(dataspaceName, 'sourceAnchor', 'targetAnchor', xpath, OMIT_DESCENDANTS) >> [deltaReports]
when: 'get delta request is performed using REST API'
def response =
@@ -350,7 +357,48 @@ class DataRestControllerSpec extends Specification {
then: 'expected response code is returned'
assert response.status == HttpStatus.OK.value()
and: 'the response contains expected value'
- assert response.contentAsString.contains("[{\"action\":\"add\",\"xpath\":\"/bookstore\",\"sourceData\":{\"bookstore-name\":\"Easons\"},\"targetData\":{\"bookstore-name\":\"Easons\"}}]")
+ assert response.contentAsString.contains("[{\"action\":\"update\",\"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().actionAdd().withXpath('some xpath').build()
+ def xpath = 'some xpath'
+ def endpoint = "$dataNodeBaseEndpointV2/anchors/$anchorName/deltaPayload"
+ 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\":\"add\",\"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/deltaPayload"
+ 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.'() {
@@ -507,4 +555,5 @@ class DataRestControllerSpec extends Specification {
'without observed timestamp' | null || 1 | HttpStatus.NO_CONTENT
'with invalid observed timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST
}
+
}
diff --git a/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java b/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java
index 71ed061032..f396b49e6b 100644
--- a/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java
+++ b/cps-service/src/main/java/org/onap/cps/api/CpsDataService.java
@@ -4,7 +4,7 @@
* Modifications Copyright (C) 2021 Pantheon.tech
* Modifications Copyright (C) 2021-2022 Bell Canada
* Modifications Copyright (C) 2022 Deutsche Telekom AG
- * Modifications Copyright (C) 2024 TechMahindra Ltd.
+ * Modifications Copyright (C) 2023-2024 TechMahindra Ltd.
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -303,4 +303,20 @@ public interface CpsDataService {
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 yangResourcesNameToContentMap 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> yangResourcesNameToContentMap,
+ String targetData,
+ FetchDescendantsOption fetchDescendantsOption);
}
diff --git a/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java b/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java
index 3496fc7c45..6386d38ffc 100644
--- a/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java
+++ b/cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java
@@ -30,6 +30,7 @@ import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@@ -50,6 +51,9 @@ import org.onap.cps.spi.model.DataNodeBuilder;
import org.onap.cps.spi.model.DeltaReport;
import org.onap.cps.spi.utils.CpsValidator;
import org.onap.cps.utils.ContentType;
+import org.onap.cps.utils.DataMapUtils;
+import org.onap.cps.utils.JsonObjectMapper;
+import org.onap.cps.utils.PrefixResolver;
import org.onap.cps.utils.YangParser;
import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
import org.springframework.stereotype.Service;
@@ -69,6 +73,8 @@ public class CpsDataServiceImpl implements CpsDataService {
private final CpsValidator cpsValidator;
private final YangParser yangParser;
private final CpsDeltaService cpsDeltaService;
+ private final JsonObjectMapper jsonObjectMapper;
+ private final PrefixResolver prefixResolver;
@Override
public void saveData(final String dataspaceName, final String anchorName, final String nodeData,
@@ -83,7 +89,8 @@ public class CpsDataServiceImpl implements CpsDataService {
final OffsetDateTime observedTimestamp, final ContentType contentType) {
cpsValidator.validateNameCharacters(dataspaceName, anchorName);
final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName);
- final Collection<DataNode> dataNodes = buildDataNodes(anchor, ROOT_NODE_XPATH, nodeData, contentType);
+ final Collection<DataNode> dataNodes =
+ buildDataNodesWithParentNodeXpath(anchor, ROOT_NODE_XPATH, nodeData, contentType);
cpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName, dataNodes);
sendDataUpdatedEvent(anchor, ROOT_NODE_XPATH, Operation.CREATE, observedTimestamp);
}
@@ -102,7 +109,8 @@ public class CpsDataServiceImpl implements CpsDataService {
final ContentType contentType) {
cpsValidator.validateNameCharacters(dataspaceName, anchorName);
final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName);
- final Collection<DataNode> dataNodes = buildDataNodes(anchor, parentNodeXpath, nodeData, contentType);
+ final Collection<DataNode> dataNodes =
+ buildDataNodesWithParentNodeXpath(anchor, parentNodeXpath, nodeData, contentType);
cpsDataPersistenceService.addChildDataNodes(dataspaceName, anchorName, parentNodeXpath, dataNodes);
sendDataUpdatedEvent(anchor, parentNodeXpath, Operation.CREATE, observedTimestamp);
}
@@ -115,7 +123,7 @@ public class CpsDataServiceImpl implements CpsDataService {
cpsValidator.validateNameCharacters(dataspaceName, anchorName);
final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName);
final Collection<DataNode> listElementDataNodeCollection =
- buildDataNodes(anchor, parentNodeXpath, jsonData, ContentType.JSON);
+ buildDataNodesWithParentNodeXpath(anchor, parentNodeXpath, jsonData, ContentType.JSON);
if (isRootNodeXpath(parentNodeXpath)) {
cpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName, listElementDataNodeCollection);
} else {
@@ -153,8 +161,8 @@ public class CpsDataServiceImpl implements CpsDataService {
final String nodeData, final OffsetDateTime observedTimestamp, final ContentType contentType) {
cpsValidator.validateNameCharacters(dataspaceName, anchorName);
final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName);
- final Collection<DataNode> dataNodesInPatch = buildDataNodes(anchor, parentNodeXpath, nodeData,
- contentType);
+ final Collection<DataNode> dataNodesInPatch =
+ buildDataNodesWithParentNodeXpath(anchor, parentNodeXpath, nodeData, contentType);
final Map<String, Map<String, Serializable>> xpathToUpdatedLeaves = dataNodesInPatch.stream()
.collect(Collectors.toMap(DataNode::getXpath, DataNode::getLeaves));
cpsDataPersistenceService.batchUpdateDataLeaves(dataspaceName, anchorName, xpathToUpdatedLeaves);
@@ -171,7 +179,7 @@ public class CpsDataServiceImpl implements CpsDataService {
cpsValidator.validateNameCharacters(dataspaceName, anchorName);
final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName);
final Collection<DataNode> dataNodeUpdates =
- buildDataNodes(anchor, parentNodeXpath, dataNodeUpdatesAsJson, ContentType.JSON);
+ buildDataNodesWithParentNodeXpath(anchor, parentNodeXpath, dataNodeUpdatesAsJson, ContentType.JSON);
for (final DataNode dataNodeUpdate : dataNodeUpdates) {
processDataNodeUpdate(anchor, dataNodeUpdate);
}
@@ -215,6 +223,29 @@ public class CpsDataServiceImpl implements CpsDataService {
return cpsDeltaService.getDeltaReports(sourceDataNodes, targetDataNodes);
}
+ @Timed(value = "cps.data.service.get.deltaBetweenAnchorAndPayload",
+ 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> yangResourcesNameToContentMap,
+ 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, yangResourcesNameToContentMap, 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")
@@ -223,7 +254,8 @@ public class CpsDataServiceImpl implements CpsDataService {
final OffsetDateTime observedTimestamp) {
cpsValidator.validateNameCharacters(dataspaceName, anchorName);
final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName);
- final Collection<DataNode> dataNodes = buildDataNodes(anchor, parentNodeXpath, jsonData, ContentType.JSON);
+ final Collection<DataNode> dataNodes =
+ buildDataNodesWithParentNodeXpath(anchor, parentNodeXpath, jsonData, ContentType.JSON);
cpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName, dataNodes);
sendDataUpdatedEvent(anchor, parentNodeXpath, Operation.UPDATE, observedTimestamp);
}
@@ -236,7 +268,7 @@ public class CpsDataServiceImpl implements CpsDataService {
final OffsetDateTime observedTimestamp) {
cpsValidator.validateNameCharacters(dataspaceName, anchorName);
final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName);
- final Collection<DataNode> dataNodes = buildDataNodes(anchor, nodesJsonData);
+ final Collection<DataNode> dataNodes = buildDataNodesWithParentNodeXpath(anchor, nodesJsonData);
cpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName, dataNodes);
nodesJsonData.keySet().forEach(nodeXpath ->
sendDataUpdatedEvent(anchor, nodeXpath, Operation.UPDATE, observedTimestamp));
@@ -250,7 +282,7 @@ public class CpsDataServiceImpl implements CpsDataService {
cpsValidator.validateNameCharacters(dataspaceName, anchorName);
final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName);
final Collection<DataNode> newListElements =
- buildDataNodes(anchor, parentNodeXpath, jsonData, ContentType.JSON);
+ buildDataNodesWithParentNodeXpath(anchor, parentNodeXpath, jsonData, ContentType.JSON);
replaceListContent(dataspaceName, anchorName, parentNodeXpath, newListElements, observedTimestamp);
}
@@ -324,16 +356,68 @@ public class CpsDataServiceImpl implements CpsDataService {
sendDataUpdatedEvent(anchor, listNodeXpath, Operation.DELETE, observedTimestamp);
}
- private Collection<DataNode> buildDataNodes(final Anchor anchor, final Map<String, String> nodesJsonData) {
+
+ private Collection<DataNode> rebuildSourceDataNodes(final String xpath, final Anchor sourceAnchor,
+ final Collection<DataNode> sourceDataNodes) {
+
+ final Collection<DataNode> sourceDataNodesRebuilt = new ArrayList<>();
+ if (sourceDataNodes != null) {
+ final String sourceDataNodesAsJson = getDataNodesAsJson(sourceAnchor, sourceDataNodes);
+ sourceDataNodesRebuilt.addAll(
+ buildDataNodesWithAnchorAndXpath(sourceAnchor, xpath, sourceDataNodesAsJson, ContentType.JSON));
+ }
+ return sourceDataNodesRebuilt;
+ }
+
+ private Collection<DataNode> buildTargetDataNodes(final Anchor sourceAnchor, final String xpath,
+ final Map<String, String> yangResourcesNameToContentMap,
+ final String targetData) {
+ if (yangResourcesNameToContentMap.isEmpty()) {
+ return buildDataNodesWithAnchorAndXpath(sourceAnchor, xpath, targetData, ContentType.JSON);
+ } else {
+ return buildDataNodesWithYangResourceAndXpath(yangResourcesNameToContentMap, xpath,
+ targetData, ContentType.JSON);
+ }
+ }
+
+ private String getDataNodesAsJson(final Anchor anchor, final Collection<DataNode> dataNodes) {
+
+ final List<Map<String, Object>> prefixToDataNodes = prefixResolver(anchor, dataNodes);
+ final Map<String, Object> targetDataAsJsonObject = getNodeDataAsJsonString(prefixToDataNodes);
+ return jsonObjectMapper.asJsonString(targetDataAsJsonObject);
+ }
+
+ private Map<String, Object> getNodeDataAsJsonString(final List<Map<String, Object>> prefixToDataNodes) {
+ final Map<String, Object> nodeDataAsJson = new HashMap<>();
+ for (final Map<String, Object> prefixToDataNode : prefixToDataNodes) {
+ nodeDataAsJson.putAll(prefixToDataNode);
+ }
+ return nodeDataAsJson;
+ }
+
+ private List<Map<String, Object>> prefixResolver(final Anchor anchor, final Collection<DataNode> dataNodes) {
+ final List<Map<String, Object>> prefixToDataNodes = new ArrayList<>(dataNodes.size());
+ for (final DataNode dataNode: dataNodes) {
+ final String prefix = prefixResolver
+ .getPrefix(anchor.getDataspaceName(), anchor.getName(), dataNode.getXpath());
+ final Map<String, Object> prefixToDataNode = DataMapUtils.toDataMapWithIdentifier(dataNode, prefix);
+ prefixToDataNodes.add(prefixToDataNode);
+ }
+ return prefixToDataNodes;
+ }
+
+ private Collection<DataNode> buildDataNodesWithParentNodeXpath(final Anchor anchor,
+ final Map<String, String> nodesJsonData) {
final Collection<DataNode> dataNodes = new ArrayList<>();
for (final Map.Entry<String, String> nodeJsonData : nodesJsonData.entrySet()) {
- dataNodes.addAll(buildDataNodes(anchor, nodeJsonData.getKey(), nodeJsonData.getValue(), ContentType.JSON));
+ dataNodes.addAll(buildDataNodesWithParentNodeXpath(anchor, nodeJsonData.getKey(),
+ nodeJsonData.getValue(), ContentType.JSON));
}
return dataNodes;
}
- private Collection<DataNode> buildDataNodes(final Anchor anchor, final String parentNodeXpath,
- final String nodeData, final ContentType contentType) {
+ private Collection<DataNode> buildDataNodesWithParentNodeXpath(final Anchor anchor, final String parentNodeXpath,
+ final String nodeData, final ContentType contentType) {
if (ROOT_NODE_XPATH.equals(parentNodeXpath)) {
final ContainerNode containerNode = yangParser.parseData(contentType, nodeData, anchor, "");
@@ -358,6 +442,63 @@ public class CpsDataServiceImpl implements CpsDataService {
return dataNodes;
}
+ private Collection<DataNode> buildDataNodesWithParentNodeXpath(
+ final Map<String, String> yangResourcesNameToContentMap, final String xpath,
+ final String nodeData, final ContentType contentType) {
+
+ if (isRootNodeXpath(xpath)) {
+ final ContainerNode containerNode = yangParser.parseData(contentType, nodeData,
+ yangResourcesNameToContentMap, "");
+ final Collection<DataNode> dataNodes = new DataNodeBuilder()
+ .withContainerNode(containerNode)
+ .buildCollection();
+ if (dataNodes.isEmpty()) {
+ throw new DataValidationException("No data nodes.",
+ "Data nodes were not found under the xpath " + xpath);
+ }
+ return dataNodes;
+ }
+ final String normalizedParentNodeXpath = CpsPathUtil.getNormalizedXpath(xpath);
+ final ContainerNode containerNode =
+ yangParser.parseData(contentType, nodeData, yangResourcesNameToContentMap, normalizedParentNodeXpath);
+ final Collection<DataNode> dataNodes = new DataNodeBuilder()
+ .withParentNodeXpath(normalizedParentNodeXpath)
+ .withContainerNode(containerNode)
+ .buildCollection();
+ if (dataNodes.isEmpty()) {
+ throw new DataValidationException("No data nodes.", "Data nodes were not found under the xpath " + xpath);
+ }
+ return dataNodes;
+ }
+
+ private Collection<DataNode> buildDataNodesWithAnchorAndXpath(final Anchor anchor, final String xpath,
+ final String nodeData,
+ final ContentType contentType) {
+
+ if (!isRootNodeXpath(xpath)) {
+ final String parentNodeXpath = CpsPathUtil.getNormalizedParentXpath(xpath);
+ if (parentNodeXpath.isEmpty()) {
+ return buildDataNodesWithParentNodeXpath(anchor, ROOT_NODE_XPATH, nodeData, contentType);
+ }
+ return buildDataNodesWithParentNodeXpath(anchor, parentNodeXpath, nodeData, contentType);
+ }
+ return buildDataNodesWithParentNodeXpath(anchor, xpath, nodeData, contentType);
+ }
+
+ private Collection<DataNode> buildDataNodesWithYangResourceAndXpath(
+ final Map<String, String> yangResourcesNameToContentMap, final String xpath,
+ final String nodeData, final ContentType contentType) {
+ if (!isRootNodeXpath(xpath)) {
+ final String parentNodeXpath = CpsPathUtil.getNormalizedParentXpath(xpath);
+ if (parentNodeXpath.isEmpty()) {
+ return buildDataNodesWithParentNodeXpath(yangResourcesNameToContentMap, ROOT_NODE_XPATH,
+ nodeData, contentType);
+ }
+ return buildDataNodesWithParentNodeXpath(yangResourcesNameToContentMap, parentNodeXpath,
+ nodeData, contentType);
+ }
+ return buildDataNodesWithParentNodeXpath(yangResourcesNameToContentMap, xpath, nodeData, contentType);
+ }
private static boolean isRootNodeXpath(final String xpath) {
return ROOT_NODE_XPATH.equals(xpath);
diff --git a/cps-service/src/main/java/org/onap/cps/spi/model/DeltaReport.java b/cps-service/src/main/java/org/onap/cps/spi/model/DeltaReport.java
index fb9c1971b2..34715e70b9 100644
--- a/cps-service/src/main/java/org/onap/cps/spi/model/DeltaReport.java
+++ b/cps-service/src/main/java/org/onap/cps/spi/model/DeltaReport.java
@@ -20,6 +20,7 @@
package org.onap.cps.spi.model;
+import com.fasterxml.jackson.annotation.JsonInclude;
import java.io.Serializable;
import java.util.Map;
import lombok.AccessLevel;
@@ -28,6 +29,7 @@ import lombok.Setter;
@Setter(AccessLevel.PROTECTED)
@Getter
+@JsonInclude(JsonInclude.Include.NON_NULL)
public class DeltaReport {
public static final String ADD_ACTION = "add";
diff --git a/cps-service/src/main/java/org/onap/cps/utils/YangParser.java b/cps-service/src/main/java/org/onap/cps/utils/YangParser.java
index 6299ef39f4..dc23c6bc4a 100644
--- a/cps-service/src/main/java/org/onap/cps/utils/YangParser.java
+++ b/cps-service/src/main/java/org/onap/cps/utils/YangParser.java
@@ -1,6 +1,7 @@
/*
* ============LICENSE_START=======================================================
* Copyright (C) 2024 Nordix Foundation.
+ * Modifications Copyright (C) 2024 TechMahindra Ltd.
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,20 +22,25 @@
package org.onap.cps.utils;
import io.micrometer.core.annotation.Timed;
+import java.util.Map;
import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
import org.onap.cps.api.impl.YangTextSchemaSourceSetCache;
import org.onap.cps.spi.exceptions.DataValidationException;
import org.onap.cps.spi.model.Anchor;
+import org.onap.cps.yang.TimedYangTextSchemaSourceSetBuilder;
import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
import org.opendaylight.yangtools.yang.model.api.SchemaContext;
import org.springframework.stereotype.Service;
+@Slf4j
@Service
@RequiredArgsConstructor
public class YangParser {
private final YangParserHelper yangParserHelper;
private final YangTextSchemaSourceSetCache yangTextSchemaSourceSetCache;
+ private final TimedYangTextSchemaSourceSetBuilder timedYangTextSchemaSourceSetBuilder;
/**
* Parses data into (normalized) ContainerNode according to schema context for the given anchor.
@@ -58,11 +64,33 @@ public class YangParser {
return yangParserHelper.parseData(contentType, nodeData, schemaContext, parentNodeXpath);
}
+ /**
+ * Parses data into (normalized) ContainerNode according to schema context for the given yang resource.
+ *
+ * @param nodeData data string
+ * @param yangResourcesNameToContentMap yang resource to content map
+ * @return the NormalizedNode object
+ */
+ @Timed(value = "cps.utils.yangparser.nodedata.with.parent.with.yangResourceMap.parse",
+ description = "Time taken to parse node data with a parent")
+ public ContainerNode parseData(final ContentType contentType,
+ final String nodeData,
+ final Map<String, String> yangResourcesNameToContentMap,
+ final String parentNodeXpath) {
+ final SchemaContext schemaContext = getSchemaContext(yangResourcesNameToContentMap);
+ return yangParserHelper.parseData(contentType, nodeData, schemaContext, parentNodeXpath);
+ }
+
private SchemaContext getSchemaContext(final Anchor anchor) {
return yangTextSchemaSourceSetCache.get(anchor.getDataspaceName(),
anchor.getSchemaSetName()).getSchemaContext();
}
+ private SchemaContext getSchemaContext(final Map<String, String> yangResourcesNameToContentMap) {
+ return timedYangTextSchemaSourceSetBuilder.getYangTextSchemaSourceSet(yangResourcesNameToContentMap)
+ .getSchemaContext();
+ }
+
private void invalidateCache(final Anchor anchor) {
yangTextSchemaSourceSetCache.removeFromCache(anchor.getDataspaceName(), anchor.getSchemaSetName());
}
diff --git a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy
index 4542ecb673..edf25715b3 100644
--- a/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy
+++ b/cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy
@@ -23,6 +23,7 @@
package org.onap.cps.api.impl
+import com.fasterxml.jackson.databind.ObjectMapper
import ch.qos.logback.classic.Level
import ch.qos.logback.classic.Logger
import ch.qos.logback.core.read.ListAppender
@@ -43,6 +44,9 @@ import org.onap.cps.spi.utils.CpsValidator
import org.onap.cps.utils.ContentType
import org.onap.cps.utils.YangParser
import org.onap.cps.utils.YangParserHelper
+import org.onap.cps.utils.JsonObjectMapper
+import org.onap.cps.utils.PrefixResolver
+import org.onap.cps.yang.TimedYangTextSchemaSourceSetBuilder
import org.onap.cps.yang.YangTextSchemaSourceSet
import org.onap.cps.yang.YangTextSchemaSourceSetBuilder
import org.slf4j.LoggerFactory
@@ -56,11 +60,15 @@ class CpsDataServiceImplSpec extends Specification {
def mockCpsAnchorService = Mock(CpsAnchorService)
def mockYangTextSchemaSourceSetCache = Mock(YangTextSchemaSourceSetCache)
def mockCpsValidator = Mock(CpsValidator)
- def yangParser = new YangParser(new YangParserHelper(), mockYangTextSchemaSourceSetCache)
+ def mockTimedYangTextSchemaSourceSetBuilder = Mock(TimedYangTextSchemaSourceSetBuilder)
+ def yangParser = new YangParser(new YangParserHelper(), mockYangTextSchemaSourceSetCache, mockTimedYangTextSchemaSourceSetBuilder)
def mockCpsDeltaService = Mock(CpsDeltaService);
def mockDataUpdateEventsService = Mock(CpsDataUpdateEventsService)
+ def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
+ def mockPrefixResolver = Mock(PrefixResolver)
- def objectUnderTest = new CpsDataServiceImpl(mockCpsDataPersistenceService, mockDataUpdateEventsService, mockCpsAnchorService, mockCpsValidator, yangParser, mockCpsDeltaService)
+ def objectUnderTest = new CpsDataServiceImpl(mockCpsDataPersistenceService, mockDataUpdateEventsService, mockCpsAnchorService,
+ mockCpsValidator, yangParser, mockCpsDeltaService, jsonObjectMapper, mockPrefixResolver)
def logger = (Logger) LoggerFactory.getLogger(objectUnderTest.class)
def loggingListAppender
@@ -230,6 +238,60 @@ class CpsDataServiceImplSpec extends Specification {
1 * mockCpsDeltaService.getDeltaReports(sourceDataNodes, targetDataNodes)
}
+ def 'Get delta between anchor and payload with user provided schema #scenario'() {
+ given: 'user provided schema set '
+ def yangResourcesNameToContentMap = TestUtils.getYangResourcesAsMap('bookstore.yang')
+ setupSchemaSetMocksForDelta(yangResourcesNameToContentMap)
+ when: 'attempt to get delta between an anchor and a JSON payload'
+ objectUnderTest.getDeltaByDataspaceAnchorAndPayload(dataspaceName, anchorName, xpath, yangResourcesNameToContentMap, 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 yangResourcesNameToContentMap = TestUtils.getYangResourcesAsMap('bookstore.yang')
+ setupSchemaSetMocksForDelta(yangResourcesNameToContentMap)
+ when: 'attempt to get delta between anchor and payload'
+ objectUnderTest.getDeltaByDataspaceAnchorAndPayload(dataspaceName, anchorName, xpath, yangResourcesNameToContentMap, 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')
@@ -503,4 +565,12 @@ class CpsDataServiceImplSpec extends Specification {
mockYangTextSchemaSourceSet.getSchemaContext() >> schemaContext
}
+ def setupSchemaSetMocksForDelta(Map<String, String> yangResourcesNameToContentMap) {
+ def mockYangTextSchemaSourceSet = Mock(YangTextSchemaSourceSet)
+ mockTimedYangTextSchemaSourceSetBuilder.getYangTextSchemaSourceSet(yangResourcesNameToContentMap) >> mockYangTextSchemaSourceSet
+ mockYangTextSchemaSourceSetCache.get(_, _) >> mockYangTextSchemaSourceSet
+ def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourcesNameToContentMap).getSchemaContext()
+ mockYangTextSchemaSourceSet.getSchemaContext() >> schemaContext
+ }
+
}
diff --git a/cps-service/src/test/groovy/org/onap/cps/api/impl/E2ENetworkSliceSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/api/impl/E2ENetworkSliceSpec.groovy
index 57f2f8ea7c..9e55e8f10a 100755
--- a/cps-service/src/test/groovy/org/onap/cps/api/impl/E2ENetworkSliceSpec.groovy
+++ b/cps-service/src/test/groovy/org/onap/cps/api/impl/E2ENetworkSliceSpec.groovy
@@ -23,6 +23,7 @@
package org.onap.cps.api.impl
+import com.fasterxml.jackson.databind.ObjectMapper
import org.onap.cps.TestUtils
import org.onap.cps.api.CpsAnchorService
import org.onap.cps.api.CpsDeltaService
@@ -31,6 +32,8 @@ import org.onap.cps.spi.CpsDataPersistenceService
import org.onap.cps.spi.CpsModulePersistenceService
import org.onap.cps.spi.model.Anchor
import org.onap.cps.spi.utils.CpsValidator
+import org.onap.cps.utils.JsonObjectMapper
+import org.onap.cps.utils.PrefixResolver
import org.onap.cps.utils.ContentType
import org.onap.cps.utils.YangParser
import org.onap.cps.utils.YangParserHelper
@@ -45,15 +48,17 @@ class E2ENetworkSliceSpec extends Specification {
def mockYangTextSchemaSourceSetCache = Mock(YangTextSchemaSourceSetCache)
def mockCpsValidator = Mock(CpsValidator)
def timedYangTextSchemaSourceSetBuilder = new TimedYangTextSchemaSourceSetBuilder()
- def yangParser = new YangParser(new YangParserHelper(), mockYangTextSchemaSourceSetCache)
+ def yangParser = new YangParser(new YangParserHelper(), mockYangTextSchemaSourceSetCache, timedYangTextSchemaSourceSetBuilder)
def mockCpsDeltaService = Mock(CpsDeltaService)
+ def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())
+ def mockPrefixResolver = Mock(PrefixResolver)
def cpsModuleServiceImpl = new CpsModuleServiceImpl(mockModuleStoreService,
mockYangTextSchemaSourceSetCache, mockCpsAnchorService, mockCpsValidator,timedYangTextSchemaSourceSetBuilder)
def mockDataUpdateEventsService = Mock(CpsDataUpdateEventsService)
- def cpsDataServiceImpl = new CpsDataServiceImpl(mockDataStoreService, mockDataUpdateEventsService, mockCpsAnchorService, mockCpsValidator, yangParser, mockCpsDeltaService)
-
+ def cpsDataServiceImpl = new CpsDataServiceImpl(mockDataStoreService, mockDataUpdateEventsService, mockCpsAnchorService, mockCpsValidator,
+ yangParser, mockCpsDeltaService, jsonObjectMapper, mockPrefixResolver)
def dataspaceName = 'someDataspace'
def anchorName = 'someAnchor'
def schemaSetName = 'someSchemaSet'
diff --git a/cps-service/src/test/groovy/org/onap/cps/utils/YangParserSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/utils/YangParserSpec.groovy
index 99070fe729..18d0502e30 100644
--- a/cps-service/src/test/groovy/org/onap/cps/utils/YangParserSpec.groovy
+++ b/cps-service/src/test/groovy/org/onap/cps/utils/YangParserSpec.groovy
@@ -1,6 +1,7 @@
/*
* ============LICENSE_START=======================================================
* Copyright (C) 2024 Nordix Foundation
+ * Modifications Copyright (C) 2024 TechMahindra Ltd.
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,9 +21,12 @@
package org.onap.cps.utils
+import org.onap.cps.TestUtils
import org.onap.cps.spi.exceptions.DataValidationException
import org.onap.cps.spi.model.Anchor
+import org.onap.cps.yang.TimedYangTextSchemaSourceSetBuilder
import org.onap.cps.yang.YangTextSchemaSourceSet
+import org.onap.cps.yang.YangTextSchemaSourceSetBuilder
import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode
import org.opendaylight.yangtools.yang.model.api.SchemaContext
import spock.lang.Specification
@@ -32,10 +36,12 @@ class YangParserSpec extends Specification {
def mockYangParserHelper = Mock(YangParserHelper)
def mockYangTextSchemaSourceSetCache = Mock(YangTextSchemaSourceSetCache)
+ def mockTimedYangTextSchemaSourceSetBuilder = Mock(TimedYangTextSchemaSourceSetBuilder)
- def objectUnderTest = new YangParser(mockYangParserHelper, mockYangTextSchemaSourceSetCache)
+ def objectUnderTest = new YangParser(mockYangParserHelper, mockYangTextSchemaSourceSetCache, mockTimedYangTextSchemaSourceSetBuilder)
def anchor = new Anchor(dataspaceName: 'my dataspace', schemaSetName: 'my schema')
+ def yangResourcesNameToContentMap = TestUtils.getYangResourcesAsMap('bookstore.yang')
def mockYangTextSchemaSourceSet = Mock(YangTextSchemaSourceSet)
def mockSchemaContext = Mock(SchemaContext)
def containerNodeFromYangUtils = Mock(ContainerNode)
@@ -82,4 +88,15 @@ class YangParserSpec extends Specification {
1 * mockYangTextSchemaSourceSetCache.removeFromCache('my dataspace', 'my schema')
}
+ def 'Parsing data with yang resource to context map.'() {
+ given: 'the schema source set for the yang resource map is returned'
+ mockTimedYangTextSchemaSourceSetBuilder.getYangTextSchemaSourceSet(yangResourcesNameToContentMap) >> mockYangTextSchemaSourceSet
+ when: 'parsing some json data'
+ def result = objectUnderTest.parseData(ContentType.JSON, 'some json', yangResourcesNameToContentMap, noParent)
+ then: 'the yang parser helper always returns a container node'
+ 1 * mockYangParserHelper.parseData(ContentType.JSON, 'some json', mockSchemaContext, noParent) >> containerNodeFromYangUtils
+ and: 'the result is the same container node as return from yang utils'
+ assert result == containerNodeFromYangUtils
+ }
+
}
diff --git a/docs/release-notes.rst b/docs/release-notes.rst
index 82a890d79c..b9df799c9e 100644
--- a/docs/release-notes.rst
+++ b/docs/release-notes.rst
@@ -70,6 +70,8 @@ Bug Fixes
Features
--------
+3.4.9
+ - `CPS-1836 <https://jira.onap.org/browse/CPS-1836>`_ Delta between anchor and JSON payload.
Version: 3.4.8
==============
diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsDataServiceIntegrationSpec.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsDataServiceIntegrationSpec.groovy
index 897d4aeb5e..779c0b84c4 100644
--- a/integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsDataServiceIntegrationSpec.groovy
+++ b/integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsDataServiceIntegrationSpec.groovy
@@ -1,7 +1,7 @@
/*
* ============LICENSE_START=======================================================
* Copyright (C) 2023-2024 Nordix Foundation
- * Modifications Copyright (C) 2024 TechMahindra Ltd.
+ * Modifications Copyright (C) 2023-2024 TechMahindra Ltd.
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
@@ -456,7 +456,7 @@ class CpsDataServiceIntegrationSpec extends FunctionalSpecBase {
restoreBookstoreDataAnchor(2)
}
- def 'Get delta between 2 anchors for when #scenario'() {
+ 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)
then: 'delta report contains expected number of changes'
@@ -585,6 +585,46 @@ class CpsDataServiceIntegrationSpec extends FunctionalSpecBase {
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 UPDATE action with expected xpath'
+ assert result[0].getAction() == 'update'
+ 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 ADD action with expected xpath'
+ assert result[2].getAction() == 'add'
+ 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 = []