XML content support on get a node 55/137555/78
authorRudrangi Anupriya <ra00745022@techmahindra.com>
Fri, 1 Nov 2024 18:46:58 +0000 (00:16 +0530)
committerRudrangi Anupriya <ra00745022@techmahindra.com>
Tue, 5 Nov 2024 08:49:35 +0000 (08:49 +0000)
Here to bring Support for XML Response Entity In GET A NODE

- Made changes in components.yml to support contentType as
   application/xml
- Add ContentTypeInheadr in cpsDataV2.yml to support application/xml
- Add contentTypeInHeader parameter to accept xml  in
    DataRestController.java
- Implemented Logic to convert DataMaps To XML Data
- written testcase for above changes made

Issue-ID: CPS-2280
Change-Id: Ibe7ffb66ccbb03703626132c6d5c2eade0e7ab4b
Signed-off-by: Rudrangi Anupriya <ra00745022@techmahindra.com>
12 files changed:
cps-rest/docs/openapi/components.yml
cps-rest/docs/openapi/cpsDataV2.yml
cps-rest/src/main/java/org/onap/cps/rest/controller/DataRestController.java
cps-rest/src/test/groovy/org/onap/cps/rest/controller/DataRestControllerSpec.groovy
cps-service/pom.xml
cps-service/src/main/java/org/onap/cps/utils/ContentType.java
cps-service/src/main/java/org/onap/cps/utils/XmlFileUtils.java
cps-service/src/test/groovy/org/onap/cps/utils/ContentTypeSpec.groovy [new file with mode: 0644]
cps-service/src/test/groovy/org/onap/cps/utils/XmlFileUtilsSpec.groovy
docs/api/swagger/cps/openapi.yaml
docs/api/swagger/ncmp/openapi-inventory.yaml
docs/api/swagger/ncmp/openapi.yaml

index 40f0e17..728295f 100644 (file)
@@ -293,7 +293,9 @@ components:
       description: Content type in header
       schema:
         type: string
-        example: 'application/json'
+        enum:
+          - application/json
+          - application/xml
       required: true
     descendantsInQuery:
       name: descendants
index d5a8ef3..999c5b2 100644 (file)
@@ -28,6 +28,7 @@ nodeByDataspaceAndAnchor:
       - $ref: 'components.yml#/components/parameters/anchorNameInPath'
       - $ref: 'components.yml#/components/parameters/xpathInQuery'
       - $ref: 'components.yml#/components/parameters/descendantsInQuery'
+      - $ref: 'components.yml#/components/parameters/contentTypeInHeader'
     responses:
       '200':
         description: OK
@@ -38,6 +39,14 @@ nodeByDataspaceAndAnchor:
             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':
