diff options
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: |