XML content support on replace a node with descendants 59/138259/16
authorRudrangi Anupriya <ra00745022@techmahindra.com>
Thu, 11 Jul 2024 16:26:24 +0000 (21:56 +0530)
committerRudrangi Anupriya <ra00745022@techmahindra.com>
Tue, 16 Jul 2024 11:00:11 +0000 (11:00 +0000)
Issue-ID: CPS-2282
Change-Id: Ibb7ffb65ccbb03703266712c6d5c2eade0e7ab4b
Signed-off-by: Rudrangi Anupriya <ra00745022@techmahindra.com>
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/inventory/InventoryPersistenceImpl.java
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/inventory/InventoryPersistenceImplSpec.groovy
cps-rest/docs/openapi/components.yml
cps-rest/docs/openapi/cpsData.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/src/main/java/org/onap/cps/api/CpsDataService.java
cps-service/src/main/java/org/onap/cps/api/impl/CpsDataServiceImpl.java
cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy
integration-test/src/test/groovy/org/onap/cps/integration/functional/cps/DataServiceIntegrationSpec.groovy
integration-test/src/test/groovy/org/onap/cps/integration/performance/cps/UpdatePerfTest.groovy

index 2930d4f..cd1237b 100644 (file)
@@ -2,7 +2,7 @@
  *  ============LICENSE_START=======================================================
  *  Copyright (C) 2022-2024 Nordix Foundation
  *  Modifications Copyright (C) 2022 Bell Canada
- *  Modifications Copyright (C) 2023 TechMahindra Ltd.
+ *  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.
@@ -47,6 +47,7 @@ import org.onap.cps.spi.model.DataNode;
 import org.onap.cps.spi.model.ModuleDefinition;
 import org.onap.cps.spi.model.ModuleReference;
 import org.onap.cps.spi.utils.CpsValidator;
+import org.onap.cps.utils.ContentType;
 import org.onap.cps.utils.JsonObjectMapper;
 import org.springframework.stereotype.Component;
 
@@ -95,7 +96,7 @@ public class InventoryPersistenceImpl extends NcmpPersistenceImpl implements Inv
     public void saveCmHandleState(final String cmHandleId, final CompositeState compositeState) {
         final String cmHandleJsonData = createStateJsonData(jsonObjectMapper.asJsonString(compositeState));
         cpsDataService.updateDataNodeAndDescendants(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,
-                getXPathForCmHandleById(cmHandleId), cmHandleJsonData, OffsetDateTime.now());
+                getXPathForCmHandleById(cmHandleId), cmHandleJsonData, OffsetDateTime.now(), ContentType.JSON);
     }
 
     @Override
@@ -105,7 +106,7 @@ public class InventoryPersistenceImpl extends NcmpPersistenceImpl implements Inv
                 getXPathForCmHandleById(cmHandleId),
                 createStateJsonData(jsonObjectMapper.asJsonString(compositeState))));
         cpsDataService.updateDataNodesAndDescendants(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR,
-                cmHandlesJsonDataMap, OffsetDateTime.now());
+                cmHandlesJsonDataMap, OffsetDateTime.now(), ContentType.JSON);
     }
 
     @Override
index d58a1df..e098fb8 100644 (file)
@@ -2,7 +2,7 @@
  *  ============LICENSE_START=======================================================
  *  Copyright (C) 2022-2024 Nordix Foundation
  *  Modifications Copyright (C) 2022 Bell Canada
- *  Modifications Copyright (C) 2023 TechMahindra Ltd.
+ *  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.
@@ -36,6 +36,7 @@ import org.onap.cps.spi.model.DataNode
 import org.onap.cps.spi.model.ModuleDefinition
 import org.onap.cps.spi.model.ModuleReference
 import org.onap.cps.spi.utils.CpsValidator
+import org.onap.cps.utils.ContentType
 import org.onap.cps.utils.JsonObjectMapper
 import spock.lang.Shared
 import spock.lang.Specification
@@ -166,7 +167,7 @@ class InventoryPersistenceImplSpec extends Specification {
         when: 'update cm handle state is invoked with the #scenario state'
             objectUnderTest.saveCmHandleState(cmHandleId, compositeState)
         then: 'update node leaves is invoked with the correct params'
-            1 * mockCpsDataService.updateDataNodeAndDescendants(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle\']', expectedJsonData, _ as OffsetDateTime)
+            1 * mockCpsDataService.updateDataNodeAndDescendants(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle\']', expectedJsonData, _ as OffsetDateTime, ContentType.JSON)
         where: 'the following states are used'
             scenario    | cmHandleState          || expectedJsonData
             'READY'     | CmHandleState.READY    || '{"state":{"cm-handle-state":"READY","last-update-time":"2022-12-31T20:30:40.000+0000"}}'
@@ -182,7 +183,7 @@ class InventoryPersistenceImplSpec extends Specification {
             def cmHandleStateMap = ['Some-Cm-Handle1' : compositeState1, 'Some-Cm-Handle2' : compositeState2]
             objectUnderTest.saveCmHandleStateBatch(cmHandleStateMap)
         then: 'update node leaves is invoked with the correct params'
-            1 * mockCpsDataService.updateDataNodesAndDescendants(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, cmHandlesJsonDataMap, _ as OffsetDateTime)
+            1 * mockCpsDataService.updateDataNodesAndDescendants(NCMP_DATASPACE_NAME, NCMP_DMI_REGISTRY_ANCHOR, cmHandlesJsonDataMap, _ as OffsetDateTime, ContentType.JSON)
         where: 'the following states are used'
             scenario    | cmHandleState          || cmHandlesJsonDataMap
             'READY'     | CmHandleState.READY    || ['/dmi-registry/cm-handles[@id=\'Some-Cm-Handle1\']':'{"state":{"cm-handle-state":"READY","last-update-time":"2022-12-31T20:30:40.000+0000"}}', '/dmi-registry/cm-handles[@id=\'Some-Cm-Handle2\']':'{"state":{"cm-handle-state":"READY","last-update-time":"2022-12-31T20:30:40.000+0000"}}']
index 01375f5..99502e3 100644 (file)
@@ -279,10 +279,10 @@ components:
         type: string
         enum: [v1, v2]
         default: v2
-    contentTypeHeader:
+    contentTypeInHeader:
       name: Content-Type
       in: header
-      description: Content type header
+      description: Content type in header
       schema:
         type: string
         example: 'application/json'
index d13c285..80d07c8 100644 (file)
@@ -94,7 +94,7 @@ nodesByDataspaceAndAnchor:
       - $ref: 'components.yml#/components/parameters/anchorNameInPath'
       - $ref: 'components.yml#/components/parameters/xpathInQuery'
       - $ref: 'components.yml#/components/parameters/observedTimestampInQuery'
-      - $ref: 'components.yml#/components/parameters/contentTypeHeader'
+      - $ref: 'components.yml#/components/parameters/contentTypeInHeader'
     requestBody:
       required: true
       content:
@@ -137,7 +137,7 @@ nodesByDataspaceAndAnchor:
       - $ref: 'components.yml#/components/parameters/anchorNameInPath'
       - $ref: 'components.yml#/components/parameters/xpathInQuery'
       - $ref: 'components.yml#/components/parameters/observedTimestampInQuery'
-      - $ref: 'components.yml#/components/parameters/contentTypeHeader'
+      - $ref: 'components.yml#/components/parameters/contentTypeInHeader'
     requestBody:
       required: true
       content:
@@ -197,15 +197,24 @@ nodesByDataspaceAndAnchor:
       - $ref: 'components.yml#/components/parameters/anchorNameInPath'
       - $ref: 'components.yml#/components/parameters/xpathInQuery'
       - $ref: 'components.yml#/components/parameters/observedTimestampInQuery'
+      - $ref: 'components.yml#/components/parameters/contentTypeInHeader'
     requestBody:
       required: true
       content:
         application/json:
           schema:
-            type: object
+            type: string
           examples:
             dataSample:
               $ref: 'components.yml#/components/examples/dataSample'
+        application/xml:
+          schema:
+            type: object
+            xml:
+              name: stores
+          examples:
+            dataSample:
+              $ref: 'components.yml#/components/examples/dataSampleXml'
     responses:
       '200':
         $ref: 'components.yml#/components/responses/Ok'
index f579c82..6100b7e 100755 (executable)
@@ -49,7 +49,6 @@ import org.onap.cps.utils.PrefixResolver;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.MediaType;
 import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.RequestHeader;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 import org.springframework.web.multipart.MultipartFile;
@@ -70,11 +69,10 @@ public class DataRestController implements CpsDataApi {
     @Override
     public ResponseEntity<String> createNode(final String apiVersion,
                                              final String dataspaceName, final String anchorName,
-                                             @RequestHeader(value = "Content-Type") final String contentTypeHeader,
+                                             final String contentTypeInHeader,
                                              final String nodeData, final String parentNodeXpath,
                                              final String observedTimestamp) {
-        final ContentType contentType = contentTypeHeader.contains(MediaType.APPLICATION_XML_VALUE) ? ContentType.XML
-                : ContentType.JSON;
+        final ContentType contentType = getContentTypeFromHeader(contentTypeInHeader);
         if (isRootXpath(parentNodeXpath)) {
             cpsDataService.saveData(dataspaceName, anchorName, nodeData,
                     toOffsetDateTime(observedTimestamp), contentType);
@@ -137,23 +135,23 @@ public class DataRestController implements CpsDataApi {
 
     @Override
     public ResponseEntity<Object> updateNodeLeaves(final String apiVersion, final String dataspaceName,
-                                                   final String anchorName, final String contentTypeHeader,
+                                                   final String anchorName, final String contentTypeInHeader,
                                                    final String nodeData, final String parentNodeXpath,
                                                    final String observedTimestamp) {
-        final ContentType contentType = contentTypeHeader.contains(MediaType.APPLICATION_XML_VALUE) ? ContentType.XML
-                : ContentType.JSON;
+        final ContentType contentType = getContentTypeFromHeader(contentTypeInHeader);
         cpsDataService.updateNodeLeaves(dataspaceName, anchorName, parentNodeXpath,
                 nodeData, toOffsetDateTime(observedTimestamp), contentType);
         return new ResponseEntity<>(HttpStatus.OK);
     }
 
     @Override
-    public ResponseEntity<Object> replaceNode(final String apiVersion,
-        final String dataspaceName, final String anchorName,
-        final Object jsonData, final String parentNodeXpath, final String observedTimestamp) {
-        cpsDataService
-                .updateDataNodeAndDescendants(dataspaceName, anchorName, parentNodeXpath,
-                        jsonObjectMapper.asJsonString(jsonData), toOffsetDateTime(observedTimestamp));
+    public ResponseEntity<Object> replaceNode(final String apiVersion, final String dataspaceName,
+                                             final String anchorName, final String contentTypeInHeader,
+                                             final String nodeData, final String parentNodeXpath,
+                                              final String observedTimestamp) {
+        final ContentType contentType = getContentTypeFromHeader(contentTypeInHeader);
+        cpsDataService.updateDataNodeAndDescendants(dataspaceName, anchorName, parentNodeXpath,
+                        nodeData, toOffsetDateTime(observedTimestamp), contentType);
         return new ResponseEntity<>(HttpStatus.OK);
     }
 
@@ -213,6 +211,10 @@ public class DataRestController implements CpsDataApi {
         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;
+    }
+
     private static boolean isRootXpath(final String xpath) {
         return ROOT_XPATH.equals(xpath);
     }
index 317b9c5..205d85d 100755 (executable)
@@ -456,19 +456,22 @@ class DataRestControllerSpec extends Specification {
             def response =
                 mvc.perform(
                     put(endpoint)
-                        .contentType(MediaType.APPLICATION_JSON)
-                        .content(requestBodyJson)
+                        .contentType(contentType)
+                        .content(requestBody)
                         .param('xpath', inputXpath))
                     .andReturn().response
         then: 'the service method is invoked with expected parameters'
-            1 * mockCpsDataService.updateDataNodeAndDescendants(dataspaceName, anchorName, xpathServiceParameter, expectedJsonData, noTimestamp)
+            1 * mockCpsDataService.updateDataNodeAndDescendants(dataspaceName, anchorName, xpathServiceParameter, expectedData, noTimestamp, expectedContentType)
         and: 'response status indicates success'
             response.status == HttpStatus.OK.value()
         where:
-            scenario               | inputXpath    || xpathServiceParameter
-            'root node by default' | ''            || '/'
-            'root node by choice'  | '/'           || '/'
-            'some xpath by parent' | '/some/xpath' || '/some/xpath'
+            scenario                             | inputXpath    | contentType                || xpathServiceParameter | requestBody     | expectedData     | expectedContentType
+            'JSON content: root node by default' | ''            | MediaType.APPLICATION_JSON || '/'                   | requestBodyJson | expectedJsonData | ContentType.JSON
+            'JSON content: root node by choice'  | '/'           | MediaType.APPLICATION_JSON || '/'                   | requestBodyJson | expectedJsonData | ContentType.JSON
+            'JSON content: some xpath by parent' | '/some/xpath' | MediaType.APPLICATION_JSON || '/some/xpath'         | requestBodyJson | expectedJsonData | ContentType.JSON
+            'XML content: root node by default'  | ''            | MediaType.APPLICATION_XML  || '/'                   | requestBodyXml  | expectedXmlData  | ContentType.XML
+            'XML content: root node by choice'   | '/'           | MediaType.APPLICATION_XML  || '/'                   | requestBodyXml  | expectedXmlData  | ContentType.XML
+            'XML content: some xpath by parent'  | '/some/xpath' | MediaType.APPLICATION_XML  || '/some/xpath'         | requestBodyXml  | expectedXmlData  | ContentType.XML
     }
 
     def 'Update data node and descendants with observedTimestamp.'() {
@@ -485,7 +488,7 @@ class DataRestControllerSpec extends Specification {
                     .andReturn().response
         then: 'the service method is invoked with expected parameters'
             expectedApiCount * mockCpsDataService.updateDataNodeAndDescendants(dataspaceName, anchorName, '/', expectedJsonData,
-                { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
+                { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) }, ContentType.JSON)
         and: 'response status indicates success'
             response.status == expectedHttpStatus.value()
         where:
index f396b49..cfa5f2d 100644 (file)
@@ -55,7 +55,7 @@ public interface CpsDataService {
      * @param anchorName    anchor name
      * @param nodeData      node data
      * @param observedTimestamp observedTimestamp
-     * @param contentType       node data content type
+     * @param contentType       JSON/XML content type
      */
     void saveData(String dataspaceName, String anchorName, String nodeData, OffsetDateTime observedTimestamp,
                   ContentType contentType);
@@ -80,7 +80,7 @@ public interface CpsDataService {
      * @param parentNodeXpath   parent node xpath
      * @param nodeData          node data
      * @param observedTimestamp observedTimestamp
-     * @param contentType       node data content type
+     * @param contentType       JSON/XML content type
      *
      */
     void saveData(String dataspaceName, String anchorName, String parentNodeXpath, String nodeData,
@@ -134,7 +134,7 @@ public interface CpsDataService {
      * @param parentNodeXpath xpath to parent node
      * @param nodeData        node data
      * @param observedTimestamp observedTimestamp
-     * @param contentType       node data content type
+     * @param contentType       JSON/XML content type
      */
     void updateNodeLeaves(String dataspaceName, String anchorName, String parentNodeXpath, String nodeData,
         OffsetDateTime observedTimestamp, ContentType contentType);
@@ -145,22 +145,24 @@ public interface CpsDataService {
      * @param dataspaceName     dataspace name
      * @param anchorName        anchor name
      * @param parentNodeXpath   xpath to parent node
-     * @param jsonData          json data
+     * @param nodeData          node data
      * @param observedTimestamp observedTimestamp
+     * @param contentType       JSON/XML content type
      */
-    void updateDataNodeAndDescendants(String dataspaceName, String anchorName, String parentNodeXpath, String jsonData,
-                                       OffsetDateTime observedTimestamp);
+    void updateDataNodeAndDescendants(String dataspaceName, String anchorName, String parentNodeXpath, String nodeData,
+                                       OffsetDateTime observedTimestamp, ContentType contentType);
 
     /**
      * Replaces multiple existing data nodes' content including descendants in a batch operation.
      *
      * @param dataspaceName   dataspace name
      * @param anchorName      anchor name
-     * @param nodesJsonData   map of xpath and node JSON data
+     * @param nodeDataPerXPath   map of xpath and node JSON/XML data
      * @param observedTimestamp observedTimestamp
+     * @param contentType       JSON/XML content type
      */
-    void updateDataNodesAndDescendants(String dataspaceName, String anchorName, Map<String, String> nodesJsonData,
-                                       OffsetDateTime observedTimestamp);
+    void updateDataNodesAndDescendants(String dataspaceName, String anchorName, Map<String, String> nodeDataPerXPath,
+                                       OffsetDateTime observedTimestamp, ContentType contentType);
 
     /**
      * Replaces list content by removing all existing elements and inserting the given new elements as json
index 5a48428..c65bc5e 100644 (file)
@@ -251,12 +251,12 @@ public class CpsDataServiceImpl implements CpsDataService {
     @Timed(value = "cps.data.service.datanode.descendants.update",
         description = "Time taken to update a data node and descendants")
     public void updateDataNodeAndDescendants(final String dataspaceName, final String anchorName,
-                                             final String parentNodeXpath, final String jsonData,
-                                             final OffsetDateTime observedTimestamp) {
+                                             final String parentNodeXpath, final String nodeData,
+                                             final OffsetDateTime observedTimestamp, final ContentType contentType) {
         cpsValidator.validateNameCharacters(dataspaceName, anchorName);
         final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName);
         final Collection<DataNode> dataNodes =
-                buildDataNodesWithParentNodeXpath(anchor, parentNodeXpath, jsonData, ContentType.JSON);
+                buildDataNodesWithParentNodeXpath(anchor, parentNodeXpath, nodeData, contentType);
         cpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName, dataNodes);
         sendDataUpdatedEvent(anchor, parentNodeXpath, Operation.UPDATE, observedTimestamp);
     }
@@ -265,13 +265,13 @@ public class CpsDataServiceImpl implements CpsDataService {
     @Timed(value = "cps.data.service.datanode.descendants.batch.update",
         description = "Time taken to update a batch of data nodes and descendants")
     public void updateDataNodesAndDescendants(final String dataspaceName, final String anchorName,
-                                              final Map<String, String> nodesJsonData,
-                                              final OffsetDateTime observedTimestamp) {
+                                              final Map<String, String> nodeDataPerXPath,
+                                              final OffsetDateTime observedTimestamp, final ContentType contentType) {
         cpsValidator.validateNameCharacters(dataspaceName, anchorName);
         final Anchor anchor = cpsAnchorService.getAnchor(dataspaceName, anchorName);
-        final Collection<DataNode> dataNodes = buildDataNodesWithParentNodeXpath(anchor, nodesJsonData);
+        final Collection<DataNode> dataNodes = buildDataNodesWithParentNodeXpath(anchor, nodeDataPerXPath, contentType);
         cpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName, dataNodes);
-        nodesJsonData.keySet().forEach(nodeXpath ->
+        nodeDataPerXPath.keySet().forEach(nodeXpath ->
                 sendDataUpdatedEvent(anchor, nodeXpath, Operation.UPDATE, observedTimestamp));
     }
 
@@ -408,11 +408,12 @@ public class CpsDataServiceImpl implements CpsDataService {
     }
 
     private Collection<DataNode> buildDataNodesWithParentNodeXpath(final Anchor anchor,
-                                                                   final Map<String, String> nodesJsonData) {
+                                                                   final Map<String, String> nodesJsonData,
+                                                                   final ContentType contentType) {
         final Collection<DataNode> dataNodes = new ArrayList<>();
         for (final Map.Entry<String, String> nodeJsonData : nodesJsonData.entrySet()) {
             dataNodes.addAll(buildDataNodesWithParentNodeXpath(anchor, nodeJsonData.getKey(),
-                    nodeJsonData.getValue(), ContentType.JSON));
+                    nodeJsonData.getValue(), contentType));
         }
         return dataNodes;
     }
index 4e5807e..2e38d79 100644 (file)
@@ -362,11 +362,11 @@ class CpsDataServiceImplSpec extends Specification {
             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
     }
 
-    def 'Replace data node using singular data node: #scenario.'() {
+    def 'Replace data node using singular JSON data node: #scenario.'() {
         given: 'schema set for given anchor and dataspace references test-tree model'
             setupSchemaSetMocks('test-tree.yang')
         when: 'replace data method is invoked with json data #jsonData and parent node xpath #parentNodeXpath'
-            objectUnderTest.updateDataNodeAndDescendants(dataspaceName, anchorName, parentNodeXpath, jsonData, observedTimestamp)
+            objectUnderTest.updateDataNodeAndDescendants(dataspaceName, anchorName, parentNodeXpath, jsonData, observedTimestamp, ContentType.JSON)
         then: 'the persistence service method is invoked with correct parameters'
             1 * mockCpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName,
                     { dataNode -> dataNode.xpath == expectedNodeXpath})
@@ -379,30 +379,63 @@ class CpsDataServiceImplSpec extends Specification {
             'json list'      | '/test-tree'    | '{"branch": [{"name":"Name1"}, {"name":"Name2"}]}' || ["/test-tree/branch[@name='Name1']", "/test-tree/branch[@name='Name2']"]
     }
 
-    def 'Replace data node using multiple data nodes: #scenario.'() {
+    def 'Replace data node using singular XML data node: #scenario.'() {
+        given: 'schema set for given anchor and dataspace references test-tree model'
+            setupSchemaSetMocks('test-tree.yang')
+        when: 'replace data method is invoked with XML data #xmlData and parent node xpath #parentNodeXpath'
+            objectUnderTest.updateDataNodeAndDescendants(dataspaceName, anchorName, parentNodeXpath, xmlData, observedTimestamp, ContentType.XML)
+        then: 'the persistence service method is invoked with correct parameters'
+            1 * mockCpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName,
+                { dataNode -> dataNode.xpath == expectedNodeXpath })
+        and: 'the CpsValidator is called on the dataspaceName and AnchorName'
+            1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
+        where: 'following parameters were used'
+            scenario       | parentNodeXpath | xmlData                                                                                                                                  || expectedNodeXpath
+            'level 2 node' | '/test-tree'    | '<branch><name>Name</name></branch>'                                                                                                     || ['/test-tree/branch[@name=\'Name\']']
+            'xml list'     | '/test-tree'    | '<test-tree xmlns="org:onap:cps:test:test-tree"><branch><name>Name1</name></branch>' + '<branch><name>Name2</name></branch></test-tree>' || ["/test-tree/branch[@name='Name1']", "/test-tree/branch[@name='Name2']"]
+    }
+
+    def 'Replace data node using multiple JSON data nodes: #scenario.'() {
         given: 'schema set for given anchor and dataspace references test-tree model'
             setupSchemaSetMocks('test-tree.yang')
         when: 'replace data method is invoked with a map of xpaths and json data'
-            objectUnderTest.updateDataNodesAndDescendants(dataspaceName, anchorName, nodesJsonData, observedTimestamp)
+            objectUnderTest.updateDataNodesAndDescendants(dataspaceName, anchorName, nodeDataPerXPath, observedTimestamp, ContentType.JSON)
         then: 'the persistence service method is invoked with correct parameters'
             1 * mockCpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName,
                 { dataNode -> dataNode.xpath == expectedNodeXpath})
         and: 'the CpsValidator is called on the dataspaceName and AnchorName'
             1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
         where: 'following parameters were used'
-            scenario         | nodesJsonData                                                                                                        || expectedNodeXpath
+            scenario         | nodeDataPerXPath                                                                                                     || expectedNodeXpath
             'top level node' | ['/' : '{"test-tree": {"branch": []}}', '/test-tree' : '{"branch": [{"name":"Name"}]}']                              || ["/test-tree", "/test-tree/branch[@name='Name']"]
             'level 2 node'   | ['/test-tree' : '{"branch": [{"name":"Name"}]}', '/test-tree/branch[@name=\'Name\']':'{"nest":{"name":"nestName"}}'] || ["/test-tree/branch[@name='Name']", "/test-tree/branch[@name='Name']/nest"]
             'json list'      | ['/test-tree' : '{"branch": [{"name":"Name1"}, {"name":"Name2"}]}']                                                  || ["/test-tree/branch[@name='Name1']", "/test-tree/branch[@name='Name2']"]
     }
 
+    def 'Replace data node using multiple XML data nodes: #scenario.'() {
+        given: 'schema set for given anchor and dataspace references test-tree model'
+            setupSchemaSetMocks('test-tree.yang')
+        when: 'replace data method is invoked with a map of xpaths and XML data'
+            objectUnderTest.updateDataNodesAndDescendants(dataspaceName, anchorName, nodeXmlDataPerXPath, observedTimestamp, ContentType.XML)
+        then: 'the persistence service method is invoked with correct parameters'
+            1 * mockCpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName,
+                { dataNode -> dataNode.xpath == expectedNodeXpath })
+        and: 'the CpsValidator is called on the dataspaceName and AnchorName'
+            1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
+        where: 'following parameters were used'
+            scenario         | nodeXmlDataPerXPath                                                                                                                                      || expectedNodeXpath
+            'top level node' | ['/test-tree': '<branch><name>Name</name></branch>']                                                                                                     || ["/test-tree/branch[@name='Name']"]
+            'level 2 node'   | ['/test-tree': '<branch><name>Name</name></branch>', '/test-tree/branch[@name=\'Name\']': '<nest><name>nestName</name></nest>']                          || ["/test-tree/branch[@name='Name']", "/test-tree/branch[@name='Name']/nest"]
+            'xml list'       | ['/test-tree': '<test-tree xmlns="org:onap:cps:test:test-tree"><branch><name>Name1</name></branch>' + '<branch><name>Name2</name></branch></test-tree>'] || ["/test-tree/branch[@name='Name1']", "/test-tree/branch[@name='Name2']"]
+    }
+
     def 'Replace data node with concurrency exception in persistence layer.'() {
         given: 'the persistence layer throws an concurrency exception'
             def originalException = new ConcurrencyException('message', 'details')
             mockCpsDataPersistenceService.updateDataNodesAndDescendants(*_) >> { throw originalException }
             setupSchemaSetMocks('test-tree.yang')
         when: 'attempt to replace data node'
-            objectUnderTest.updateDataNodesAndDescendants(dataspaceName, anchorName, ['/' : '{"test-tree": {}}'] , observedTimestamp)
+            objectUnderTest.updateDataNodesAndDescendants(dataspaceName, anchorName, ['/' : '{"test-tree": {}}'] , observedTimestamp, ContentType.JSON)
         then: 'the same exception is thrown up'
             def thrownUp = thrown(ConcurrencyException)
             assert thrownUp == originalException
index a488b3b..869b72d 100644 (file)
@@ -396,7 +396,7 @@ class DataServiceIntegrationSpec extends FunctionalSpecBase {
             objectUnderTest.saveData(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1 , '/bookstore', json, now)
         when: 'the webinfo (container) is updated'
             json = '{"webinfo": {"domain-name":"newdomain.com" ,"contact-email":"info@newdomain.com" }}'
-            objectUnderTest.updateDataNodeAndDescendants(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/bookstore', json, now)
+            objectUnderTest.updateDataNodeAndDescendants(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/bookstore', json, now, ContentType.JSON)
         then: 'webinfo has been updated with teh new details'
             def result = objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/bookstore/webinfo', DIRECT_CHILDREN_ONLY)
             result.leaves.'domain-name'[0] == 'newdomain.com'
@@ -408,7 +408,7 @@ class DataServiceIntegrationSpec extends FunctionalSpecBase {
     def 'Update bookstore top-level container data node.'() {
         when: 'the bookstore top-level container is updated'
             def json = '{ "bookstore": { "bookstore-name": "new bookstore" }}'
-            objectUnderTest.updateDataNodeAndDescendants(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/', json, now)
+            objectUnderTest.updateDataNodeAndDescendants(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/', json, now, ContentType.JSON)
         then: 'bookstore name has been updated'
             def result = objectUnderTest.getDataNodes(FUNCTIONAL_TEST_DATASPACE_1, BOOKSTORE_ANCHOR_1, '/bookstore', DIRECT_CHILDREN_ONLY)
             result.leaves.'bookstore-name'[0] == 'new bookstore'
index 7bcec96..03abdb4 100644 (file)
@@ -50,7 +50,7 @@ class UpdatePerfTest extends CpsPerfTestBase {
         given: 'replacement JSON for node containing list of device nodes'
             def jsonData = '{ "openroadm-devices": ' + generateJsonForOpenRoadmDevices(startId, totalNodes, changeLeaves) + '}'
         when: 'the container node is updated'
-            objectUnderTest.updateDataNodeAndDescendants(CPS_PERFORMANCE_TEST_DATASPACE, UPDATE_TEST_ANCHOR, '/', jsonData, now)
+            objectUnderTest.updateDataNodeAndDescendants(CPS_PERFORMANCE_TEST_DATASPACE, UPDATE_TEST_ANCHOR, '/', jsonData, now, ContentType.JSON)
         then: 'there are the expected number of total nodes'
             assert totalNodes == countDataNodes('/openroadm-devices/openroadm-device')
         where:
@@ -68,7 +68,7 @@ class UpdatePerfTest extends CpsPerfTestBase {
             def jsonData = '{ "openroadm-devices": ' + generateJsonForOpenRoadmDevices(startId, totalNodes, changeLeaves) + '}'
         when: 'the container node is updated'
             resourceMeter.start()
-            objectUnderTest.updateDataNodeAndDescendants(CPS_PERFORMANCE_TEST_DATASPACE, UPDATE_TEST_ANCHOR, '/', jsonData, now)
+            objectUnderTest.updateDataNodeAndDescendants(CPS_PERFORMANCE_TEST_DATASPACE, UPDATE_TEST_ANCHOR, '/', jsonData, now, ContentType.JSON)
             resourceMeter.stop()
         then: 'there are the expected number of total nodes'
             assert totalNodes == countDataNodes('/openroadm-devices/openroadm-device')