From 5baf570979a06ec52e40dfaf613bb74665a8d9ff Mon Sep 17 00:00:00 2001 From: Rudrangi Anupriya Date: Wed, 6 Nov 2024 22:20:11 +0530 Subject: [PATCH] XML content support for only cps Query v2 Here to bring Support for XML Response Entity in query data nodes - Add ContentTypeInheadr in cpsQueryV2.yml to support application/xml - Add contentTypeInHeader parameter to accept xml in QueryRestController.java - Implement logic to convert data to xml - written testcase for above changes made Issue-ID: CPS-2359 Change-Id: Ieb7eeb66ccbb03703626132c6d5c2eade0e7cb4b Signed-off-by: Rudrangi Anupriya --- cps-rest/docs/openapi/cpsQueryV2.yml | 10 ++++ .../cps/rest/controller/QueryRestController.java | 28 ++++++++--- .../rest/controller/QueryRestControllerSpec.groovy | 55 ++++++++++++++++------ .../main/java/org/onap/cps/utils/XmlFileUtils.java | 46 +++++++++--------- .../org/onap/cps/utils/XmlFileUtilsSpec.groovy | 34 ++++++------- docs/api/swagger/cps/openapi.yaml | 18 +++++++ 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 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 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 dataNodes = cpsQueryService.queryDataNodes(dataspaceName, anchorName, cpsPath, fetchDescendantsOption); final List> dataNodesAsListOfMaps = new ArrayList<>(dataNodes.size()); @@ -143,6 +148,17 @@ public class QueryRestController implements CpsQueryApi { final Map dataMap = DataMapUtils.toDataMapWithIdentifier(dataNode, prefix); dataNodesAsListOfMaps.add(dataMap); } - return new ResponseEntity<>(jsonObjectMapper.asJsonString(dataNodesAsListOfMaps), HttpStatus.OK); + return buildResponseEntity(dataNodesAsListOfMaps, contentType); + } + + private ResponseEntity buildResponseEntity(final List> 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('valueleaveListElement1leaveListElement2') + 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 dataMap) { - for (final Map.Entry 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 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 mapEntry) { - final List list = (List) mapEntry.getValue(); - if (list.isEmpty()) { - final Element listElement = document.createElement(mapEntry.getKey()); + final Map.Entry dataNodeMapEntry) { + final List dataNodeMaps = (List) 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) 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) 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 mapEntry) { - final Element childElement = document.createElement(mapEntry.getKey()); - createXmlElements(document, childElement, (Map) mapEntry.getValue()); + final Map.Entry dataNodeMapEntry) { + final Element childElement = document.createElement(dataNodeMapEntry.getKey()); + createXmlElements(document, childElement, (Map) dataNodeMapEntry.getValue()); parentNode.appendChild(childElement); } private static void appendObject(final Document document, final Node parentNode, - final Map.Entry mapEntry) { - final Element element = document.createElement(mapEntry.getKey()); - element.appendChild(document.createTextNode(mapEntry.getValue().toString())); + final Map.Entry 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' | ' ' || ' ' - 'with root data node' | ' ' || ' ' - 'no xml header' | ' ' || ' ' + scenario | xmlData || expectedOutput + 'without root data node' | ' ' || ' ' + 'with root data node' | ' ' || ' ' + 'no xml header' | ' ' || ' ' } - 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']]]]] || 'LeftSmallSparrow' 'list of branch within a test tree' | [['test-tree': [branch: [[name: 'Left', nest: [name: 'Small', birds: 'Sparrow']], [name: 'Right', nest: [name: 'Big', birds: 'Owl']]]]]] || 'LeftSmallSparrowRightBigOwl' 'list of birds under a nest' | [['nest': ['name': 'Small', 'birds': ['Sparrow']]]] || 'SmallSparrow' - 'XML Content map with null key/value' | [['test-tree': [branch: [name: 'Left', nest: []]]]] || 'Left' - 'XML Content list is empty' | [['nest': ['name': 'Small', 'birds': []]]] || 'Small' - 'XML with mixed content in list' | [['branch': ['name': 'Left', 'nest': ['name': 'Small', 'birds': ['', 'Sparrow']]]]] || 'LeftSmallSparrow' } 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': []]] || '' - 'list with null object' | [['branch': [name: 'Left', nest: [name: 'Small', birds: []]]]] || 'LeftSmall' - 'list containing null list' | [['test-tree': [branch: '']]] || '' - 'nested map with null values' | [['test-tree': [branch: [name: 'Left', nest: '']]]] || 'Left' + scenario | dataMaps || expectedXmlOutput + 'null entry in map' | [['branch': []]] || '' + 'XML Content list is empty' | [['nest': ['name': 'Small', 'birds': [null]]]] || 'Small' + 'XML with mixed content in list' | [['branch': ['name': 'Left', 'nest': ['name': 'Small', 'birds': [null, 'Sparrow']]]]] || 'LeftSmallSparrow' + 'list with null object' | [['branch': [name: 'Left', nest: [name: 'Small', birds: [null]]]]] || 'LeftSmall' + 'list containing null values' | [['branch': [null, null, null]]] || '' + 'nested map with null values' | [['test-tree': [branch: [name: 'Left', nest: null]]]] || 'Left' + 'mixed list with null values' | [['branch': ['name': 'Left', 'nest': ['name': 'Small', 'birds': [null, 'Sparrow', null]]]]] || 'LeftSmallSparrow' } 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: -- 2.16.6