index 7390afc..6d22581 100755 (executable)
@@ -48,8 +48,8 @@ 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.MediaType;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
@@ -75,7 +75,7 @@ public class DataRestController implements CpsDataApi {
                                              final String contentTypeInHeader,
                                              final String nodeData, final String parentNodeXpath,
                                              final Boolean dryRunEnabled, final String observedTimestamp) {
-        final ContentType contentType = getContentTypeFromHeader(contentTypeInHeader);
+        final ContentType contentType = ContentType.fromString(contentTypeInHeader);
         if (Boolean.TRUE.equals(dryRunEnabled)) {
             cpsDataService.validateData(dataspaceName, anchorName, parentNodeXpath, nodeData, contentType);
             return ResponseEntity.ok().build();
@@ -105,7 +105,7 @@ public class DataRestController implements CpsDataApi {
                                                   final String anchorName, final String parentNodeXpath,
                                                   final String contentTypeInHeader, final String nodeData,
                                                   final String observedTimestamp) {
-        final ContentType contentType = getContentTypeFromHeader(contentTypeInHeader);
+        final ContentType contentType = ContentType.fromString(contentTypeInHeader);
         cpsDataService.saveListElements(dataspaceName, anchorName, parentNodeXpath,
                 nodeData, toOffsetDateTime(observedTimestamp), contentType);
         return new ResponseEntity<>(HttpStatus.CREATED);
@@ -129,8 +129,9 @@ public class DataRestController implements CpsDataApi {
     @Timed(value = "cps.data.controller.datanode.get.v2",
             description = "Time taken to get data node")
     public ResponseEntity<Object> getNodeByDataspaceAndAnchorV2(final String dataspaceName, final String anchorName,
-                                                                final String xpath,
+                                                                final String contentTypeInHeader, final String xpath,
                                                                 final String fetchDescendantsOptionAsString) {
+        final ContentType contentType = ContentType.fromString(contentTypeInHeader);
         final FetchDescendantsOption fetchDescendantsOption =
                 FetchDescendantsOption.getFetchDescendantsOption(fetchDescendantsOptionAsString);
         final Collection<DataNode> dataNodes = cpsDataService.getDataNodes(dataspaceName, anchorName, xpath,
@@ -142,7 +143,7 @@ public class DataRestController implements CpsDataApi {
             final Map<String, Object> dataMap = DataMapUtils.toDataMapWithIdentifier(dataNode, prefix);
             dataMaps.add(dataMap);
         }
-        return new ResponseEntity<>(jsonObjectMapper.asJsonString(dataMaps), HttpStatus.OK);
+        return buildResponseEntity(dataMaps, contentType);
     }
 
     @Override
@@ -150,7 +151,7 @@ public class DataRestController implements CpsDataApi {
                                                    final String anchorName, final String contentTypeInHeader,
                                                    final String nodeData, final String parentNodeXpath,
                                                    final String observedTimestamp) {
-        final ContentType contentType = getContentTypeFromHeader(contentTypeInHeader);
+        final ContentType contentType = ContentType.fromString(contentTypeInHeader);
         cpsDataService.updateNodeLeaves(dataspaceName, anchorName, parentNodeXpath,
                 nodeData, toOffsetDateTime(observedTimestamp), contentType);
         return new ResponseEntity<>(HttpStatus.OK);
@@ -161,7 +162,7 @@ public class DataRestController implements CpsDataApi {
                                              final String anchorName, final String contentTypeInHeader,
                                              final String nodeData, final String parentNodeXpath,
                                               final String observedTimestamp) {
-        final ContentType contentType = getContentTypeFromHeader(contentTypeInHeader);
+        final ContentType contentType = ContentType.fromString(contentTypeInHeader);
         cpsDataService.updateDataNodeAndDescendants(dataspaceName, anchorName, parentNodeXpath,
                         nodeData, toOffsetDateTime(observedTimestamp), contentType);
         return new ResponseEntity<>(HttpStatus.OK);
@@ -219,12 +220,19 @@ public class DataRestController implements CpsDataApi {
 
         final List<DeltaReport> deltaBetweenAnchors =
                 cpsDataService.getDeltaByDataspaceAndAnchors(dataspaceName, sourceAnchorName,
-                targetAnchorName, xpath, fetchDescendantsOption);
+                        targetAnchorName, xpath, fetchDescendantsOption);
         return new ResponseEntity<>(jsonObjectMapper.asJsonString(deltaBetweenAnchors), HttpStatus.OK);
     }
 
-    private static ContentType getContentTypeFromHeader(final String contentTypeInHeader) {
-        return contentTypeInHeader.contains(MediaType.APPLICATION_XML_VALUE) ? ContentType.XML : ContentType.JSON;
+    ResponseEntity<Object> buildResponseEntity(final List<Map<String, Object>> dataMaps,
+                                               final ContentType contentType) {
+        final String responseData;
+        if (contentType == ContentType.XML) {
+            responseData = XmlFileUtils.convertDataMapsToXml(dataMaps);
+        } else {
+            responseData = jsonObjectMapper.asJsonString(dataMaps);
+        }
+        return new ResponseEntity<>(responseData, HttpStatus.OK);
     }
 
     private static boolean isRootXpath(final String xpath) {
index 705c2fe..27738b0 100755 (executable)
@@ -314,7 +314,9 @@ class DataRestControllerSpec extends Specification {
             mockCpsDataService.getDataNodes(dataspaceName, anchorName, xpath, OMIT_DESCENDANTS) >> [dataNodeWithLeavesNoChildren, dataNodeWithLeavesNoChildren2]
         when: 'V2 of get request is performed through REST API'
             def response =
-                mvc.perform(get(endpoint).param('xpath', xpath))
+                mvc.perform(get(endpoint)
+                    .contentType(MediaType.APPLICATION_JSON)
+                    .param('xpath', xpath))
                     .andReturn().response
         then: 'a success response is returned'
             response.status == HttpStatus.OK.value()
@@ -326,6 +328,21 @@ class DataRestControllerSpec extends Specification {
             assert numberOfDataTrees == 2
     }
 
+    def 'Get all the data trees as XML with root node xPath using V2'() {
+        given: 'the service returns all data node leaves'
+            def xpath = '/'
+            def endpoint = "$dataNodeBaseEndpointV2/anchors/$anchorName/node"
+            mockCpsDataService.getDataNodes(dataspaceName, anchorName, xpath, OMIT_DESCENDANTS) >> [dataNodeWithLeavesNoChildren]
+        when: 'V2 of get request is performed through REST API with XML content type'
+            def response =
+                mvc.perform(get(endpoint).contentType(MediaType.APPLICATION_XML).param('xpath', xpath))
+                    .andReturn().response
+        then: 'a success response is returned'
+            response.status == HttpStatus.OK.value()
+        and: 'the response contains the datanode in XML format'
+            response.getContentAsString() == '<parent-1><leaf>value</leaf><leafList>leaveListElement1</leafList><leafList>leaveListElement2</leafList></parent-1>'
+    }
+
     def 'Get data node with #scenario using V2.'() {
         given: 'the service returns data nodes with #scenario'
             def xpath = 'some xPath'
@@ -335,6 +352,7 @@ class DataRestControllerSpec extends Specification {
             def response =
                 mvc.perform(
                     get(endpoint)
+                        .contentType(MediaType.APPLICATION_JSON)
                         .param('xpath', xpath)
                         .param('descendants', includeDescendantsOption))
                     .andReturn().response
@@ -361,6 +379,7 @@ class DataRestControllerSpec extends Specification {
             def response =
                 mvc.perform(
                     get(endpoint)
+                        .contentType(MediaType.APPLICATION_JSON)
                         .param('xpath', xpath)
                         .param('descendants', '2'))
                     .andReturn().response
index c7a3666..fdd6272 100644 (file)
@@ -5,6 +5,7 @@
   Modifications Copyright (C) 2021 Bell Canada.
   Modifications Copyright (C) 2021 Pantheon.tech
   Modifications Copyright (C) 2022 Deutsche Telekom AG
+  Modifications Copyright (C) 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.
       <artifactId>spring-kafka-test</artifactId>
       <scope>test</scope>
     </dependency>
+      <dependency>
+          <groupId>org.springframework</groupId>
+          <artifactId>spring-web</artifactId>
+      </dependency>
   </dependencies>
 </project>
index f888504..eb8e592 100644 (file)
@@ -1,6 +1,7 @@
 /*
  *  ============LICENSE_START=======================================================
  *  Copyright (C) 2022 Deutsche Telekom AG
+ *  Modifications Copyright (C) 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.
 
 package org.onap.cps.utils;
 
+import org.springframework.http.MediaType;
+
 public enum ContentType {
     JSON,
-    XML
+    XML;
+
+    public static ContentType fromString(final String contentTypeAsString) {
+        return contentTypeAsString.contains(MediaType.APPLICATION_XML_VALUE) ? XML : JSON;
+    }
+
 }
index 7a6d0bb..94b97bd 100644 (file)
@@ -2,6 +2,7 @@
  *  ============LICENSE_START=======================================================
  *  Copyright (C) 2022 Deutsche Telekom AG
  *  Modifications Copyright (C) 2023-2024 Nordix Foundation.
+ *  Modifications Copyright (C) 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.
 
 package org.onap.cps.utils;
 
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.StringWriter;
 import java.nio.charset.StandardCharsets;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -33,6 +36,7 @@ import javax.xml.XMLConstants;
 import javax.xml.parsers.DocumentBuilder;
 import javax.xml.parsers.DocumentBuilderFactory;
 import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.OutputKeys;
 import javax.xml.transform.Transformer;
 import javax.xml.transform.TransformerException;
 import javax.xml.transform.TransformerFactory;
@@ -40,10 +44,14 @@ import javax.xml.transform.dom.DOMSource;
 import javax.xml.transform.stream.StreamResult;
 import lombok.AccessLevel;
 import lombok.NoArgsConstructor;
+import org.onap.cps.spi.exceptions.DataValidationException;
 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
+import org.w3c.dom.DOMException;
 import org.w3c.dom.Document;
+import org.w3c.dom.DocumentFragment;
 import org.w3c.dom.Element;
+import org.w3c.dom.Node;
 import org.xml.sax.SAXException;
 
 @NoArgsConstructor(access = AccessLevel.PRIVATE)
@@ -156,6 +164,85 @@ public class XmlFileUtils {
         return document;
     }
 
+    /**
+     * Convert a list of data maps to XML format.
+     *
+     * @param dataMaps List of data maps to convert
+     * @return XML string representation of the data maps
+     */
+    @SuppressFBWarnings(value = "DCN_NULLPOINTER_EXCEPTION")
+    public static String convertDataMapsToXml(final List<Map<String, Object>> dataMaps) {
+        try {
+            final DocumentBuilder documentBuilder = getDocumentBuilderFactory().newDocumentBuilder();
+            final Document document = documentBuilder.newDocument();
+            final DocumentFragment documentFragment = document.createDocumentFragment();
+            for (final Map<String, Object> dataMap : dataMaps) {
+                createXmlElements(document, documentFragment, dataMap);
+            }
+            return transformFragmentToString(documentFragment);
+        } catch (final DOMException |  NullPointerException | ParserConfigurationException | TransformerException
+                exception) {
+            throw new DataValidationException(
+                    "Data Validation Failed", "Failed to parse xml data: " + exception.getMessage(), exception);
+        }
+    }
+
+    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);
+            } else {
+                appendObject(document, parentNode, mapEntry);
+            }
+        }
+    }
+
+    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());
+            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);
+                } else {
+                    listElement.appendChild(document.createTextNode(element.toString()));
+                }
+                parentNode.appendChild(listElement);
+            }
+        }
+    }
+
+    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());
+        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()));
+        parentNode.appendChild(element);
+    }
+
+    private static String transformFragmentToString(final DocumentFragment documentFragment)
+            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();
+    }
+
     private static DocumentBuilderFactory getDocumentBuilderFactory() {
 
         if (isNewDocumentBuilderFactoryInstance) {
diff --git a/cps-service/src/test/groovy/org/onap/cps/utils/ContentTypeSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/utils/ContentTypeSpec.groovy
new file mode 100644 (file)
index 0000000..cada33e
--- /dev/null
@@ -0,0 +1,37 @@
+/*\r
+ *  ============LICENSE_START=======================================================\r
+ *  Copyright (C) 2024 TechMahindra Ltd\r
+ *  ================================================================================\r
+ *  Licensed under the Apache License, Version 2.0 (the "License");\r
+ *  you may not use this file except in compliance with the License.\r
+ *  You may obtain a copy of the License at\r
+ *\r
+ *        http://www.apache.org/licenses/LICENSE-2.0\r
+ *  Unless required by applicable law or agreed to in writing, software\r
+ *  distributed under the License is distributed on an "AS IS" BASIS,\r
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ *  See the License for the specific language governing permissions and\r
+ *  limitations under the License.\r
+ *\r
+ *  SPDX-License-Identifier: Apache-2.0\r
+ *  ============LICENSE_END=========================================================\r
+ */\r
+\r
+package org.onap.cps.utils;\r
+\r
+import spock.lang.Specification;\r
+import org.springframework.http.MediaType\r
+\r
+\r
+class ContentTypeSpec extends Specification {\r
+\r
+    def 'Should return correct ContentType based on given input.'() {\r
+        given: 'contentType fromString method converts the input string as expectedContentType'\r
+             ContentType.fromString(contentTypeString) == expectedContentType\r
+        where:\r
+             contentTypeString                || expectedContentType\r
+             MediaType.APPLICATION_XML_VALUE  || ContentType.XML\r
+             MediaType.APPLICATION_JSON_VALUE || ContentType.JSON\r
+    }\r
+\r
+}\r
index dc6027d..3b21145 100644 (file)
@@ -2,6 +2,7 @@
  * ============LICENSE_START=======================================================
  *  Copyright (C) 2022 Deutsche Telekom AG
  *  Modifications Copyright (c) 2023-2024 Nordix Foundation
+ *  Modifications Copyright (C) 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.
 package org.onap.cps.utils
 
 import org.onap.cps.TestUtils
+import org.onap.cps.spi.exceptions.DataValidationException
 import org.onap.cps.yang.YangTextSchemaSourceSetBuilder
+import org.w3c.dom.DOMException
 import org.xml.sax.SAXParseException
 import spock.lang.Specification
 
+import static org.onap.cps.utils.XmlFileUtils.convertDataMapsToXml
+
 class XmlFileUtilsSpec extends Specification {
 
     def 'Parse a valid xml content #scenario'(){
@@ -68,4 +73,56 @@ class XmlFileUtilsSpec extends Specification {
             'without root data node' | '<?xml version="1.0" encoding="UTF-8"?><nest xmlns="org:onap:cps:test:test-tree"><name>Small</name><birds>Sparrow</birds></nest>'                                                          | '/test-tree/branch[@name=\'Branch\']' || '<?xml version="1.0" encoding="UTF-8"?><branch xmlns="org:onap:cps:test:test-tree"><name>Branch</name><nest><name>Small</name><birds>Sparrow</birds></nest></branch>'
     }
 
+    def 'Convert data maps to XML #scenario'() {
+        when: 'data maps are converted to XML'
+            def result = convertDataMapsToXml(dataMaps)
+        then: 'the result contains the expected XML'
+            assert result == expectedXmlOutput
+        where:
+            scenario                              | dataMaps                                                                                                                                 || expectedXmlOutput
+            'single XML branch'                   | [['branch': ['name': 'Left', 'nest': ['name': 'Small', 'birds': ['Sparrow', 'Owl']]]]]                                                   || '<branch><name>Left</name><nest><name>Small</name><birds>Sparrow</birds><birds>Owl</birds></nest></branch>'
+            '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'() {
+        when: 'data maps with empty content are converted to XML'
+            def result = convertDataMapsToXml(dataMaps)
+        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>'
+    }
+
+    def 'Converting data maps to xml with no data'() {
+        given: 'A list of maps where entry is null'
+            def dataMapWithNull = [null]
+        when: 'convert the dataMaps to XML'
+            convertDataMapsToXml(dataMapWithNull)
+        then: 'a validation exception is thrown'
+            def exception = thrown(DataValidationException)
+        and:'the cause is a null pointer exception'
+            assert exception.cause instanceof NullPointerException
+    }
+
+    def 'Converting data maps to xml with document syntax error'() {
+        given: 'A list of maps with an invalid entry'
+            def dataMap = [['invalid<tag>': 'value']]
+        when: 'convert the dataMaps to XML'
+            convertDataMapsToXml(dataMap)
+        then: 'a validation exception is thrown'
+            def exception = thrown(DataValidationException)
+        and:'the cause is a document object model exception'
+            assert exception.cause instanceof DOMException
+
+    }
+
 }
index 3069b18..3f889c1 100644 (file)
@@ -1162,6 +1162,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:
@@ -1172,6 +1181,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:
@@ -1347,7 +1365,9 @@ paths:
         name: Content-Type
         required: true
         schema:
-          example: application/json
+          enum:
+          - application/json
+          - application/xml
           type: string
       requestBody:
         content:
@@ -1472,7 +1492,9 @@ paths:
         name: Content-Type
         required: true
         schema:
-          example: application/json
+          enum:
+          - application/json
+          - application/xml
           type: string
       requestBody:
         content:
@@ -1597,7 +1619,9 @@ paths:
         name: Content-Type
         required: true
         schema:
-          example: application/json
+          enum:
+          - application/json
+          - application/xml
           type: string
       requestBody:
         content:
@@ -1788,7 +1812,9 @@ paths:
         name: Content-Type
         required: true
         schema:
-          example: application/json
+          enum:
+          - application/json
+          - application/xml
           type: string
       requestBody:
         content:
@@ -2543,6 +2569,16 @@ components:
         default: none
         example: "3"
         type: string
+    contentTypeInHeader:
+      description: Content type in header
+      in: header
+      name: Content-Type
+      required: true
+      schema:
+        enum:
+        - application/json
+        - application/xml
+        type: string
     observedTimestampInQuery:
       description: observed-timestamp
       in: query
@@ -2551,14 +2587,6 @@ components:
       schema:
         example: 2021-03-21T00:10:34.030-0100
         type: string
-    contentTypeInHeader:
-      description: Content type in header
-      in: header
-      name: Content-Type
-      required: true
-      schema:
-        example: application/json
-        type: string
     dryRunInQuery:
       description: "Boolean flag to validate data, without persisting it. Default\
         \ value is set to false."
index a2c7af6..8552ad5 100644 (file)
@@ -131,9 +131,19 @@ paths:
       - network-cm-proxy-inventory
   /v1/ch/searches:
     post:
-      description: "Query and get CMHandleIds for additional properties, public properties\
-        \ and registered DMI plugin (DMI plugin, DMI data plugin, DMI model plugin)."
+      description: "Query and get CMHandle references for additional properties, public\
+        \ properties and registered DMI plugin (DMI plugin, DMI data plugin, DMI model\
+        \ plugin)."
       operationId: searchCmHandleIds
+      parameters:
+      - description: Boolean parameter to determine if returned value(s) will be cmHandle
+          Ids or Alternate Ids for a given query
+        in: query
+        name: outputAlternateId
+        required: false
+        schema:
+          default: false
+          type: boolean
       requestBody:
         content:
           application/json:
@@ -182,6 +192,15 @@ components:
       schema:
         example: my-dmi-plugin
         type: string
+    outputAlternateIdOptionInQuery:
+      description: Boolean parameter to determine if returned value(s) will be cmHandle
+        Ids or Alternate Ids for a given query
+      in: query
+      name: outputAlternateId
+      required: false
+      schema:
+        default: false
+        type: boolean
   responses:
     NoContent:
       content: {}
index f93395a..aa732c8 100644 (file)
@@ -1129,7 +1129,7 @@ paths:
       - network-cm-proxy
   /v1/ch/id-searches:
     post:
-      description: Execute cm handle query search and return a list of cm handle ids.
+      description: Execute cm handle query search and return a list of cm handle references.
         Any number of conditions can be applied. To be included in the result a cm-handle
         must fulfill ALL the conditions. An empty collection will be returned in the
         case that the cm handle does not match a condition. For more on cm handle
@@ -1140,6 +1140,15 @@ paths:
         Path Read the Docs</a>. The cm handle ancestor is automatically returned for
         this query.
       operationId: searchCmHandleIds
+      parameters:
+      - description: Boolean parameter to determine if returned value(s) will be cmHandle
+          Ids or Alternate Ids for a given query
+        in: query
+        name: outputAlternateId
+        required: false
+        schema:
+          default: false
+          type: boolean
       requestBody:
         content:
           application/json:
@@ -1608,6 +1617,15 @@ components:
       schema:
         example: 2024-01-22
         type: string
+    outputAlternateIdOptionInQuery:
+      description: Boolean parameter to determine if returned value(s) will be cmHandle
+        Ids or Alternate Ids for a given query
+      in: query
+      name: outputAlternateId
+      required: false
+      schema:
+        default: false
+        type: boolean
     dataSyncEnabled:
       description: Is used to enable or disable the data synchronization flag
       in: query