From 66bdcdc8cfa116e338373ee78c3d472076db781b Mon Sep 17 00:00:00 2001 From: Rudrangi Anupriya Date: Tue, 12 Aug 2025 14:21:19 +0530 Subject: [PATCH] XML content support on Get delta between two anchors within a given dataspace Here to bring Support for XML content on Get delta between two anchors within a given dataspace - Add deltaReportSampleXml Example in component.yaml - Add ContentTypeInheader in cpsDataV2.yml to support application/xml in Delta - Add contentTypeInHeader parameter to accept xml in DataRestController.java - Implemented Logic to convert deltaReport to XML in XmlFileUtils.java - written testcase for above changes made Issue-ID: CPS-2452 Change-Id: Iff7fab66ddcc03703255123c6d5c2eade0e7cb4a Signed-off-by: GM001016278 --- cps-rest/docs/openapi/components.yml | 32 +++++++++++- cps-rest/docs/openapi/cpsDelta.yml | 9 +++- .../cps/rest/controller/DataRestController.java | 7 +-- .../cps/rest/controller/DeltaRestController.java | 27 ++++++++-- .../cps/rest/controller/QueryRestController.java | 7 +-- .../rest/controller/DeltaRestControllerSpec.groovy | 33 ++++++++----- .../exceptions/CpsRestExceptionHandlerSpec.groovy | 6 ++- cps-service/pom.xml | 9 +++- .../java/org/onap/cps/utils/XmlObjectMapper.java | 52 ++++++++++++++++++++ .../cps/utils/{XmlFileUtils.java => XmlUtils.java} | 38 ++++++++++----- .../java/org/onap/cps/utils/YangParserHelper.java | 5 +- .../cps/utils/deltareport/DeltaReportWrapper.java | 39 +++++++++++++++ .../org/onap/cps/utils/XmlObjectMapperSpec.groovy | 57 ++++++++++++++++++++++ ...XmlFileUtilsSpec.groovy => XmlUtilsSpec.groovy} | 14 +++--- .../integration/base/CpsIntegrationSpecBase.groovy | 7 ++- 15 files changed, 294 insertions(+), 48 deletions(-) create mode 100644 cps-service/src/main/java/org/onap/cps/utils/XmlObjectMapper.java rename cps-service/src/main/java/org/onap/cps/utils/{XmlFileUtils.java => XmlUtils.java} (91%) create mode 100644 cps-service/src/main/java/org/onap/cps/utils/deltareport/DeltaReportWrapper.java create mode 100644 cps-service/src/test/groovy/org/onap/cps/utils/XmlObjectMapperSpec.groovy rename cps-service/src/test/groovy/org/onap/cps/utils/{XmlFileUtilsSpec.groovy => XmlUtilsSpec.groovy} (95%) diff --git a/cps-rest/docs/openapi/components.yml b/cps-rest/docs/openapi/components.yml index f308a1ff8b..e5596562a3 100644 --- a/cps-rest/docs/openapi/components.yml +++ b/cps-rest/docs/openapi/components.yml @@ -1,7 +1,7 @@ # ============LICENSE_START======================================================= # Copyright (c) 2021-2022 Bell Canada. # Modifications Copyright (C) 2021-2023 Nordix Foundation -# Modifications Copyright (C) 2022-2025 Deutsche Telekom AG +# Modifications Copyright (C) 2025-2026 Deutsche Telekom AG # ================================================================================ # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -292,6 +292,36 @@ components: dataspace: - name: my-dataspace - name: bookstore-dataspace + deltaReportSampleXml: + value: + + + replace + /bookstore/categories[@code='1'] + + SciFi + + + Comic + + + + remove + /bookstore/categories[@code='2'] + + 2 + kids + + + + create + /bookstore/categories[@code='3'] + + 3 + Fiction + + + parameters: dataspaceNameInQuery: name: dataspace-name diff --git a/cps-rest/docs/openapi/cpsDelta.yml b/cps-rest/docs/openapi/cpsDelta.yml index 044de220e2..d200785529 100644 --- a/cps-rest/docs/openapi/cpsDelta.yml +++ b/cps-rest/docs/openapi/cpsDelta.yml @@ -1,5 +1,5 @@ # ============LICENSE_START======================================================= -# Copyright (c) 2025 Deutsche Telekom AG +# Copyright (c) 2025-2026 Deutsche Telekom AG # ================================================================================ # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -30,6 +30,7 @@ delta: - $ref: 'components.yml#/components/parameters/xpathInQuery' - $ref: 'components.yml#/components/parameters/descendantsInQuery' - $ref: 'components.yml#/components/parameters/groupDataNodesInQuery' + - $ref: 'components.yml#/components/parameters/contentTypeInHeader' responses: '200': description: OK @@ -40,6 +41,12 @@ delta: examples: dataSample: $ref: 'components.yml#/components/examples/deltaReportSample' + application/xml: + schema: + type: object + examples: + dataSample: + $ref: 'components.yml#/components/examples/deltaReportSampleXml' '400': $ref: 'components.yml#/components/responses/BadRequest' '403': 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 a15aa63fa2..36f2428547 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 @@ -3,7 +3,7 @@ * Copyright (C) 2020-2022 Bell Canada. * Modifications Copyright (C) 2021 Pantheon.tech * Modifications Copyright (C) 2021-2025 Nordix Foundation - * Modifications Copyright (C) 2022-2025 Deutsche Telekom AG + * Modifications Copyright (C) 2025-2026 Deutsche Telekom AG * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,8 @@ package org.onap.cps.rest.controller; +import static org.onap.cps.utils.XmlUtils.convertDataMapsToXml; + import io.micrometer.core.annotation.Timed; import jakarta.validation.ValidationException; import java.time.OffsetDateTime; @@ -37,7 +39,6 @@ import org.onap.cps.api.parameters.FetchDescendantsOption; import org.onap.cps.rest.api.CpsDataApi; import org.onap.cps.utils.ContentType; import org.onap.cps.utils.JsonObjectMapper; -import org.onap.cps.utils.XmlFileUtils; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RequestMapping; @@ -203,7 +204,7 @@ public class DataRestController implements CpsDataApi { private ResponseEntity buildResponseEntity(final Object dataMaps, final ContentType contentType) { final String responseData; if (ContentType.XML.equals(contentType)) { - responseData = XmlFileUtils.convertDataMapsToXml(dataMaps); + responseData = convertDataMapsToXml(dataMaps); } else { responseData = jsonObjectMapper.asJsonString(dataMaps); } diff --git a/cps-rest/src/main/java/org/onap/cps/rest/controller/DeltaRestController.java b/cps-rest/src/main/java/org/onap/cps/rest/controller/DeltaRestController.java index b1dcb456fb..0b6cc3b89a 100644 --- a/cps-rest/src/main/java/org/onap/cps/rest/controller/DeltaRestController.java +++ b/cps-rest/src/main/java/org/onap/cps/rest/controller/DeltaRestController.java @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2025 Deutsche Telekom AG + * Copyright (C) 2025-2026 Deutsche Telekom AG * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ package org.onap.cps.rest.controller; import static org.onap.cps.rest.utils.MultipartFileUtil.extractYangResourcesMap; +import static org.onap.cps.utils.ContentType.XML; import io.micrometer.core.annotation.Timed; import java.util.Collection; @@ -33,7 +34,10 @@ 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.ContentType; import org.onap.cps.utils.JsonObjectMapper; +import org.onap.cps.utils.XmlObjectMapper; +import org.onap.cps.utils.deltareport.DeltaReportWrapper; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RequestMapping; @@ -45,8 +49,11 @@ import org.springframework.web.multipart.MultipartFile; @RequiredArgsConstructor public class DeltaRestController implements CpsDeltaApi { + private static final String XML_ROOT_NAME = "deltaReports"; + private final CpsDeltaService cpsDeltaService; private final JsonObjectMapper jsonObjectMapper; + private final XmlObjectMapper xmlObjectMapper; @Timed(value = "cps.delta.controller.get.delta", description = "Time taken to get delta between anchors") @@ -56,13 +63,15 @@ public class DeltaRestController implements CpsDeltaApi { final String targetAnchorName, final String xpath, final String descendants, - final Boolean groupDataNodes) { + final Boolean groupDataNodes, + final String contentTypeInHeader) { final FetchDescendantsOption fetchDescendantsOption = FetchDescendantsOption.getFetchDescendantsOption(descendants); - final List deltaBetweenAnchors = + final List deltaReports = cpsDeltaService.getDeltaByDataspaceAndAnchors(dataspaceName, sourceAnchorName, targetAnchorName, xpath, fetchDescendantsOption, groupDataNodes); - return new ResponseEntity<>(jsonObjectMapper.asJsonString(deltaBetweenAnchors), HttpStatus.OK); + return buildDeltaResponseEntity(deltaReports, ContentType.fromString(contentTypeInHeader)); + } @Timed(value = "cps.delta.controller.get.delta", @@ -95,4 +104,14 @@ public class DeltaRestController implements CpsDeltaApi { return ResponseEntity.status(HttpStatus.CREATED).build(); } + private ResponseEntity buildDeltaResponseEntity(final List deltaReports, + final ContentType contentType) { + if (XML.equals(contentType)) { + final DeltaReportWrapper deltaReportWrapper = new DeltaReportWrapper<>(deltaReports); + final String xmlDeltaReport = xmlObjectMapper.asXmlString(deltaReportWrapper, XML_ROOT_NAME); + return new ResponseEntity<>(xmlDeltaReport, HttpStatus.OK); + } else { + return new ResponseEntity<>(jsonObjectMapper.asJsonString(deltaReports), HttpStatus.OK); + } + } } 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 702eb703c1..05cad19afc 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-2025 Nordix Foundation * Modifications Copyright (C) 2022 Bell Canada. - * Modifications Copyright (C) 2022-2024 Deutsche Telekom AG + * Modifications Copyright (C) 2025-2026 Deutsche Telekom AG * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,8 @@ package org.onap.cps.rest.controller; +import static org.onap.cps.utils.XmlUtils.convertDataMapsToXml; + import io.micrometer.core.annotation.Timed; import java.util.List; import java.util.Map; @@ -32,7 +34,6 @@ import org.onap.cps.api.parameters.PaginationOption; import org.onap.cps.rest.api.CpsQueryApi; import org.onap.cps.utils.ContentType; import org.onap.cps.utils.JsonObjectMapper; -import org.onap.cps.utils.XmlFileUtils; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RequestMapping; @@ -98,7 +99,7 @@ public class QueryRestController implements CpsQueryApi { final ContentType contentType) { final String responseData; if (ContentType.XML.equals(contentType)) { - responseData = XmlFileUtils.convertDataMapsToXml(dataNodesAsMaps); + responseData = convertDataMapsToXml(dataNodesAsMaps); } else { responseData = jsonObjectMapper.asJsonString(dataNodesAsMaps); } diff --git a/cps-rest/src/test/groovy/org/onap/cps/rest/controller/DeltaRestControllerSpec.groovy b/cps-rest/src/test/groovy/org/onap/cps/rest/controller/DeltaRestControllerSpec.groovy index ade991979a..b24fd2158f 100644 --- a/cps-rest/src/test/groovy/org/onap/cps/rest/controller/DeltaRestControllerSpec.groovy +++ b/cps-rest/src/test/groovy/org/onap/cps/rest/controller/DeltaRestControllerSpec.groovy @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2025 Deutsche Telekom AG + * Copyright (C) 2025-2026 Deutsche Telekom AG * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import org.onap.cps.api.CpsDeltaService import org.onap.cps.impl.DeltaReportBuilder import org.onap.cps.utils.JsonObjectMapper +import org.onap.cps.utils.XmlObjectMapper import org.spockframework.spring.SpringBean import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Value @@ -35,7 +36,6 @@ import org.springframework.test.web.servlet.MockMvc 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 @@ -56,6 +56,9 @@ class DeltaRestControllerSpec extends Specification { @SpringBean JsonObjectMapper jsonObjectMapper = new JsonObjectMapper(new ObjectMapper()) + @SpringBean + XmlObjectMapper xmlObjectMapper = new XmlObjectMapper() + @Autowired MockMvc mvc @@ -89,21 +92,27 @@ class DeltaRestControllerSpec extends Specification { Files.deleteIfExists(targetDataAsJsonFile) } - def 'Get delta between two anchors'() { - given: 'the service returns a list containing delta reports' - def deltaReports = new DeltaReportBuilder().actionReplace().withXpath('some xpath').withSourceData('some key': 'some value').withTargetData('some key': 'some value').build() + def 'Get delta between two anchors with content type #scenario'() { + given: 'a xpath and delta report' def xpath = 'some xpath' + def deltaReports = new DeltaReportBuilder().actionReplace().withXpath(xpath).withSourceData('some_key': 'some value').withTargetData('some_key': 'some value').build() + and: 'service returns a list containing delta reports' mockCpsDeltaService.getDeltaByDataspaceAndAnchors(dataspaceName, anchorName, 'targetAnchor', xpath, OMIT_DESCENDANTS, NO_GROUPING) >> [deltaReports] when: 'get delta request is performed using REST API' - def response = - mvc.perform(get(dataNodeBaseEndpointV2) - .param('target-anchor-name', 'targetAnchor') - .param('xpath', xpath)) - .andReturn().response + def response = mvc.perform(get(dataNodeBaseEndpointV2) + .contentType(contentType) + .accept(contentType) + .param('target-anchor-name', 'targetAnchor') + .param('xpath', xpath)) + .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\":\"replace\",\"xpath\":\"some xpath\",\"sourceData\":{\"some key\":\"some value\"},\"targetData\":{\"some key\":\"some value\"}}]') + assert response.contentAsString.contains(expectedResponse) + where: + scenario | contentType || expectedResponse + 'JSON' | MediaType.APPLICATION_JSON || '[{"action":"replace","xpath":"some xpath","sourceData":{"some_key":"some value"},"targetData":{"some_key":"some value"}}]' + 'XML' | MediaType.APPLICATION_XML || 'replacesome xpathsome valuesome value' } def 'Get delta between anchor and JSON payload with yangResourceFile'() { @@ -193,4 +202,4 @@ class DeltaRestControllerSpec extends Specification { then: 'expected response code is returned' assert response.status == HttpStatus.CREATED.value() } -} +} \ No newline at end of file diff --git a/cps-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy b/cps-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy index b32279704d..95d0197baa 100644 --- a/cps-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy +++ b/cps-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy @@ -3,7 +3,7 @@ * Copyright (C) 2020 Pantheon.tech * Modifications Copyright (C) 2021-2025 Nordix Foundation * Modifications Copyright (C) 2021 Bell Canada. - * Modifications Copyright (C) 2022-2025 Deutsche Telekom AG + * Modifications Copyright (C) 2025-2026 Deutsche Telekom AG * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,6 +46,7 @@ import org.onap.cps.api.exceptions.SchemaSetInUseException import org.onap.cps.rest.controller.CpsRestInputMapper import org.onap.cps.utils.JsonObjectMapper import org.onap.cps.utils.PrefixResolver +import org.onap.cps.utils.XmlObjectMapper import org.spockframework.spring.SpringBean import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Value @@ -98,6 +99,9 @@ class CpsRestExceptionHandlerSpec extends Specification { @SpringBean CpsDeltaService cpsDeltaService = Stub() + @SpringBean + XmlObjectMapper xmlObjectMapper = new XmlObjectMapper() + @Autowired MockMvc mvc diff --git a/cps-service/pom.xml b/cps-service/pom.xml index 1e98d20454..76d6c8567f 100644 --- a/cps-service/pom.xml +++ b/cps-service/pom.xml @@ -4,7 +4,7 @@ Copyright (C) 2021-2025 OpenInfra Foundation Europe. All rights reserved. Modifications Copyright (C) 2021 Bell Canada. Modifications Copyright (C) 2021 Pantheon.tech - Modifications Copyright (C) 2022-2024 Deutsche Telekom AG + Modifications Copyright (C) 2025-2026 Deutsche Telekom AG ================================================================================ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -73,8 +73,8 @@ com.github.spotbugs spotbugs-annotations + - com.google.code.gson gson @@ -204,5 +204,10 @@ org.springframework spring-web + + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + diff --git a/cps-service/src/main/java/org/onap/cps/utils/XmlObjectMapper.java b/cps-service/src/main/java/org/onap/cps/utils/XmlObjectMapper.java new file mode 100644 index 0000000000..35c83b4569 --- /dev/null +++ b/cps-service/src/main/java/org/onap/cps/utils/XmlObjectMapper.java @@ -0,0 +1,52 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2025-2026 Deutsche Telekom AG. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps.utils; + +import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import lombok.NoArgsConstructor; +import org.onap.cps.api.exceptions.DataValidationException; +import org.springframework.stereotype.Component; + +@NoArgsConstructor +@Component +public class XmlObjectMapper { + + private final XmlMapper xmlMapper = new XmlMapper(); + + /** + * Serializing generic java object to XML using Jackson. + * + * @param object any java object value + * @param rootName the name of the XML root name + * @return the generated XML as a string. + */ + + public String asXmlString(final Object object, final String rootName) { + try { + return xmlMapper.writer().withRootName(rootName).writeValueAsString(object); + } catch (final Exception exception) { + throw new DataValidationException("Data Validation Failed", + "Failed to build XML: " + exception.getMessage(), + exception + ); + } + } +} diff --git a/cps-service/src/main/java/org/onap/cps/utils/XmlFileUtils.java b/cps-service/src/main/java/org/onap/cps/utils/XmlUtils.java similarity index 91% rename from cps-service/src/main/java/org/onap/cps/utils/XmlFileUtils.java rename to cps-service/src/main/java/org/onap/cps/utils/XmlUtils.java index e5c0542a04..071c326947 100644 --- a/cps-service/src/main/java/org/onap/cps/utils/XmlFileUtils.java +++ b/cps-service/src/main/java/org/onap/cps/utils/XmlUtils.java @@ -2,7 +2,7 @@ * ============LICENSE_START======================================================= * Copyright (C) 2022 Deutsche Telekom AG * Modifications Copyright (C) 2023-2025 OpenInfra Foundation Europe. - * Modifications Copyright (C) 2022-2025 Deutsche Telekom AG + * Modifications Copyright (C) 2025-2026 Deutsche Telekom AG * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -55,7 +55,7 @@ import org.w3c.dom.Node; import org.xml.sax.SAXException; @NoArgsConstructor(access = AccessLevel.PRIVATE) -public class XmlFileUtils { +public class XmlUtils { private static final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); private static boolean isNewDocumentBuilderFactoryInstance = true; @@ -186,7 +186,7 @@ public class XmlFileUtils { } else { throw new IllegalArgumentException("Unsupported data type for XML conversion"); } - return transformFragmentToString(documentFragment); + return transformNodeToString(documentFragment); } catch (final DOMException | NullPointerException | ParserConfigurationException | TransformerException exception) { throw new DataValidationException( @@ -245,27 +245,44 @@ public class XmlFileUtils { } parentNode.appendChild(element); } + /** + * Converts the given XML Node into a String. + * + * @param node the XML node to convert + * @return string representation of the XML node + * @throws TransformerException if transformation fails + */ - private static String transformFragmentToString(final DocumentFragment documentFragment) + private static String transformNodeToString(final Node node) throws TransformerException { final Transformer transformer = getTransformerFactory().newTransformer(); transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); - final StringWriter writer = new StringWriter(); - final StreamResult result = new StreamResult(writer); - transformer.transform(new DOMSource(documentFragment), result); - return writer.toString(); + final StringWriter stringWriter = new StringWriter(); + final StreamResult streamResult = new StreamResult(stringWriter); + transformer.transform(new DOMSource(node), streamResult); + return stringWriter.toString(); } + /** + * Provides a configured instance of DocumentBuilderFactory. + * This method initializes the factory with secure processing settings + * to prevent XML External Entity (XXE) attacks. + * @return a configured DocumentBuilderFactory instance + */ - @SuppressWarnings("SameReturnValue") private static DocumentBuilderFactory getDocumentBuilderFactory() { if (isNewDocumentBuilderFactoryInstance) { documentBuilderFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); documentBuilderFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); isNewDocumentBuilderFactoryInstance = false; } - return documentBuilderFactory; } + /** + * Provides a configured instance of TransformerFactory. + * This method initializes the factory with secure settings to prevent + * unwanted stylesheet or DTD access. + * @return a configured TransformerFactory instance + */ @SuppressWarnings("SameReturnValue") private static TransformerFactory getTransformerFactory() { @@ -274,7 +291,6 @@ public class XmlFileUtils { transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, ""); isNewTransformerFactoryInstance = false; } - return transformerFactory; } } diff --git a/cps-service/src/main/java/org/onap/cps/utils/YangParserHelper.java b/cps-service/src/main/java/org/onap/cps/utils/YangParserHelper.java index be2c657e63..c5c141a8a1 100644 --- a/cps-service/src/main/java/org/onap/cps/utils/YangParserHelper.java +++ b/cps-service/src/main/java/org/onap/cps/utils/YangParserHelper.java @@ -1,6 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2024-2025 OpenInfra Foundation Europe. All rights reserved. + * Modifications Copyright (C) 2025-2026 Deutsche Telekom AG * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -156,7 +157,7 @@ public class YangParserHelper { final String preparedXmlContent; try { if (parentNodeXpath.isEmpty()) { - preparedXmlContent = XmlFileUtils.prepareXmlContent(xmlData, schemaContext); + preparedXmlContent = XmlUtils.prepareXmlContent(xmlData, schemaContext); xmlParserStream = XmlParserStream.create(normalizedNodeStreamWriter, effectiveModelContext); } else { final DataSchemaNode parentSchemaNode = @@ -167,7 +168,7 @@ public class YangParserHelper { final EffectiveStatementInference effectiveStatementInference = SchemaInferenceStack.of(effectiveModelContext, SchemaNodeIdentifier.Absolute.of(dataSchemaNodeIdentifiers)).toInference(); - preparedXmlContent = XmlFileUtils.prepareXmlContent(xmlData, parentSchemaNode, parentNodeXpath); + preparedXmlContent = XmlUtils.prepareXmlContent(xmlData, parentSchemaNode, parentNodeXpath); xmlParserStream = XmlParserStream.create(normalizedNodeStreamWriter, effectiveStatementInference); } diff --git a/cps-service/src/main/java/org/onap/cps/utils/deltareport/DeltaReportWrapper.java b/cps-service/src/main/java/org/onap/cps/utils/deltareport/DeltaReportWrapper.java new file mode 100644 index 0000000000..1df63d972f --- /dev/null +++ b/cps-service/src/main/java/org/onap/cps/utils/deltareport/DeltaReportWrapper.java @@ -0,0 +1,39 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2025-2026 Deutsche Telekom AG. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps.utils.deltareport; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class DeltaReportWrapper { + @JacksonXmlElementWrapper(useWrapping = false) + @JacksonXmlProperty(localName = "deltaReport") + private List deltaReports; +} diff --git a/cps-service/src/test/groovy/org/onap/cps/utils/XmlObjectMapperSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/utils/XmlObjectMapperSpec.groovy new file mode 100644 index 0000000000..ef4c74f83e --- /dev/null +++ b/cps-service/src/test/groovy/org/onap/cps/utils/XmlObjectMapperSpec.groovy @@ -0,0 +1,57 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2025-2026 Deutsche Telekom AG. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.onap.cps.utils + +import com.fasterxml.jackson.dataformat.xml.XmlMapper +import org.onap.cps.api.exceptions.DataValidationException +import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectWriter +import spock.lang.Specification + +class XmlObjectMapperSpec extends Specification { + + def objectUnderTest = new XmlObjectMapper() + + def 'Map a structured object to Xml'() { + given: 'an object model' + def object = [bookstore: [['bookstore-name': 'Chapters',categories: [code: '1', name: 'SciFi']]]] + def expectedResult = 'Chapters1SciFi' + when: 'the object is mapped to string' + def result = objectUnderTest.asXmlString(object, "stores") + then: 'the result is a valid xml string' + assert result == expectedResult + } + + def 'Map a structured object with exception during mapping.'() { + given: 'some object' + def object = new Object(); + and: 'the XmlMapper chain throws an exception' + def spiedXmlMapper = Spy(XmlMapper) + def writer = Mock(ObjectWriter) + spiedXmlMapper.writer() >> writer + writer.withRootName('stores') >> writer + writer.writeValueAsString(_) >> { throw new Exception() } + when: 'attempt to build XML' + objectUnderTest.asXmlString(object, 'stores') + then: 'the exception has correct message' + def thrownException = thrown(DataValidationException) + thrownException.message == 'Data Validation Failed' + } +} + diff --git a/cps-service/src/test/groovy/org/onap/cps/utils/XmlFileUtilsSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/utils/XmlUtilsSpec.groovy similarity index 95% rename from cps-service/src/test/groovy/org/onap/cps/utils/XmlFileUtilsSpec.groovy rename to cps-service/src/test/groovy/org/onap/cps/utils/XmlUtilsSpec.groovy index 9ab75c5e63..3bcf5a7f04 100644 --- a/cps-service/src/test/groovy/org/onap/cps/utils/XmlFileUtilsSpec.groovy +++ b/cps-service/src/test/groovy/org/onap/cps/utils/XmlUtilsSpec.groovy @@ -2,7 +2,7 @@ * ============LICENSE_START======================================================= * Copyright (C) 2022 Deutsche Telekom AG * Modifications Copyright (c) 2023-2024 Nordix Foundation - * Modifications Copyright (C) 2024-2025 Deutsche Telekom AG + * Modifications Copyright (C) 2024-2026 Deutsche Telekom AG * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,17 +27,17 @@ import org.onap.cps.yang.YangTextSchemaSourceSetBuilder import org.w3c.dom.DOMException import org.xml.sax.SAXParseException import spock.lang.Specification +import static XmlUtils.convertDataMapsToXml -import static org.onap.cps.utils.XmlFileUtils.convertDataMapsToXml -class XmlFileUtilsSpec extends Specification { +class XmlUtilsSpec extends Specification { def 'Parse a valid xml content #scenario'() { given: 'YANG model schema context' def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('bookstore.yang') def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).schemaContext() when: 'the xml data is parsed' - def parsedXmlContent = XmlFileUtils.prepareXmlContent(xmlData, schemaContext) + def parsedXmlContent = XmlUtils.prepareXmlContent(xmlData, schemaContext) then: 'the result xml is wrapped by root node defined in YANG schema' assert parsedXmlContent == expectedOutput where: @@ -52,7 +52,7 @@ class XmlFileUtilsSpec extends Specification { def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('bookstore.yang') def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).schemaContext() when: 'attempt to parse invalid xml' - XmlFileUtils.prepareXmlContent('invalid-xml', schemaContext) + XmlUtils.prepareXmlContent('invalid-xml', schemaContext) then: 'a Sax Parser exception is thrown' thrown(SAXParseException) } @@ -64,7 +64,7 @@ class XmlFileUtilsSpec extends Specification { and: 'Parent schema node by xPath' def parentSchemaNode = YangParserHelper.getDataSchemaNodeAndIdentifiersByXpath(xPath, schemaContext).get('dataSchemaNode') when: 'the XML data is parsed' - def parsedXmlContent = XmlFileUtils.prepareXmlContent(xmlData, parentSchemaNode, xPath) + def parsedXmlContent = XmlUtils.prepareXmlContent(xmlData, parentSchemaNode, xPath) then: 'the result XML is wrapped by xPath defined parent root node' assert parsedXmlContent == expectedOutput where: @@ -133,4 +133,4 @@ class XmlFileUtilsSpec extends Specification { } -} +} \ No newline at end of file diff --git a/integration-test/src/test/groovy/org/onap/cps/integration/base/CpsIntegrationSpecBase.groovy b/integration-test/src/test/groovy/org/onap/cps/integration/base/CpsIntegrationSpecBase.groovy index 22e4f6b3b2..4a2e13f38e 100644 --- a/integration-test/src/test/groovy/org/onap/cps/integration/base/CpsIntegrationSpecBase.groovy +++ b/integration-test/src/test/groovy/org/onap/cps/integration/base/CpsIntegrationSpecBase.groovy @@ -1,7 +1,7 @@ /* * ============LICENSE_START======================================================= * Copyright (C) 2023-2025 OpenInfra Foundation Europe. All rights reserved. - * Modifications Copyright (C) 2024-2025 Deutsche Telekom AG + * Modifications Copyright (C) 2024-2026 Deutsche Telekom AG * ================================================================================ * Licensed under the Apache License, Version 2.0 (the 'License'); * you may not use this file except in compliance with the License. @@ -54,6 +54,8 @@ import org.onap.cps.ri.repository.SchemaSetRepository import org.onap.cps.ri.utils.SessionManager import org.onap.cps.spi.CpsModulePersistenceService import org.onap.cps.utils.JsonObjectMapper +import org.onap.cps.utils.XmlObjectMapper +import org.spockframework.spring.SpringBean import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Qualifier import org.springframework.beans.factory.annotation.Value @@ -167,6 +169,9 @@ abstract class CpsIntegrationSpecBase extends Specification { @Autowired MeterRegistry meterRegistry + @SpringBean + XmlObjectMapper xmlObjectMapper = new XmlObjectMapper() + @Value('${ncmp.policy-executor.server.port:8080}') private String policyServerPort; -- 2.16.6