summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--cps-rest/docs/openapi/cpsQueryV2.yml10
-rw-r--r--cps-rest/src/main/java/org/onap/cps/rest/controller/QueryRestController.java28
-rw-r--r--cps-rest/src/test/groovy/org/onap/cps/rest/controller/QueryRestControllerSpec.groovy55
-rw-r--r--cps-service/src/main/java/org/onap/cps/utils/XmlFileUtils.java46
-rw-r--r--cps-service/src/test/groovy/org/onap/cps/utils/XmlFileUtilsSpec.groovy34
-rw-r--r--docs/api/swagger/cps/openapi.yaml18
6 files changed, 133 insertions, 58 deletions
diff --git a/cps-rest/docs/openapi/cpsQueryV2.yml b/cps-rest/docs/openapi/cpsQueryV2.yml
index 7f0ceff768..9aaa4193c3 100644
--- a/cps-rest/docs/openapi/cpsQueryV2.yml
+++ b/cps-rest/docs/openapi/cpsQueryV2.yml
@@ -1,5 +1,6 @@
# ============LICENSE_START=======================================================
# Copyright (C) 2023 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.
@@ -28,6 +29,7 @@ nodesByDataspaceAndAnchorAndCpsPath:
- $ref: 'components.yml#/components/parameters/anchorNameInPath'
- $ref: 'components.yml#/components/parameters/cpsPathInQuery'
- $ref: 'components.yml#/components/parameters/descendantsInQuery'
+ - $ref: 'components.yml#/components/parameters/contentTypeInHeader'
responses:
'200':
description: OK
@@ -38,6 +40,14 @@ nodesByDataspaceAndAnchorAndCpsPath:
examples:
dataSample:
$ref: 'components.yml#/components/examples/dataSample'
+ application/xml:
+ schema:
+ type: object
+ xml:
+ name: stores
+ examples:
+ dataSample:
+ $ref: 'components.yml#/components/examples/dataSampleXml'
'400':
$ref: 'components.yml#/components/responses/BadRequest'
'403':
diff --git a/cps-rest/src/main/java/org/onap/cps/rest/controller/QueryRestController.java b/cps-rest/src/main/java/org/onap/cps/rest/controller/QueryRestController.java
index 547be669ae..6823f6b03e 100644
--- a/cps-rest/src/main/java/org/onap/cps/rest/controller/QueryRestController.java
+++ b/cps-rest/src/main/java/org/onap/cps/rest/controller/QueryRestController.java
@@ -2,7 +2,7 @@
* ============LICENSE_START=======================================================
* Copyright (C) 2021-2024 Nordix Foundation
* Modifications Copyright (C) 2022 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.
@@ -36,9 +36,11 @@ import org.onap.cps.spi.FetchDescendantsOption;
import org.onap.cps.spi.PaginationOption;
import org.onap.cps.spi.model.Anchor;
import org.onap.cps.spi.model.DataNode;
+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.XmlFileUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -62,18 +64,20 @@ public class QueryRestController implements CpsQueryApi {
final FetchDescendantsOption fetchDescendantsOption = Boolean.TRUE.equals(includeDescendants)
? FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS : FetchDescendantsOption.OMIT_DESCENDANTS;
return executeNodesByDataspaceQueryAndCreateResponse(dataspaceName, anchorName, cpsPath,
- fetchDescendantsOption);
+ fetchDescendantsOption, ContentType.JSON);
}
@Override
@Timed(value = "cps.data.controller.datanode.query.v2",
description = "Time taken to query data nodes")
public ResponseEntity<Object> getNodesByDataspaceAndAnchorAndCpsPathV2(final String dataspaceName,
- final String anchorName, final String cpsPath, final String fetchDescendantsOptionAsString) {
+ final String anchorName, final String contentTypeInHeader, final String cpsPath,
+ final String fetchDescendantsOptionAsString) {
+ final ContentType contentType = ContentType.fromString(contentTypeInHeader);
final FetchDescendantsOption fetchDescendantsOption =
FetchDescendantsOption.getFetchDescendantsOption(fetchDescendantsOptionAsString);
return executeNodesByDataspaceQueryAndCreateResponse(dataspaceName, anchorName, cpsPath,
- fetchDescendantsOption);
+ fetchDescendantsOption, contentType);
}
@Override
@@ -130,7 +134,8 @@ public class QueryRestController implements CpsQueryApi {
}
private ResponseEntity<Object> executeNodesByDataspaceQueryAndCreateResponse(final String dataspaceName,
- final String anchorName, final String cpsPath, final FetchDescendantsOption fetchDescendantsOption) {
+ final String anchorName, final String cpsPath, final FetchDescendantsOption fetchDescendantsOption,
+ final ContentType contentType) {
final Collection<DataNode> dataNodes =
cpsQueryService.queryDataNodes(dataspaceName, anchorName, cpsPath, fetchDescendantsOption);
final List<Map<String, Object>> dataNodesAsListOfMaps = new ArrayList<>(dataNodes.size());
@@ -143,6 +148,17 @@ public class QueryRestController implements CpsQueryApi {
final Map<String, Object> dataMap = DataMapUtils.toDataMapWithIdentifier(dataNode, prefix);
dataNodesAsListOfMaps.add(dataMap);
}
- return new ResponseEntity<>(jsonObjectMapper.asJsonString(dataNodesAsListOfMaps), HttpStatus.OK);
+ return buildResponseEntity(dataNodesAsListOfMaps, contentType);
+ }
+
+ private ResponseEntity<Object> buildResponseEntity(final List<Map<String, Object>> dataNodesAsListOfMaps,
+ final ContentType contentType) {
+ final String responseData;
+ if (contentType == ContentType.XML) {
+ responseData = XmlFileUtils.convertDataMapsToXml(dataNodesAsListOfMaps);
+ } else {
+ responseData = jsonObjectMapper.asJsonString(dataNodesAsListOfMaps);
+ }
+ return new ResponseEntity<>(responseData, HttpStatus.OK);
}
}
diff --git a/cps-rest/src/test/groovy/org/onap/cps/rest/controller/QueryRestControllerSpec.groovy b/cps-rest/src/test/groovy/org/onap/cps/rest/controller/QueryRestControllerSpec.groovy
index 80b287cda8..076ab32454 100644
--- a/cps-rest/src/test/groovy/org/onap/cps/rest/controller/QueryRestControllerSpec.groovy
+++ b/cps-rest/src/test/groovy/org/onap/cps/rest/controller/QueryRestControllerSpec.groovy
@@ -3,7 +3,7 @@
* Copyright (C) 2021-2024 Nordix Foundation
* Modifications Copyright (C) 2021-2022 Bell Canada.
* Modifications Copyright (C) 2021 Pantheon.tech
- * 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.
@@ -35,6 +35,7 @@ import org.springframework.beans.factory.annotation.Autowired
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.test.web.servlet.MockMvc
import spock.lang.Specification
@@ -97,26 +98,52 @@ class QueryRestControllerSpec extends Specification {
'descendants' | 'true' || INCLUDE_ALL_DESCENDANTS
}
- def 'Query data node v2 api by cps path for the given dataspace and anchor with #scenario.'() {
+ def 'Query data node v2 API by cps path for the given dataspace and anchor with #scenario and media type JSON'() {
given: 'service method returns a list containing a data node'
- def dataNode1 = new DataNodeBuilder().withXpath('/xpath')
+ def dataNode = new DataNodeBuilder().withXpath('/xpath')
.withLeaves([leaf: 'value', leafList: ['leaveListElement1', 'leaveListElement2']]).build()
- mockCpsQueryService.queryDataNodes(dataspaceName, anchorName, cpsPath, { descendantsOption -> {
- assert descendantsOption.depth == expectedDepth}}) >> [dataNode1, dataNode1]
+ mockCpsQueryService.queryDataNodes(dataspaceName, anchorName, cpsPath, { descendantsOption ->
+ assert descendantsOption.depth == expectedDepth
+ }) >> [dataNode, dataNode]
when: 'query data nodes API is invoked'
def response =
mvc.perform(
- get(dataNodeEndpointV2)
- .param('cps-path', cpsPath)
- .param('descendants', includeDescendantsOptionString))
- .andReturn().response
- then: 'the response contains the the datanode in json format'
+ get(dataNodeEndpointV2)
+ .contentType(MediaType.APPLICATION_JSON)
+ .param('cps-path', cpsPath)
+ .param('descendants', includeDescendantsOptionString))
+ .andReturn().response
+ then: 'the response contains the datanode in the expected JSON format'
assert response.status == HttpStatus.OK.value()
assert response.getContentAsString().contains('{"xpath":{"leaf":"value","leafList":["leaveListElement1","leaveListElement2"]}}')
- where: 'the following options for include descendants are provided in the request'
- scenario | includeDescendantsOptionString || expectedDepth
- 'direct children' | 'direct' || 1
- 'descendants' | '2' || 2
+ where: 'the following options for include descendants are provided in the request'
+ scenario | includeDescendantsOptionString || expectedDepth
+ 'direct children' | 'direct' || 1
+ 'descendants' | '2' || 2
+ }
+
+ def 'Query data node v2 API by cps path for the given dataspace and anchor with #scenario and media type XML'() {
+ given: 'service method returns a list containing a data node'
+ def dataNode = new DataNodeBuilder().withXpath('/xpath')
+ .withLeaves([leaf: 'value', leafList: ['leaveListElement1', 'leaveListElement2']]).build()
+ mockCpsQueryService.queryDataNodes(dataspaceName, anchorName, cpsPath, { descendantsOption ->
+ assert descendantsOption.depth == expectedDepth
+ }) >> [dataNode, dataNode]
+ when: 'query data nodes API is invoked'
+ def response =
+ mvc.perform(
+ get(dataNodeEndpointV2)
+ .contentType(MediaType.APPLICATION_XML)
+ .param('cps-path', cpsPath)
+ .param('descendants', includeDescendantsOptionString))
+ .andReturn().response
+ then: 'the response contains the datanode in the expected XML format'
+ assert response.status == HttpStatus.OK.value()
+ assert response.getContentAsString().contains('<xpath><leaf>value</leaf><leafList>leaveListElement1</leafList><leafList>leaveListElement2</leafList></xpath>')
+ where: 'the following options for include descendants are provided in the request'
+ scenario | includeDescendantsOptionString || expectedDepth
+ 'direct children' | 'direct' || 1
+ 'descendants' | '2' || 2
}
def 'Query data node by cps path for the given dataspace across all anchors with #scenario.'() {
diff --git a/cps-service/src/main/java/org/onap/cps/utils/XmlFileUtils.java b/cps-service/src/main/java/org/onap/cps/utils/XmlFileUtils.java
index 94b97bd88f..bbfb7f4d2e 100644
--- a/cps-service/src/main/java/org/onap/cps/utils/XmlFileUtils.java
+++ b/cps-service/src/main/java/org/onap/cps/utils/XmlFileUtils.java
@@ -189,30 +189,32 @@ public class XmlFileUtils {
private static void createXmlElements(final Document document, final Node parentNode,
final Map<String, Object> dataMap) {
- for (final Map.Entry<String, Object> mapEntry : dataMap.entrySet()) {
- if (mapEntry.getValue() instanceof List) {
- appendList(document, parentNode, mapEntry);
- } else if (mapEntry.getValue() instanceof Map) {
- appendMap(document, parentNode, mapEntry);
+ for (final Map.Entry<String, Object> dataNodeMapEntry : dataMap.entrySet()) {
+ if (dataNodeMapEntry.getValue() instanceof List) {
+ appendList(document, parentNode, dataNodeMapEntry);
+ } else if (dataNodeMapEntry.getValue() instanceof Map) {
+ appendMap(document, parentNode, dataNodeMapEntry);
} else {
- appendObject(document, parentNode, mapEntry);
+ appendObject(document, parentNode, dataNodeMapEntry);
}
}
}
private static void appendList(final Document document, final Node parentNode,
- final Map.Entry<String, Object> mapEntry) {
- final List<Object> list = (List<Object>) mapEntry.getValue();
- if (list.isEmpty()) {
- final Element listElement = document.createElement(mapEntry.getKey());
+ final Map.Entry<String, Object> dataNodeMapEntry) {
+ final List<Object> dataNodeMaps = (List<Object>) dataNodeMapEntry.getValue();
+ if (dataNodeMaps.isEmpty()) {
+ final Element listElement = document.createElement(dataNodeMapEntry.getKey());
parentNode.appendChild(listElement);
} else {
- for (final Object element : list) {
- final Element listElement = document.createElement(mapEntry.getKey());
- if (element instanceof Map) {
- createXmlElements(document, listElement, (Map<String, Object>) element);
+ for (final Object dataNodeMap : dataNodeMaps) {
+ final Element listElement = document.createElement(dataNodeMapEntry.getKey());
+ if (dataNodeMap == null) {
+ parentNode.appendChild(listElement);
+ } else if (dataNodeMap instanceof Map) {
+ createXmlElements(document, listElement, (Map<String, Object>) dataNodeMap);
} else {
- listElement.appendChild(document.createTextNode(element.toString()));
+ listElement.appendChild(document.createTextNode(dataNodeMap.toString()));
}
parentNode.appendChild(listElement);
}
@@ -220,16 +222,18 @@ public class XmlFileUtils {
}
private static void appendMap(final Document document, final Node parentNode,
- final Map.Entry<String, Object> mapEntry) {
- final Element childElement = document.createElement(mapEntry.getKey());
- createXmlElements(document, childElement, (Map<String, Object>) mapEntry.getValue());
+ final Map.Entry<String, Object> dataNodeMapEntry) {
+ final Element childElement = document.createElement(dataNodeMapEntry.getKey());
+ createXmlElements(document, childElement, (Map<String, Object>) dataNodeMapEntry.getValue());
parentNode.appendChild(childElement);
}
private static void appendObject(final Document document, final Node parentNode,
- final Map.Entry<String, Object> mapEntry) {
- final Element element = document.createElement(mapEntry.getKey());
- element.appendChild(document.createTextNode(mapEntry.getValue().toString()));
+ final Map.Entry<String, Object> dataNodeMapEntry) {
+ final Element element = document.createElement(dataNodeMapEntry.getKey());
+ if (dataNodeMapEntry.getValue() != null) {
+ element.appendChild(document.createTextNode(dataNodeMapEntry.getValue().toString()));
+ }
parentNode.appendChild(element);
}
diff --git a/cps-service/src/test/groovy/org/onap/cps/utils/XmlFileUtilsSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/utils/XmlFileUtilsSpec.groovy
index 3b21145293..9a932c9279 100644
--- a/cps-service/src/test/groovy/org/onap/cps/utils/XmlFileUtilsSpec.groovy
+++ b/cps-service/src/test/groovy/org/onap/cps/utils/XmlFileUtilsSpec.groovy
@@ -32,7 +32,7 @@ import static org.onap.cps.utils.XmlFileUtils.convertDataMapsToXml
class XmlFileUtilsSpec extends Specification {
- def 'Parse a valid xml content #scenario'(){
+ def 'Parse a valid xml content #scenario'() {
given: 'YANG model schema context'
def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('bookstore.yang')
def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext()
@@ -41,13 +41,13 @@ class XmlFileUtilsSpec extends Specification {
then: 'the result xml is wrapped by root node defined in YANG schema'
assert parsedXmlContent == expectedOutput
where:
- scenario | xmlData || expectedOutput
- 'without root data node' | '<?xml version="1.0" encoding="UTF-8"?><class> </class>' || '<?xml version="1.0" encoding="UTF-8"?><stores xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"><class> </class></stores>'
- 'with root data node' | '<?xml version="1.0" encoding="UTF-8"?><stores><class> </class></stores>' || '<?xml version="1.0" encoding="UTF-8"?><stores><class> </class></stores>'
- 'no xml header' | '<stores><class> </class></stores>' || '<stores><class> </class></stores>'
+ scenario | xmlData || expectedOutput
+ 'without root data node' | '<?xml version="1.0" encoding="UTF-8"?><class> </class>' || '<?xml version="1.0" encoding="UTF-8"?><stores xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"><class> </class></stores>'
+ 'with root data node' | '<?xml version="1.0" encoding="UTF-8"?><stores><class> </class></stores>' || '<?xml version="1.0" encoding="UTF-8"?><stores><class> </class></stores>'
+ 'no xml header' | '<stores><class> </class></stores>' || '<stores><class> </class></stores>'
}
- def 'Parse a invalid xml content'(){
+ def 'Parse a invalid xml content'() {
given: 'YANG model schema context'
def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('bookstore.yang')
def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext()
@@ -84,9 +84,6 @@ class XmlFileUtilsSpec extends Specification {
'nested XML branch' | [['test-tree': [branch: [name: 'Left', nest: [name: 'Small', birds: 'Sparrow']]]]] || '<test-tree><branch><name>Left</name><nest><name>Small</name><birds>Sparrow</birds></nest></branch></test-tree>'
'list of branch within a test tree' | [['test-tree': [branch: [[name: 'Left', nest: [name: 'Small', birds: 'Sparrow']], [name: 'Right', nest: [name: 'Big', birds: 'Owl']]]]]] || '<test-tree><branch><name>Left</name><nest><name>Small</name><birds>Sparrow</birds></nest></branch><branch><name>Right</name><nest><name>Big</name><birds>Owl</birds></nest></branch></test-tree>'
'list of birds under a nest' | [['nest': ['name': 'Small', 'birds': ['Sparrow']]]] || '<nest><name>Small</name><birds>Sparrow</birds></nest>'
- 'XML Content map with null key/value' | [['test-tree': [branch: [name: 'Left', nest: []]]]] || '<test-tree><branch><name>Left</name><nest/></branch></test-tree>'
- 'XML Content list is empty' | [['nest': ['name': 'Small', 'birds': []]]] || '<nest><name>Small</name><birds/></nest>'
- 'XML with mixed content in list' | [['branch': ['name': 'Left', 'nest': ['name': 'Small', 'birds': ['', 'Sparrow']]]]] || '<branch><name>Left</name><nest><name>Small</name><birds/><birds>Sparrow</birds></nest></branch>'
}
def 'Convert data maps to XML with null or empty maps and lists'() {
@@ -95,11 +92,14 @@ class XmlFileUtilsSpec extends Specification {
then: 'the result contains the expected XML or handles nulls correctly'
assert result == expectedXmlOutput
where:
- scenario | dataMaps || expectedXmlOutput
- 'null entry in map' | [['branch': []]] || '<branch/>'
- 'list with null object' | [['branch': [name: 'Left', nest: [name: 'Small', birds: []]]]] || '<branch><name>Left</name><nest><name>Small</name><birds/></nest></branch>'
- 'list containing null list' | [['test-tree': [branch: '']]] || '<test-tree><branch/></test-tree>'
- 'nested map with null values' | [['test-tree': [branch: [name: 'Left', nest: '']]]] || '<test-tree><branch><name>Left</name><nest/></branch></test-tree>'
+ scenario | dataMaps || expectedXmlOutput
+ 'null entry in map' | [['branch': []]] || '<branch/>'
+ 'XML Content list is empty' | [['nest': ['name': 'Small', 'birds': [null]]]] || '<nest><name>Small</name><birds/></nest>'
+ 'XML with mixed content in list' | [['branch': ['name': 'Left', 'nest': ['name': 'Small', 'birds': [null, 'Sparrow']]]]] || '<branch><name>Left</name><nest><name>Small</name><birds/><birds>Sparrow</birds></nest></branch>'
+ 'list with null object' | [['branch': [name: 'Left', nest: [name: 'Small', birds: [null]]]]] || '<branch><name>Left</name><nest><name>Small</name><birds/></nest></branch>'
+ 'list containing null values' | [['branch': [null, null, null]]] || '<branch/><branch/><branch/>'
+ 'nested map with null values' | [['test-tree': [branch: [name: 'Left', nest: null]]]] || '<test-tree><branch><name>Left</name><nest/></branch></test-tree>'
+ 'mixed list with null values' | [['branch': ['name': 'Left', 'nest': ['name': 'Small', 'birds': [null, 'Sparrow', null]]]]] || '<branch><name>Left</name><nest><name>Small</name><birds/><birds>Sparrow</birds><birds/></nest></branch>'
}
def 'Converting data maps to xml with no data'() {
@@ -109,7 +109,7 @@ class XmlFileUtilsSpec extends Specification {
convertDataMapsToXml(dataMapWithNull)
then: 'a validation exception is thrown'
def exception = thrown(DataValidationException)
- and:'the cause is a null pointer exception'
+ and: 'the cause is a null pointer exception'
assert exception.cause instanceof NullPointerException
}
@@ -120,9 +120,9 @@ class XmlFileUtilsSpec extends Specification {
convertDataMapsToXml(dataMap)
then: 'a validation exception is thrown'
def exception = thrown(DataValidationException)
- and:'the cause is a document object model exception'
+ and: 'the cause is a document object model exception'
assert exception.cause instanceof DOMException
}
-}
+} \ No newline at end of file
diff --git a/docs/api/swagger/cps/openapi.yaml b/docs/api/swagger/cps/openapi.yaml
index 3f889c1e6c..3b6bd43d6c 100644
--- a/docs/api/swagger/cps/openapi.yaml
+++ b/docs/api/swagger/cps/openapi.yaml
@@ -2283,6 +2283,15 @@ paths:
default: none
example: "3"
type: string
+ - description: Content type in header
+ in: header
+ name: Content-Type
+ required: true
+ schema:
+ enum:
+ - application/json
+ - application/xml
+ type: string
responses:
"200":
content:
@@ -2293,6 +2302,15 @@ paths:
value: null
schema:
type: object
+ application/xml:
+ examples:
+ dataSample:
+ $ref: '#/components/examples/dataSampleXml'
+ value: null
+ schema:
+ type: object
+ xml:
+ name: stores
description: OK
"400":
content: