CPS Delta API 2: Delta between anchor and payload 59/135859/53
authorArpit Singh <as00745003@techmahindra.com>
Thu, 7 Sep 2023 11:35:37 +0000 (17:05 +0530)
committerArpit Singh <as00745003@techmahindra.com>
Thu, 6 Jun 2024 08:43:32 +0000 (14:13 +0530)
  - 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

13 files changed:
cps-rest/docs/openapi/cpsDataV2.yml
cps-rest/docs/openapi/openapi.yml
cps-rest/src/main/java/org/onap/cps/rest/controller/DataRestController.java
cps-rest/src/test/groovy/org/onap/cps/rest/controller/DataRestControllerSpec.groovy
cps-service/src/main/java/org/onap/cps/api/CpsDataService.java
cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java
cps-service/src/main/java/org/onap/cps/spi/model/DeltaReport.java
cps-service/src/main/java/org/onap/cps/utils/YangParser.java
cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy
cps-service/src/test/groovy/org/onap/cps/api/impl/E2ENetworkSliceSpec.groovy
cps-service/src/test/groovy/org/onap/cps/utils/YangParserSpec.groovy
docs/release-notes.rst
integration-test/src/test/groovy/org/onap/cps/integration/functional/CpsDataServiceIntegrationSpec.groovy

index cbb5ce4..a1433ba 100644 (file)
@@ -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
index f29335a..b4e0b70 100644 (file)
@@ -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'
 
index 310171b..f579c82 100755 (executable)
 
 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}")
@@ -171,6 +175,27 @@ public class DataRestController implements CpsDataApi {
         return new ResponseEntity<>(HttpStatus.NO_CONTENT);
     }
 
+    @Override
+    public ResponseEntity<Object> getDeltaByDataspaceAnchorAndPayload(final String dataspaceName,
+                                                                      final String sourceAnchorName,
+                                                                      final Object jsonPayload,
+                                                                      final String xpath,
+                                                                      final MultipartFile multipartFile) {
+        final FetchDescendantsOption fetchDescendantsOption = FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS;
+
+        final Map<String, String> yangResourceMap;
+        if (multipartFile == null) {
+            yangResourceMap = Collections.emptyMap();
+        } else {
+            yangResourceMap = extractYangResourcesMap(multipartFile);
+        }
+        final Collection<DeltaReport> deltaReports = Collections.unmodifiableList(
+                cpsDataService.getDeltaByDataspaceAnchorAndPayload(dataspaceName, sourceAnchorName,
+                xpath, yangResourceMap, jsonPayload.toString(), fetchDescendantsOption));
+
+        return new ResponseEntity<>(jsonObjectMapper.asJsonString(deltaReports), HttpStatus.OK);
+    }
+
     @Override
     @Timed(value = "cps.data.controller.get.delta",
             description = "Time taken to get delta between anchors")
index 3f5dcf2..317b9c5 100755 (executable)
@@ -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
     }
+
 }
index 71ed061..f396b49 100644 (file)
@@ -4,7 +4,7 @@
  *  Modifications Copyright (C) 2021 Pantheon.tech
  *  Modifications Copyright (C) 2021-2022 Bell Canada
  *  Modifications Copyright (C) 2022 Deutsche Telekom AG
- *  Modifications Copyright (C) 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);
 }
index 3496fc7..6386d38 100644 (file)
@@ -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);
index fb9c197..34715e7 100644 (file)
@@ -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";
index 6299ef3..dc23c6b 100644 (file)
@@ -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.
 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());
     }
index 4542ecb..edf2571 100644 (file)
@@ -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
+    }
+
 }
index 57f2f8e..9e55e8f 100755 (executable)
@@ -23,6 +23,7 @@
 \r
 package org.onap.cps.api.impl\r
 \r
+import com.fasterxml.jackson.databind.ObjectMapper\r
 import org.onap.cps.TestUtils\r
 import org.onap.cps.api.CpsAnchorService\r
 import org.onap.cps.api.CpsDeltaService\r
@@ -31,6 +32,8 @@ import org.onap.cps.spi.CpsDataPersistenceService
 import org.onap.cps.spi.CpsModulePersistenceService\r
 import org.onap.cps.spi.model.Anchor\r
 import org.onap.cps.spi.utils.CpsValidator\r
+import org.onap.cps.utils.JsonObjectMapper\r
+import org.onap.cps.utils.PrefixResolver\r
 import org.onap.cps.utils.ContentType\r
 import org.onap.cps.utils.YangParser\r
 import org.onap.cps.utils.YangParserHelper\r
@@ -45,15 +48,17 @@ class E2ENetworkSliceSpec extends Specification {
     def mockYangTextSchemaSourceSetCache = Mock(YangTextSchemaSourceSetCache)\r
     def mockCpsValidator = Mock(CpsValidator)\r
     def timedYangTextSchemaSourceSetBuilder = new TimedYangTextSchemaSourceSetBuilder()\r
-    def yangParser = new YangParser(new YangParserHelper(), mockYangTextSchemaSourceSetCache)\r
+    def yangParser = new YangParser(new YangParserHelper(), mockYangTextSchemaSourceSetCache, timedYangTextSchemaSourceSetBuilder)\r
     def mockCpsDeltaService = Mock(CpsDeltaService)\r
+    def jsonObjectMapper = new JsonObjectMapper(new ObjectMapper())\r
+    def mockPrefixResolver = Mock(PrefixResolver)\r
 \r
     def cpsModuleServiceImpl = new CpsModuleServiceImpl(mockModuleStoreService,\r
             mockYangTextSchemaSourceSetCache, mockCpsAnchorService, mockCpsValidator,timedYangTextSchemaSourceSetBuilder)\r
 \r
     def mockDataUpdateEventsService = Mock(CpsDataUpdateEventsService)\r
-    def cpsDataServiceImpl = new CpsDataServiceImpl(mockDataStoreService, mockDataUpdateEventsService, mockCpsAnchorService, mockCpsValidator, yangParser, mockCpsDeltaService)\r
-\r
+    def cpsDataServiceImpl = new CpsDataServiceImpl(mockDataStoreService, mockDataUpdateEventsService, mockCpsAnchorService, mockCpsValidator,\r
+            yangParser, mockCpsDeltaService, jsonObjectMapper, mockPrefixResolver)\r
     def dataspaceName = 'someDataspace'\r
     def anchorName = 'someAnchor'\r
     def schemaSetName = 'someSchemaSet'\r
index 99070fe..18d0502 100644 (file)
@@ -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.
 
 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
+    }
+
 }
index 82a890d..b9df799 100644 (file)
@@ -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
 ==============
index 897d4ae..779c0b8 100644 (file)
@@ -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 = []