XML content support on Get delta between two anchors within a given dataspace 18/141618/103
authorRudrangi Anupriya <ra00745022@techmahindra.com>
Tue, 12 Aug 2025 08:51:19 +0000 (14:21 +0530)
committerGM001016278 <gourav.malviya@techmahindra.com>
Sat, 14 Feb 2026 06:17:26 +0000 (11:47 +0530)
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 <gourav.malviya@techmahindra.com>
15 files changed:
cps-rest/docs/openapi/components.yml
cps-rest/docs/openapi/cpsDelta.yml
cps-rest/src/main/java/org/onap/cps/rest/controller/DataRestController.java
cps-rest/src/main/java/org/onap/cps/rest/controller/DeltaRestController.java
cps-rest/src/main/java/org/onap/cps/rest/controller/QueryRestController.java
cps-rest/src/test/groovy/org/onap/cps/rest/controller/DeltaRestControllerSpec.groovy
cps-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy
cps-service/pom.xml
cps-service/src/main/java/org/onap/cps/utils/XmlObjectMapper.java [new file with mode: 0644]
cps-service/src/main/java/org/onap/cps/utils/XmlUtils.java [moved from cps-service/src/main/java/org/onap/cps/utils/XmlFileUtils.java with 91% similarity]
cps-service/src/main/java/org/onap/cps/utils/YangParserHelper.java
cps-service/src/main/java/org/onap/cps/utils/deltareport/DeltaReportWrapper.java [new file with mode: 0644]
cps-service/src/test/groovy/org/onap/cps/utils/XmlObjectMapperSpec.groovy [new file with mode: 0644]
cps-service/src/test/groovy/org/onap/cps/utils/XmlUtilsSpec.groovy [moved from cps-service/src/test/groovy/org/onap/cps/utils/XmlFileUtilsSpec.groovy with 95% similarity]
integration-test/src/test/groovy/org/onap/cps/integration/base/CpsIntegrationSpecBase.groovy

index f308a1f..e559656 100644 (file)
@@ -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:
+         <deltaReports>
+            <deltaReport>
+                   <action>replace</action>
+                   <xpath>/bookstore/categories[@code='1']</xpath>
+                   <source-data>
+                        <name>SciFi</name>
+                   </source-data>
+                   <target-data>
+                        <name>Comic</name>
+                </target-data>
+           </deltaReport>
+           <deltaReport>
+                  <action>remove</action>
+                  <xpath>/bookstore/categories[@code='2']</xpath>
+                  <source-data>
+                       <code>2</code>
+                       <name>kids</name>
+                  </source-data>
+           </deltaReport>
+           <deltaReport>
+                     <action>create</action>
+                     <xpath>/bookstore/categories[@code='3']</xpath>
+                     <target-data>
+                          <code>3</code>
+                          <name>Fiction</name>
+                     </target-data>
+          </deltaReport>
+       </deltaReports>
   parameters:
     dataspaceNameInQuery:
       name: dataspace-name
index 044de22..d200785 100644 (file)
@@ -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':
index a15aa63..36f2428 100755 (executable)
@@ -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<Object> 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);
         }
index b1dcb45..0b6cc3b 100644 (file)
@@ -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<DeltaReport> deltaBetweenAnchors =
+        final List<DeltaReport> 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<Object> buildDeltaResponseEntity(final List<DeltaReport> deltaReports,
+                                                            final ContentType contentType) {
+        if (XML.equals(contentType)) {
+            final DeltaReportWrapper<DeltaReport> 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);
+        }
+    }
 }
index 702eb70..05cad19 100644 (file)
@@ -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);
         }
index ade9919..b24fd21 100644 (file)
@@ -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     || '<deltaReports><deltaReport><action>replace</action><xpath>some xpath</xpath><sourceData><some_key>some value</some_key></sourceData><targetData><some_key>some value</some_key></targetData></deltaReport></deltaReports>'
     }
 
     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
index b322797..95d0197 100644 (file)
@@ -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
 
index 1e98d20..76d6c85 100644 (file)
@@ -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 @@
       <groupId>com.github.spotbugs</groupId>
       <artifactId>spotbugs-annotations</artifactId>
     </dependency>
+    <!-- For parsing JSON object -->
     <dependency>
-      <!-- For parsing JSON object -->
       <groupId>com.google.code.gson</groupId>
       <artifactId>gson</artifactId>
     </dependency>
           <groupId>org.springframework</groupId>
           <artifactId>spring-web</artifactId>
       </dependency>
+    <!-- For parsing XML object -->
+      <dependency>
+          <groupId>com.fasterxml.jackson.dataformat</groupId>
+          <artifactId>jackson-dataformat-xml</artifactId>
+      </dependency>
   </dependencies>
 </project>
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 (file)
index 0000000..35c83b4
--- /dev/null
@@ -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
+            );
+        }
+    }
+}
@@ -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;
     }
 }
index be2c657..c5c141a 100644 (file)
@@ -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 (file)
index 0000000..1df63d9
--- /dev/null
@@ -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<T> {
+    @JacksonXmlElementWrapper(useWrapping = false)
+    @JacksonXmlProperty(localName = "deltaReport")
+    private List<T> 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 (file)
index 0000000..ef4c74f
--- /dev/null
@@ -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 = '<stores><bookstore><bookstore-name>Chapters</bookstore-name><categories><code>1</code><name>SciFi</name></categories></bookstore></stores>'
+        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'
+    }
+}
+
@@ -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
index 22e4f6b..4a2e13f 100644 (file)
@@ -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;