description: multipartFile
format: binary
+ TargetDataAsJsonFile:
+ type: object
+ required:
+ - file
+ properties:
+ file:
+ type: string
+ description: multipartFile
+ format: binary
+
ModuleReferences:
type: object
title: Module reference object
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
+ targetDataAsJsonFile:
+ type: string
+ format: binary
+ example:
+ $ref: 'components.yml#/components/schemas/TargetDataAsJsonFile'
+ yangResourceFile:
+ type: string
+ format: binary
+ example:
+ $ref: 'components.yml#/components/schemas/MultipartFile'
required:
- - json
+ - targetDataAsJsonFile
responses:
'200':
description: OK
import org.onap.cps.api.model.DeltaReport;
import org.onap.cps.api.parameters.FetchDescendantsOption;
import org.onap.cps.rest.api.CpsDeltaApi;
+import org.onap.cps.rest.utils.MultipartFileUtil;
import org.onap.cps.utils.JsonObjectMapper;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@Override
public ResponseEntity<Object> getDeltaByDataspaceAnchorAndPayload(final String dataspaceName,
final String sourceAnchorName,
- final Object jsonPayload,
+ final MultipartFile targetDataAsJsonFile,
final String xpath,
- final MultipartFile multipartFile) {
+ final MultipartFile yangResourceFile) {
final FetchDescendantsOption fetchDescendantsOption = FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS;
-
+ final String targetData = MultipartFileUtil.extractJsonContent(targetDataAsJsonFile, jsonObjectMapper);
final Map<String, String> yangResourceMap;
- if (multipartFile == null) {
+ if (yangResourceFile == null) {
yangResourceMap = Collections.emptyMap();
} else {
- yangResourceMap = extractYangResourcesMap(multipartFile);
+ yangResourceMap = extractYangResourcesMap(yangResourceFile);
}
final Collection<DeltaReport> deltaReports = Collections.unmodifiableList(
cpsDeltaService.getDeltaByDataspaceAnchorAndPayload(dataspaceName, sourceAnchorName,
- xpath, yangResourceMap, jsonPayload.toString(), fetchDescendantsOption));
+ xpath, yangResourceMap, targetData, fetchDescendantsOption));
return new ResponseEntity<>(jsonObjectMapper.asJsonString(deltaReports), HttpStatus.OK);
}
-
}
* ============LICENSE_START=======================================================
* Copyright (C) 2020 Pantheon.tech
* Modifications Copyright (C) 2021-2023 Nordix Foundation
- * Modifications Copyright (C) 2022 TechMahindra Ltd.
+ * Modifications Copyright (C) 2022-2025 TechMahindra Ltd.
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
import org.onap.cps.api.exceptions.NotFoundInDataspaceException;
import org.onap.cps.rest.controller.AdminRestController;
import org.onap.cps.rest.controller.DataRestController;
+import org.onap.cps.rest.controller.DeltaRestController;
import org.onap.cps.rest.controller.QueryRestController;
import org.onap.cps.rest.model.ErrorMessage;
import org.springframework.http.HttpMethod;
@Slf4j
@RestControllerAdvice(assignableTypes = {AdminRestController.class, DataRestController.class,
- QueryRestController.class})
+ DeltaRestController.class, QueryRestController.class})
@NoArgsConstructor(access = AccessLevel.PACKAGE)
public class CpsRestExceptionHandler {
* Copyright (C) 2020 Pantheon.tech
* Modifications Copyright (C) 2021 Bell Canada.
* Modifications Copyright (C) 2023 Nordix Foundation.
+ * Modifications Copyright (C) 2025 TechMahindra Ltd.
* ================================================================================
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
import static org.opendaylight.yangtools.yang.common.YangConstants.RFC6020_YANG_FILE_EXTENSION;
+import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.collect.ImmutableMap;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.onap.cps.api.exceptions.CpsException;
+import org.onap.cps.api.exceptions.DataValidationException;
import org.onap.cps.api.exceptions.ModelValidationException;
+import org.onap.cps.utils.JsonObjectMapper;
import org.springframework.web.multipart.MultipartFile;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
Arrays.asList(YANG_FILE_EXTENSION, ZIP_FILE_EXTENSION)));
}
+ /**
+ * Extracts json content from multipart file instance.
+ *
+ * @param multipartFile the json file uploaded
+ * @return the string representation of the JSON content
+ * @throws IOException if the file is null or empty
+ */
+
+ public static String extractJsonContent(final MultipartFile multipartFile, final JsonObjectMapper
+ jsonObjectMapper) {
+ try {
+ if (multipartFile.isEmpty()) {
+ throw new IOException("JSON file is required");
+ }
+ final String jsonContent = new String(multipartFile.getBytes(), StandardCharsets.UTF_8);
+ final JsonNode jsonNode = jsonObjectMapper.convertToJsonNode(jsonContent);
+ return jsonNode.toString();
+ } catch (final IOException exception) {
+ throw new DataValidationException("Failed to read JSON file", exception.getMessage());
+ }
+ }
+
private static Map<String, String> extractYangResourcesMapFromZipArchive(final MultipartFile multipartFile) {
final ImmutableMap.Builder<String, String> yangResourceMapBuilder = ImmutableMap.builder();
final var zipFileSizeValidator = new ZipFileSizeValidator();
try (
final var inputStream = multipartFile.getInputStream();
- final var zipInputStream = new ZipInputStream(inputStream);
+ final var zipInputStream = new ZipInputStream(inputStream)
) {
ZipEntry zipEntry;
while ((zipEntry = zipInputStream.getNextEntry()) != null) {
import org.springframework.web.multipart.MultipartFile
import spock.lang.Shared
import spock.lang.Specification
+import java.nio.charset.StandardCharsets
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.StandardOpenOption
import static org.onap.cps.api.parameters.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
import static org.onap.cps.api.parameters.FetchDescendantsOption.OMIT_DESCENDANTS
@Shared
def expectedJsonData = '{"some-key":"some-value","categories":[{"books":[{"authors":["Iain M. Banks"]}]}]}'
@Shared
- static MultipartFile multipartYangFile = new MockMultipartFile('file', 'filename.yang', 'text/plain', 'content'.getBytes())
+ static MultipartFile multipartYangFile = new MockMultipartFile('yangResourceFile', 'filename.yang', 'text/plain', 'content'.getBytes())
+ @Shared
+ Path targetDataAsJsonFile
+ @Shared
+ MockMultipartFile multipartTargetDataAsJsonFile
def setup() {
dataNodeBaseEndpointV2 = "$basePath/v2/dataspaces/$dataspaceName/anchors/$anchorName/delta"
+ targetDataAsJsonFile = Files.createTempFile('requestBody', '.json')
+ Files.write(targetDataAsJsonFile, requestBodyJson.getBytes(StandardCharsets.UTF_8), StandardOpenOption.WRITE)
+ multipartTargetDataAsJsonFile = new MockMultipartFile('targetDataAsJsonFile', targetDataAsJsonFile.fileName.toString(), 'application/json', Files.readAllBytes(targetDataAsJsonFile))
+ }
+
+ def cleanup() {
+ Files.deleteIfExists(targetDataAsJsonFile)
}
def 'Get delta between two anchors'() {
assert response.contentAsString.contains('[{\"action\":\"replace\",\"xpath\":\"some xpath\",\"sourceData\":{\"some key\":\"some value\"},\"targetData\":{\"some key\":\"some value\"}}]')
}
- def 'Get delta between anchor and JSON payload with multipart file'() {
+ def 'Get delta between anchor and JSON payload with yangResourceFile'() {
given: 'sample delta report, xpath, yang model file and json payload'
def deltaReports = new DeltaReportBuilder().actionCreate().withXpath('some xpath').build()
def xpath = 'some xpath'
def response =
mvc.perform(multipart(dataNodeBaseEndpointV2)
.file(multipartYangFile)
- .param('json', requestBodyJson)
+ .file(multipartTargetDataAsJsonFile)
.param('xpath', xpath)
.contentType(MediaType.MULTIPART_FORM_DATA))
.andReturn().response
assert response.contentAsString.contains('[{\"action\":\"create\",\"xpath\":\"some xpath\"}]')
}
- def 'Get delta between anchor and JSON payload without multipart file'() {
+ def 'Get delta between anchor and JSON payload without yangResourceFile'() {
given: 'sample delta report, xpath, and json payload'
def deltaReports = new DeltaReportBuilder().actionRemove().withXpath('some xpath').build()
def xpath = 'some xpath'
when: 'get delta request is performed using REST API'
def response =
mvc.perform(multipart(dataNodeBaseEndpointV2)
- .param('json', requestBodyJson)
+ .file(multipartTargetDataAsJsonFile)
.param('xpath', xpath)
.contentType(MediaType.MULTIPART_FORM_DATA))
.andReturn().response
and: 'the response contains expected value'
assert response.contentAsString.contains('[{\"action\":\"remove\",\"xpath\":\"some xpath\"}]')
}
+
+ def 'Attempt to get delta between anchor and JSON payload with an Empty File'() {
+ given: 'xpath, yang model file and empty json payload'
+ def xpath = 'some xpath'
+ def emptyTargetDataAsJsonFile = new MockMultipartFile('targetDataAsJsonFile', 'empty.json', 'application/json', new byte[0])
+ when: 'get delta request is performed using REST API'
+ def response = mvc.perform(multipart(dataNodeBaseEndpointV2)
+ .file(emptyTargetDataAsJsonFile)
+ .param('xpath', xpath)
+ .contentType(MediaType.MULTIPART_FORM_DATA))
+ .andReturn()
+ .response
+ then: 'expected response code is returned'
+ assert response.status == HttpStatus.BAD_REQUEST.value()
+ then: 'the response contains expected error message'
+ assert response.contentAsString.contains("JSON file is required")
+ }
+
+ def 'Get delta between anchor and JSON payload with an invalid data'() {
+ given: 'xpath, yang model file and empty json payload'
+ def xpath = 'some xpath'
+ def invalidJsonContent = '{'
+ def invalidTargetDataAsJsonFile = new MockMultipartFile('targetDataAsJsonFile', 'invalid.json', 'application/json', invalidJsonContent.getBytes())
+ when: 'get delta request is performed using REST API'
+ def response = mvc.perform(multipart(dataNodeBaseEndpointV2)
+ .file(invalidTargetDataAsJsonFile)
+ .param('xpath', xpath)
+ .contentType(MediaType.MULTIPART_FORM_DATA))
+ .andReturn()
+ .response
+ then: 'expected response code is returned'
+ assert response.status == HttpStatus.BAD_REQUEST.value()
+ then: 'the response contains expected error message'
+ assert response.contentAsString.contains("Parsing error occurred while converting JSON content to Json Node")
+ }
}
name: cps-admin
- description: cps Data
name: cps-data
+- description: CPS Delta
+ name: cps-delta
paths:
/v1/dataspaces:
post:
description: Internal Server Error
summary: Get delta between anchors in the same dataspace
tags:
- - cps-data
+ - cps-delta
x-codegen-request-body-name: xpath
post:
description: Get delta between an anchor in a dataspace and JSON payload
description: Internal Server Error
summary: Get delta between an anchor and JSON payload
tags:
- - cps-data
+ - cps-delta
/v1/dataspaces/{dataspace-name}/anchors/{anchor-name}/nodes/query:
get:
deprecated: true
NotificationSubscriptionsDataSample: {}
getDeltaByDataspaceAnchorAndPayload_request:
properties:
- json:
- example:
- test:bookstore:
- bookstore-name: Chapters
- categories:
- - code: 1
- name: SciFi
- - code: 2
- name: kids
- type: object
- file:
+ targetDataAsJsonFile:
+ example: "{$ref=components.yml#/components/schemas/TargetDataAsJsonFile}"
+ format: binary
+ type: string
+ yangResourceFile:
+ example: "{$ref=components.yml#/components/schemas/MultipartFile}"
format: binary
type: string
required:
- - json
+ - targetDataAsJsonFile
type: object
securitySchemes:
basicAuth:
- description: |
The `resourceIdentifier` parameter specifies the target resource in the GNBDUFunctionConfig model.
For ONAP DMI Plugin, the format will follow RESTConf paths. Examples:
- - All GNBDUFunctions: `'/ManagedElement=NRNode1/GNBDUFunction=1'`
+ - All GNBDUFunctions: `/ManagedElement=node1/GNBDUFunction=1`
examples:
sample 1:
value:
- resourceIdentifier: '/ManagedElement=NRNode1/GNBDUFunction=1'
+ resourceIdentifier: /ManagedElement=node1/GNBDUFunction=1
in: query
name: resourceIdentifier
required: true
- description: |
The `resourceIdentifier` parameter specifies the target resource in the GNBDUFunctionConfig model.
For ONAP DMI Plugin, the format will follow RESTConf paths. Examples:
- - All GNBDUFunctions: `/ManagedElement=NRNode1/GNBDUFunction=1`
+ - All GNBDUFunctions: `/ManagedElement=node1/GNBDUFunction=1`
examples:
sample 1:
value:
- resourceIdentifier: '/ManagedElement=NRNode1/GNBDUFunction=1'
+ resourceIdentifier: /ManagedElement=node1/GNBDUFunction=1
in: query
name: resourceIdentifier
required: true
The `options` parameter specifies additional query options. It is mandatory to wrap key(s)=value(s) in parentheses `()`.
Examples for GNBDUFunctionConfig queries:
- Limit depth of returned sub-tree: `(depth=2)`
- - Select specific fields: `(fields=id,gNBDUName)`
- - Combine options: `(depth=3,fields=id,gNBDUName)`
+ - Select specific fields: `(fields=attributes(gNBId;gNBDUName))`
+ - Combine options: `(depth=3,fields=attributes(gNBId;gNBDUName))`
examples:
Limit Depth:
value:
options: (depth=2)
Select Specific Fields:
value:
- options: "(fields=id,gNBDUName)"
+ options: (fields=attributes(gNBId;gNBDUName))
Combine Depth and Fields:
value:
- options: "(depth=3,fields=id,gNBDUName)"
+ options: "(depth=3,fields=attributes(gNBId;gNBDUName))"
in: query
name: options
required: false
- description: |
The `resourceIdentifier` parameter specifies the target resource in the GNBDUFunctionConfig model.
For ONAP DMI Plugin, the format will follow RESTConf paths. Examples:
- - All GNBDUFunctions: `/ManagedElement=NRNode1/GNBDUFunction=1`
+ - All GNBDUFunctions: `/ManagedElement=node1/GNBDUFunction=1`
examples:
sample 1:
value:
- resourceIdentifier: '/ManagedElement=NRNode1/GNBDUFunction=1'
+ resourceIdentifier: /ManagedElement=node1/GNBDUFunction=1
in: query
name: resourceIdentifier
required: true
- description: |
The `resourceIdentifier` parameter specifies the target resource in the GNBDUFunctionConfig model.
For ONAP DMI Plugin, the format will follow RESTConf paths. Examples:
- - All GNBDUFunctions: `/ManagedElement=NRNode1/GNBDUFunction=1`
+ - All GNBDUFunctions: `/ManagedElement=node1/GNBDUFunction=1`
examples:
sample 1:
value:
- resourceIdentifier: '/ManagedElement=NRNode1/GNBDUFunction=1'
+ resourceIdentifier: /ManagedElement=node1/GNBDUFunction=1
in: query
name: resourceIdentifier
required: true
- description: |
The `resourceIdentifier` parameter specifies the target resource in the GNBDUFunctionConfig model.
For ONAP DMI Plugin, the format will follow RESTConf paths. Examples:
- - All GNBDUFunctions: `/ManagedElement=NRNode1/GNBDUFunction=1`
+ - All GNBDUFunctions: `/ManagedElement=node1/GNBDUFunction=1`
examples:
sample 1:
value:
- resourceIdentifier: '/ManagedElement=NRNode1/GNBDUFunction=1'
+ resourceIdentifier: /ManagedElement=node1/GNBDUFunction=1
in: query
name: resourceIdentifier
required: true
schema:
example: my-cm-handle
type: string
- - description: For more details on cps path, please refer https://docs.onap.org/projects/onap-cps/en/latest/cps-path.html
+ - description: "For more details on cps path, please refer https://docs.onap.org/projects/onap-cps/en/latest/cps-path.html"
examples:
container cps path:
- value: '/GNBDUFunction'
+ value: //GNBDUFunction
list attributes cps path:
value: "//GNBDUFunction[@id='1001']"
in: query
options: (depth=2)
Select Specific Fields:
value:
- options: "(fields=attributes(gNBId;gNBDUName))"
+ options: (fields=attributes(gNBId;gNBDUName))
Combine Depth and Fields:
value:
options: "(depth=3,fields=attributes(gNBId;gNBDUName))"
description: |
The `resourceIdentifier` parameter specifies the target resource in the GNBDUFunctionConfig model.
For ONAP DMI Plugin, the format will follow RESTConf paths. Examples:
- - All GNBDUFunctions: `/ManagedElement=NRNode1/GNBDUFunction=1`
+ - All GNBDUFunctions: `/ManagedElement=node1/GNBDUFunction=1`
examples:
sample 1:
value:
- resourceIdentifier: '/ManagedElement=NRNode1/GNBDUFunction=1'
+ resourceIdentifier: /ManagedElement=node1/GNBDUFunction=1
in: query
name: resourceIdentifier
required: true
options: (depth=2)
Select Specific Fields:
value:
- options: "(fields=attributes(gNBId;gNBDUName))"
+ options: (fields=attributes(gNBId;gNBDUName))
Combine Depth and Fields:
value:
options: "(depth=3,fields=attributes(gNBId;gNBDUName))"
example: my-cm-handle
type: string
cpsPathInQuery:
- description: |
- The `cps-path` parameter allows referencing elements in the GNBDUFunctionConfig data model.
- For more details on cps path, please refer to:
- [CPS Path Documentation](https://docs.onap.org/projects/onap-cps/en/latest/cps-path.html).
- Example paths:
- - Root GNBDUFunction: `/GNBDUFunction`
- - Specific gNB ID: `//GNBDUFunction[@id='1001']`
- - RIM-RS Reporting Config: `//GNBDUFunction[@id='1001']/rimRSReportConf`
+ description: "For more details on cps path, please refer https://docs.onap.org/projects/onap-cps/en/latest/cps-path.html"
examples:
- GNBDUFunction Root:
- value: /GNBDUFunction
- Specific gNB ID:
+ container cps path:
+ value: //GNBDUFunction
+ list attributes cps path:
value: "//GNBDUFunction[@id='1001']"
- RIM-RS Reporting Config:
- value: "//GNBDUFunction[@id='1001']/rimRSReportConf"
in: query
name: cps-path
required: false
DataOperationRequest:
example:
operations:
- - resourceIdentifier: '/ManagedElement=NRNode1/GNBDUFunction=1'
+ - resourceIdentifier: /ManagedElement=NRNode1/GNBDUFunction=1
targetIds:
- "[\"da310eecdb8d44c2acc0ddaae01174b1\",\"c748c58f8e0b438f9fd1f28370b17d47\"\
]"
options: (fields=NRCellDU/attributes/cellLocalId)
operationId: "12"
operation: read
- - resourceIdentifier: '/ManagedElement=NRNode1/GNBDUFunction=1'
+ - resourceIdentifier: /ManagedElement=NRNode1/GNBDUFunction=1
targetIds:
- "[\"da310eecdb8d44c2acc0ddaae01174b1\",\"c748c58f8e0b438f9fd1f28370b17d47\"\
]"
type: object
DataOperationDefinition:
example:
- resourceIdentifier: '/ManagedElement=NRNode1/GNBDUFunction=1'
+ resourceIdentifier: /ManagedElement=NRNode1/GNBDUFunction=1
targetIds:
- "[\"da310eecdb8d44c2acc0ddaae01174b1\",\"c748c58f8e0b438f9fd1f28370b17d47\"\
]"
example: (fields=NRCellDU/attributes/cellLocalId)
type: string
resourceIdentifier:
- example: '/ManagedElement=NRNode1/GNBDUFunction=1'
+ example: /ManagedElement=NRNode1/GNBDUFunction=1
type: string
targetIds:
items: