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
diff --git a/cps-rest/docs/openapi/cpsDataV2.yml b/cps-rest/docs/openapi/cpsDataV2.yml
index cbb5ce4..a1433ba 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 @@
$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 f29335a..b4e0b70 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 @@
/{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 310171b..f579c82 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.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 @@
}
@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 3f5dcf2..317b9c5 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.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.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 @@
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 @@
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 @@
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 @@
'without observed timestamp' | null || 1 | HttpStatus.NO_CONTENT
'with invalid observed timestamp' | 'invalid' || 0 | HttpStatus.BAD_REQUEST
}
+
}