Merge "Temp Table Creation improvements"
authorToine Siebelink <toine.siebelink@est.tech>
Thu, 22 Dec 2022 16:01:28 +0000 (16:01 +0000)
committerGerrit Code Review <gerrit@onap.org>
Thu, 22 Dec 2022 16:01:28 +0000 (16:01 +0000)
35 files changed:
cps-application/pom.xml
cps-application/src/main/resources/application.yml
cps-ncmp-events/src/main/resources/schemas/avc-subscription-event-v1.json [new file with mode: 0644]
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/event/avc/SubscriptionEventConsumer.java [new file with mode: 0644]
cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/event/avc/SubscriptionEventConsumerSpec.groovy [new file with mode: 0644]
cps-ncmp-service/src/test/resources/application.yml
cps-ncmp-service/src/test/resources/avcSubscriptionCreationEvent.json [new file with mode: 0644]
cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathBuilder.java
cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathQuery.java
cps-path-parser/src/main/java/org/onap/cps/cpspath/parser/CpsPathUtil.java
cps-path-parser/src/test/groovy/org/onap/cps/cpspath/parser/CpsPathUtilSpec.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-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy
cps-service/pom.xml
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/main/java/org/onap/cps/utils/ContentType.java [new file with mode: 0644]
cps-service/src/main/java/org/onap/cps/utils/XmlFileUtils.java [new file with mode: 0644]
cps-service/src/main/java/org/onap/cps/utils/YangUtils.java
cps-service/src/test/groovy/org/onap/cps/api/impl/CpsDataServiceImplSpec.groovy
cps-service/src/test/groovy/org/onap/cps/utils/JsonObjectMapperSpec.groovy
cps-service/src/test/groovy/org/onap/cps/utils/XmlFileUtilsSpec.groovy [new file with mode: 0644]
cps-service/src/test/groovy/org/onap/cps/utils/YangUtilsSpec.groovy
cps-service/src/test/resources/bookstore.json
cps-service/src/test/resources/bookstore.xml [new file with mode: 0644]
cps-service/src/test/resources/bookstore_xpath.xml [new file with mode: 0644]
cps-service/src/test/resources/test-tree.xml [new file with mode: 0644]
csit/data/test-tree.json
csit/prepare-csit.sh
csit/pylibs.txt
csit/tests/cps-data/cps-data.robot
docs/release-notes.rst

index 9990cdd..c689c75 100755 (executable)
@@ -4,6 +4,7 @@
   Copyright (c) 2021 Pantheon.tech.
   Modifications Copyright (C) 2021 Bell Canada.
   Modifications Copyright (C) 2021 Nordix Foundation
+  Modifications Copyright (C) 2022 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.
             <groupId>com.tngtech.archunit</groupId>
             <artifactId>archunit-junit5</artifactId>
         </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.dataformat</groupId>
+            <artifactId>jackson-dataformat-xml</artifactId>
+        </dependency>
     </dependencies>
 
     <build>
index e3ffd04..b5b10b0 100644 (file)
@@ -98,6 +98,8 @@ app:
     ncmp:\r
         async-m2m:\r
             topic: ${NCMP_ASYNC_M2M_TOPIC:ncmp-async-m2m}\r
+        avc:\r
+            subscription-topic: ${NCMP_CM_AVC_SUBSCRIPTION:cm-avc-subscription}\r
     lcm:\r
         events:\r
             topic: ${LCM_EVENTS_TOPIC:ncmp-events}\r
diff --git a/cps-ncmp-events/src/main/resources/schemas/avc-subscription-event-v1.json b/cps-ncmp-events/src/main/resources/schemas/avc-subscription-event-v1.json
new file mode 100644 (file)
index 0000000..5ab446c
--- /dev/null
@@ -0,0 +1,101 @@
+{
+  "$schema": "https://json-schema.org/draft/2019-09/schema",
+  "$id": "urn:cps:org.onap.cps.ncmp.events:avc-subscription-event:v1",
+  "$ref": "#/definitions/SubscriptionEvent",
+  "definitions": {
+    "SubscriptionEvent": {
+      "description": "The payload for avc subscription event.",
+      "type": "object",
+      "properties": {
+        "version": {
+          "description": "The event type version",
+          "type": "string"
+        },
+        "eventType": {
+          "description": "The event type",
+          "type": "string",
+          "enum": ["CREATE"]
+        },
+        "event": {
+          "$ref": "#/definitions/event"
+        }
+      },
+      "required": [
+        "version",
+        "eventContent"
+      ],
+      "additionalProperties": false
+    },
+    "event": {
+      "description": "The event content.",
+      "type": "object",
+      "properties": {
+        "subscription": {
+          "description": "The subscription details.",
+          "type": "object",
+          "properties": {
+            "clientID": {
+              "description": "The clientID",
+              "type": "string"
+            },
+            "name": {
+              "description": "The name of the subscription",
+              "type": "string"
+            },
+            "isTagged": {
+              "description": "optional parameter, default is no",
+              "type": "boolean",
+              "default": false
+            }
+          },
+          "required": [
+            "clientID",
+            "name"
+          ]
+        },
+        "dataType": {
+          "description": "The datatype content.",
+          "type": "object",
+          "properties": {
+            "dataspace": {
+              "description": "The dataspace name",
+              "type": "string"
+            },
+            "dataCategory": {
+              "description": "The category type of the data",
+              "type": "string"
+            },
+            "dataProvider": {
+              "description": "The provider name of the data",
+              "type": "string"
+            },
+            "schemaName": {
+              "description": "The name of the schema",
+              "type": "string"
+            },
+            "schemaVersion": {
+              "description": "The version of the schema",
+              "type": "string"
+            }
+          }
+        },
+        "required": [
+          "dataspace",
+          "dataCategory",
+          "dataProvider",
+          "schemaName",
+          "schemaVersion"
+        ],
+        "predicates": {
+          "description": "Additional values to be added into the subscription",
+          "existingJavaType" : "java.util.Map<String,Object>",
+          "type" : "object"
+          }
+        }
+      },
+      "required": [
+        "subscription",
+        "dataType"
+      ]
+    }
+}
\ No newline at end of file
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/event/avc/SubscriptionEventConsumer.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/impl/event/avc/SubscriptionEventConsumer.java
new file mode 100644 (file)
index 0000000..1f03246
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ * ============LICENSE_START=======================================================
+ *  Copyright (C) 2022 Nordix Foundation
+ *  ================================================================================
+ *  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.ncmp.api.impl.event.avc;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.onap.cps.ncmp.event.model.SubscriptionEvent;
+import org.springframework.kafka.annotation.KafkaListener;
+import org.springframework.stereotype.Component;
+
+
+@Component
+@Slf4j
+@RequiredArgsConstructor
+public class SubscriptionEventConsumer {
+
+    /**
+     * Consume the specified event.
+     *
+     * @param subscriptionEvent the event to be consumed
+     */
+    @KafkaListener(topics = "${app.ncmp.avc.subscription-topic}")
+    public void consumeSubscriptionEvent(final SubscriptionEvent subscriptionEvent) {
+        if ("CM".equals(subscriptionEvent.getEvent().getDataType().getDataCategory())) {
+            log.debug("Consuming event {} ...", subscriptionEvent.toString());
+            if ("CREATE".equals(subscriptionEvent.getEventType().value())) {
+                log.info("Subscription for ClientID {} with name{} ...",
+                        subscriptionEvent.getEvent().getSubscription().getClientID(),
+                        subscriptionEvent.getEvent().getSubscription().getName());
+            }
+        } else {
+            log.trace("Non-CM subscription event ignored");
+        }
+    }
+}
diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/event/avc/SubscriptionEventConsumerSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/api/impl/event/avc/SubscriptionEventConsumerSpec.groovy
new file mode 100644 (file)
index 0000000..20d60e3
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (c) 2022 Nordix Foundation.
+ *  ================================================================================
+ *  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.ncmp.api.impl.event.avc
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import org.onap.cps.ncmp.api.kafka.MessagingBaseSpec
+import org.onap.cps.ncmp.event.model.SubscriptionEvent
+import org.onap.cps.ncmp.utils.TestUtils
+import org.onap.cps.utils.JsonObjectMapper
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.context.SpringBootTest
+
+@SpringBootTest(classes = [SubscriptionEventConsumer, ObjectMapper, JsonObjectMapper])
+class SubscriptionEventConsumerSpec extends MessagingBaseSpec {
+
+    def objectUnderTest = new SubscriptionEventConsumer()
+
+    @Autowired
+    JsonObjectMapper jsonObjectMapper
+
+    def 'Consume valid message'() {
+        given: 'an event'
+            def jsonData = TestUtils.getResourceFileContent('avcSubscriptionCreationEvent.json')
+            def testEventSent = jsonObjectMapper.convertJsonString(jsonData, SubscriptionEvent.class)
+        and: 'dataCategory is set'
+            testEventSent.getEvent().getDataType().setDataCategory(dataCategory)
+        when: 'the valid event is consumed'
+            objectUnderTest.consumeSubscriptionEvent(testEventSent)
+        then: 'no exception is thrown'
+            noExceptionThrown()
+        where: 'data category is changed'
+            dataCategory << [ 'CM' , 'FM' ]
+    }
+}
index 8d8bfaf..4009e56 100644 (file)
 #  SPDX-License-Identifier: Apache-2.0
 #  ============LICENSE_END=========================================================
 
+app:
+    ncmp:
+        avc:
+            subscription-topic: test-avc-subscription
+
 ncmp:
     dmi:
         auth:
diff --git a/cps-ncmp-service/src/test/resources/avcSubscriptionCreationEvent.json b/cps-ncmp-service/src/test/resources/avcSubscriptionCreationEvent.json
new file mode 100644 (file)
index 0000000..1d84c3a
--- /dev/null
@@ -0,0 +1,23 @@
+{
+  "version": "1.0",
+  "eventType": "CREATE",
+  "event": {
+    "subscription": {
+      "clientID": "SCO-9989752",
+      "name": "cm-subscription-001"
+    },
+    "dataType": {
+      "dataspace": "ALL",
+      "dataCategory": "CM",
+      "dataProvider": "CM-SERVICE",
+      "schemaName": "org.onap.ncmp:cm-network-avc-event.rfc8641",
+      "schemaVersion": "1.0"
+    },
+    "predicates": {
+      "datastore": "passthrough-operational",
+      "datastore-xpath-filter": "//_3gpp-nr-nrm-gnbdufunction:GNBDUFunction/ ",
+      "_3gpp-nr-nrm-nrcelldu": "NRCellDU"
+
+    }
+  }
+}
\ No newline at end of file
index 7183120..3a9d70e 100644 (file)
@@ -22,7 +22,9 @@ package org.onap.cps.cpspath.parser;
 
 import static org.onap.cps.cpspath.parser.CpsPathPrefixType.DESCENDANT;
 
+import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import org.onap.cps.cpspath.parser.antlr4.CpsPathBaseListener;
 import org.onap.cps.cpspath.parser.antlr4.CpsPathParser;
@@ -50,6 +52,8 @@ public class CpsPathBuilder extends CpsPathBaseListener {
 
     boolean processingAncestorAxis = false;
 
+    private List<String> containerNames = new ArrayList<>();
+
     @Override
     public void exitInvalidPostFix(final CpsPathParser.InvalidPostFixContext ctx) {
         throw new PathParsingException(ctx.getText());
@@ -146,6 +150,7 @@ public class CpsPathBuilder extends CpsPathBaseListener {
 
     CpsPathQuery build() {
         cpsPathQuery.setNormalizedXpath(normalizedXpathBuilder.toString());
+        cpsPathQuery.setContainerNames(containerNames);
         return cpsPathQuery;
     }
 
@@ -155,10 +160,12 @@ public class CpsPathBuilder extends CpsPathBaseListener {
 
     @Override
     public void exitContainerName(final CpsPathParser.ContainerNameContext ctx) {
+        final String containerName = ctx.getText();
         normalizedXpathBuilder.append("/")
-                .append(ctx.getText());
+                .append(containerName);
+        containerNames.add(containerName);
         if (processingAncestorAxis) {
-            normalizedAncestorPathBuilder.append("/").append(ctx.getText());
+            normalizedAncestorPathBuilder.append("/").append(containerName);
         }
     }
 
index a9bd5d8..c9df8df 100644 (file)
@@ -22,6 +22,7 @@ package org.onap.cps.cpspath.parser;
 
 import static org.onap.cps.cpspath.parser.CpsPathPrefixType.ABSOLUTE;
 
+import java.util.List;
 import java.util.Map;
 import lombok.AccessLevel;
 import lombok.Getter;
@@ -34,6 +35,7 @@ public class CpsPathQuery {
     private String xpathPrefix;
     private String normalizedParentPath;
     private String normalizedXpath;
+    private List<String> containerNames;
     private CpsPathPrefixType cpsPathPrefixType = ABSOLUTE;
     private String descendantName;
     private Map<String, Object> leavesData;
index 283463b..60f0e2e 100644 (file)
@@ -22,6 +22,7 @@ package org.onap.cps.cpspath.parser;
 
 import static org.onap.cps.cpspath.parser.CpsPathPrefixType.ABSOLUTE;
 
+import java.util.List;
 import lombok.AccessLevel;
 import lombok.Getter;
 import lombok.NoArgsConstructor;
@@ -60,6 +61,11 @@ public class CpsPathUtil {
         return getCpsPathBuilder(xpathSource).build().getNormalizedParentPath();
     }
 
+    public static String[] getXpathNodeIdSequence(final String xpathSource) {
+        final List<String> containerNames = getCpsPathBuilder(xpathSource).build().getContainerNames();
+        return containerNames.toArray(new String[containerNames.size()]);
+    }
+
 
     /**
      * Returns boolean indicating xpath is an absolute path to a list element.
index f1a878d..36e8912 100644 (file)
@@ -29,7 +29,7 @@ class CpsPathUtilSpec extends Specification {
         when: 'xpath with #scenario is parsed'
             def result = CpsPathUtil.getNormalizedXpath(xpath)
         then: 'normalized path uses single quotes for leave values'
-            result == "/parent/child[@common-leaf-name='123']"
+            assert result == "/parent/child[@common-leaf-name='123']"
         where: 'the following xpaths are used'
             scenario        | xpath
             'no quotes'     | '/parent/child[@common-leaf-name=123]'
@@ -41,7 +41,7 @@ class CpsPathUtilSpec extends Specification {
         when: 'a given xpath with #scenario is parsed'
             def result = CpsPathUtil.getNormalizedParentXpath(xpath)
         then: 'the result is the expected parent path'
-            result == expectedParentPath
+            assert result == expectedParentPath
         where: 'the following xpaths are used'
             scenario                         | xpath                                 || expectedParentPath
             'no child'                       | '/parent'                             || ''
@@ -54,6 +54,22 @@ class CpsPathUtilSpec extends Specification {
             'parent is list element using "' | '/parent/child[@id="x"]/grandChild'   || "/parent/child[@id='x']"
     }
 
+    def 'Get node ID sequence for given xpath'() {
+        when: 'a given xpath with #scenario is parsed'
+            def result = CpsPathUtil.getXpathNodeIdSequence(xpath)
+        then: 'the result is the expected node ID sequence'
+            assert result == expectedNodeIdSequence
+        where: 'the following xpaths are used'
+            scenario                         | xpath                                 || expectedNodeIdSequence
+            'no child'                       | '/parent'                             || ["parent"]
+            'child and parent'               | '/parent/child'                       || ["parent","child"]
+            'grand child'                    | '/parent/child/grandChild'            || ["parent","child","grandChild"]
+            'parent & top is list element'   | '/parent[@id=1]/child'                || ["parent","child"]
+            'parent is list element'         | '/parent/child[@id=1]/grandChild'     || ["parent","child","grandChild"]
+            'parent is list element with /'  | "/parent/child[@id='a/b']/grandChild" || ["parent","child","grandChild"]
+            'parent is list element with ['  | "/parent/child[@id='a[b']/grandChild" || ["parent","child","grandChild"]
+    }
+
     def 'Recognizing (absolute) xpaths to List elements'() {
         expect: 'check for list returns the correct values'
             assert CpsPathUtil.isPathToListElement(xpath) == expectList
index 4f138fc..e700da6 100644 (file)
@@ -2,6 +2,7 @@
 # Copyright (c) 2021-2022 Bell Canada.
 # Modifications Copyright (C) 2021-2022 Nordix Foundation
 # Modifications Copyright (C) 2022 TechMahindra Ltd.
+# Modifications Copyright (C) 2022 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.
@@ -106,6 +107,17 @@ components:
               name: SciFi
             - code: 02
               name: kids
+    dataSampleXml:
+        value:
+          <stores xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
+            <bookstore xmlns="org:onap:ccsdk:sample">
+              <bookstore-name>Chapters</bookstore-name>
+              <categories>
+                <code>1</code>
+                <name>SciFi</name>
+              </categories>
+            </bookstore>
+          </stores>
 
   parameters:
     dataspaceNameInQuery:
@@ -220,6 +232,14 @@ components:
         type: string
         enum: [v1, v2]
         default: v2
+    contentTypeHeader:
+      name: Content-Type
+      in: header
+      description: Content type header
+      schema:
+        type: string
+        example: 'application/json'
+      required: true
 
   responses:
     NotFound:
index 9d940c3..0dc3887 100644 (file)
@@ -2,6 +2,7 @@
 # Copyright (c) 2021-2022 Bell Canada.
 # Modifications Copyright (C) 2021-2022 Nordix Foundation
 # Modifications Copyright (C) 2022 TechMahindra Ltd.
+# Modifications Copyright (C) 2022 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.
@@ -130,15 +131,25 @@ 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'
     requestBody:
       required: true
       content:
         application/json:
           schema:
-            type: object
+            type: string
           examples:
             dataSample:
               $ref: 'components.yml#/components/examples/dataSample'
+        application/xml:
+          schema:
+            type: object   # Workaround to show example
+            xml:
+              name: stores
+          examples:
+            dataSample:
+              $ref: 'components.yml#/components/examples/dataSampleXml'
+
     responses:
       '201':
         $ref: 'components.yml#/components/responses/Created'
index c7d44b6..30bed12 100755 (executable)
@@ -4,6 +4,7 @@
  *  Modifications Copyright (C) 2021 Pantheon.tech
  *  Modifications Copyright (C) 2021-2022 Nordix Foundation
  *  Modifications Copyright (C) 2022 TechMahindra Ltd.
+ *  Modifications Copyright (C) 2022 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.
@@ -32,11 +33,14 @@ import org.onap.cps.api.CpsDataService;
 import org.onap.cps.rest.api.CpsDataApi;
 import org.onap.cps.spi.FetchDescendantsOption;
 import org.onap.cps.spi.model.DataNode;
+import org.onap.cps.utils.ContentType;
 import org.onap.cps.utils.DataMapUtils;
 import org.onap.cps.utils.JsonObjectMapper;
 import org.onap.cps.utils.PrefixResolver;
 import org.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;
 
@@ -54,16 +58,19 @@ public class DataRestController implements CpsDataApi {
     private final PrefixResolver prefixResolver;
 
     @Override
-    public ResponseEntity<String> createNode(final String apiVersion,
-        final String dataspaceName, final String anchorName,
-        final Object jsonData, final String parentNodeXpath, final String observedTimestamp) {
-        final String jsonDataAsString = jsonObjectMapper.asJsonString(jsonData);
+    public ResponseEntity<String> createNode(@RequestHeader(value = "Content-Type") final String contentTypeHeader,
+                                             final String apiVersion,
+                                             final String dataspaceName, final String anchorName,
+                                             final String nodeData, final String parentNodeXpath,
+                                             final String observedTimestamp) {
+        final ContentType contentType = contentTypeHeader.contains(MediaType.APPLICATION_XML_VALUE) ? ContentType.XML
+                : ContentType.JSON;
         if (isRootXpath(parentNodeXpath)) {
-            cpsDataService.saveData(dataspaceName, anchorName, jsonDataAsString,
-                    toOffsetDateTime(observedTimestamp));
+            cpsDataService.saveData(dataspaceName, anchorName, nodeData,
+                    toOffsetDateTime(observedTimestamp), contentType);
         } else {
             cpsDataService.saveData(dataspaceName, anchorName, parentNodeXpath,
-                    jsonDataAsString, toOffsetDateTime(observedTimestamp));
+                    nodeData, toOffsetDateTime(observedTimestamp), contentType);
         }
         return new ResponseEntity<>(HttpStatus.CREATED);
     }
index 53da3e6..94f62f8 100755 (executable)
@@ -3,6 +3,7 @@
  *  Copyright (C) 2021-2022 Nordix Foundation
  *  Modifications Copyright (C) 2021 Pantheon.tech
  *  Modifications Copyright (C) 2021-2022 Bell Canada.
+ *  Modifications Copyright (C) 2022 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.
@@ -26,6 +27,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
 import org.onap.cps.api.CpsDataService
 import org.onap.cps.spi.model.DataNode
 import org.onap.cps.spi.model.DataNodeBuilder
+import org.onap.cps.utils.ContentType
 import org.onap.cps.utils.DateTimeUtility
 import org.onap.cps.utils.JsonObjectMapper
 import org.onap.cps.utils.PrefixResolver
@@ -69,9 +71,19 @@ class DataRestControllerSpec extends Specification {
     def dataspaceName = 'my_dataspace'
     def anchorName = 'my_anchor'
     def noTimestamp = null
-    def requestBody = '{"some-key" : "some-value","categories":[{"books":[{"authors":["Iain M. Banks"]}]}]}'
+
+    @Shared
+    def requestBodyJson = '{"some-key":"some-value","categories":[{"books":[{"authors":["Iain M. Banks"]}]}]}'
+
+    @Shared
     def expectedJsonData = '{"some-key":"some-value","categories":[{"books":[{"authors":["Iain M. Banks"]}]}]}'
 
+    @Shared
+    def requestBodyXml = '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n<bookstore xmlns="org:onap:ccsdk:sample">\n</bookstore>'
+
+    @Shared
+    def expectedXmlData = '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n<bookstore xmlns="org:onap:ccsdk:sample">\n</bookstore>'
+
     @Shared
     static DataNode dataNodeWithLeavesNoChildren = new DataNodeBuilder().withXpath('/xpath')
         .withLeaves([leaf: 'value', leafList: ['leaveListElement1', 'leaveListElement2']]).build()
@@ -91,18 +103,20 @@ class DataRestControllerSpec extends Specification {
             def response =
                 mvc.perform(
                     post(endpoint)
-                        .contentType(MediaType.APPLICATION_JSON)
+                        .contentType(contentType)
                         .param('xpath', parentNodeXpath)
                         .content(requestBody)
                 ).andReturn().response
         then: 'a created response is returned'
             response.status == HttpStatus.CREATED.value()
         then: 'the java API was called with the correct parameters'
-            1 * mockCpsDataService.saveData(dataspaceName, anchorName, expectedJsonData, noTimestamp)
+            1 * mockCpsDataService.saveData(dataspaceName, anchorName, expectedData, noTimestamp, expectedContentType)
         where: 'following xpath parameters are are used'
-            scenario                     | parentNodeXpath
-            'no xpath parameter'         | ''
-            'xpath parameter point root' | '/'
+            scenario                                   | parentNodeXpath | contentType                | expectedContentType | requestBody     | expectedData
+            'JSON content: no xpath parameter'         | ''              | MediaType.APPLICATION_JSON | ContentType.JSON    | requestBodyJson | expectedJsonData
+            'JSON content: xpath parameter point root' | '/'             | MediaType.APPLICATION_JSON | ContentType.JSON    | requestBodyJson | expectedJsonData
+            'XML content: no xpath parameter'          | ''              | MediaType.APPLICATION_XML  | ContentType.XML     | requestBodyXml  | expectedXmlData
+            'XML content: xpath parameter point root'  | '/'             | MediaType.APPLICATION_XML  | ContentType.XML     | requestBodyXml  | expectedXmlData
     }
 
     def 'Create a node with observed-timestamp'() {
@@ -112,30 +126,31 @@ class DataRestControllerSpec extends Specification {
             def response =
                 mvc.perform(
                     post(endpoint)
-                        .contentType(MediaType.APPLICATION_JSON)
+                        .contentType(contentType)
                         .param('xpath', '')
                         .param('observed-timestamp', observedTimestamp)
-                        .content(requestBody)
+                        .content(content)
                 ).andReturn().response
         then: 'a created response is returned'
             response.status == expectedHttpStatus.value()
         then: 'the java API was called with the correct parameters'
-            expectedApiCount * mockCpsDataService.saveData(dataspaceName, anchorName, expectedJsonData,
-                { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) })
+            expectedApiCount * mockCpsDataService.saveData(dataspaceName, anchorName, expectedData,
+                { it == DateTimeUtility.toOffsetDateTime(observedTimestamp) }, expectedContentType)
         where:
-            scenario                          | observedTimestamp              || expectedApiCount | expectedHttpStatus
-            'with observed-timestamp'         | '2021-03-03T23:59:59.999-0400' || 1                | HttpStatus.CREATED
-            'with invalid observed-timestamp' | 'invalid'                      || 0                | HttpStatus.BAD_REQUEST
+            scenario                          | observedTimestamp              | contentType                | content         || expectedApiCount | expectedHttpStatus     | expectedData     | expectedContentType
+            'with observed-timestamp JSON'    | '2021-03-03T23:59:59.999-0400' | MediaType.APPLICATION_JSON | requestBodyJson || 1                | HttpStatus.CREATED     | expectedJsonData | ContentType.JSON
+            'with observed-timestamp XML'     | '2021-03-03T23:59:59.999-0400' | MediaType.APPLICATION_XML  | requestBodyXml  || 1                | HttpStatus.CREATED     | expectedXmlData  | ContentType.XML
+            'with invalid observed-timestamp' | 'invalid'                      | MediaType.APPLICATION_JSON | requestBodyJson || 0                | HttpStatus.BAD_REQUEST | expectedJsonData | ContentType.JSON
     }
 
-    def 'Create a child node'() {
+    def 'Create a child node #scenario'() {
         given: 'endpoint to create a node'
             def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
         and: 'parent node xpath'
             def parentNodeXpath = 'some xpath'
         when: 'post is invoked with datanode endpoint and json'
             def postRequestBuilder = post(endpoint)
-                .contentType(MediaType.APPLICATION_JSON)
+                .contentType(contentType)
                 .param('xpath', parentNodeXpath)
                 .content(requestBody)
             if (observedTimestamp != null)
@@ -145,12 +160,14 @@ class DataRestControllerSpec extends Specification {
         then: 'a created response is returned'
             response.status == HttpStatus.CREATED.value()
         then: 'the java API was called with the correct parameters'
-            1 * mockCpsDataService.saveData(dataspaceName, anchorName, parentNodeXpath, expectedJsonData,
-                DateTimeUtility.toOffsetDateTime(observedTimestamp))
+            1 * mockCpsDataService.saveData(dataspaceName, anchorName, parentNodeXpath, expectedData,
+                DateTimeUtility.toOffsetDateTime(observedTimestamp), expectedContentType)
         where:
-            scenario                     | observedTimestamp
-            'with observed-timestamp'    | '2021-03-03T23:59:59.999-0400'
-            'without observed-timestamp' | null
+            scenario                          | observedTimestamp              | contentType                | requestBody     | expectedData     | expectedContentType
+            'with observed-timestamp JSON'    | '2021-03-03T23:59:59.999-0400' | MediaType.APPLICATION_JSON | requestBodyJson | expectedJsonData | ContentType.JSON
+            'with observed-timestamp XML'     | '2021-03-03T23:59:59.999-0400' | MediaType.APPLICATION_XML  | requestBodyXml  | expectedXmlData  | ContentType.XML
+            'without observed-timestamp JSON' | null                           | MediaType.APPLICATION_JSON | requestBodyJson | expectedJsonData | ContentType.JSON
+            'without observed-timestamp XML'  | null                           | MediaType.APPLICATION_XML  | requestBodyXml  | expectedXmlData  | ContentType.XML
     }
 
     def 'Save list elements #scenario.'() {
@@ -160,7 +177,7 @@ class DataRestControllerSpec extends Specification {
             def postRequestBuilder = post("$dataNodeBaseEndpoint/anchors/$anchorName/list-nodes")
                 .contentType(MediaType.APPLICATION_JSON)
                 .param('xpath', parentNodeXpath)
-                .content(requestBody)
+                .content(requestBodyJson)
             if (observedTimestamp != null)
                 postRequestBuilder.param('observed-timestamp', observedTimestamp)
             def response = mvc.perform(postRequestBuilder).andReturn().response
@@ -228,7 +245,7 @@ class DataRestControllerSpec extends Specification {
                 mvc.perform(
                     patch(endpoint)
                         .contentType(MediaType.APPLICATION_JSON)
-                        .content(requestBody)
+                        .content(requestBodyJson)
                         .param('xpath', inputXpath)
                 ).andReturn().response
         then: 'the service method is invoked with expected parameters'
@@ -250,7 +267,7 @@ class DataRestControllerSpec extends Specification {
                 mvc.perform(
                     patch(endpoint)
                         .contentType(MediaType.APPLICATION_JSON)
-                        .content(requestBody)
+                        .content(requestBodyJson)
                         .param('xpath', '/')
                         .param('observed-timestamp', observedTimestamp)
                 ).andReturn().response
@@ -273,7 +290,7 @@ class DataRestControllerSpec extends Specification {
                 mvc.perform(
                     put(endpoint)
                         .contentType(MediaType.APPLICATION_JSON)
-                        .content(requestBody)
+                        .content(requestBodyJson)
                         .param('xpath', inputXpath))
                     .andReturn().response
         then: 'the service method is invoked with expected parameters'
@@ -295,7 +312,7 @@ class DataRestControllerSpec extends Specification {
                 mvc.perform(
                     put(endpoint)
                         .contentType(MediaType.APPLICATION_JSON)
-                        .content(requestBody)
+                        .content(requestBodyJson)
                         .param('xpath', '')
                         .param('observed-timestamp', observedTimestamp))
                     .andReturn().response
@@ -315,7 +332,7 @@ class DataRestControllerSpec extends Specification {
             def putRequestBuilder = put("$dataNodeBaseEndpoint/anchors/$anchorName/list-nodes")
                 .contentType(MediaType.APPLICATION_JSON)
                 .param('xpath', 'parent xpath')
-                .content(requestBody)
+                .content(requestBodyJson)
             if (observedTimestamp != null)
                 putRequestBuilder.param('observed-timestamp', observedTimestamp)
             def response = mvc.perform(putRequestBuilder).andReturn().response
index ece3507..0821b6b 100644 (file)
@@ -4,6 +4,7 @@
  *  Modifications Copyright (C) 2021-2022 Nordix Foundation
  *  Modifications Copyright (C) 2021 Bell Canada.
  *  Modifications Copyright (C) 2022 TechMahindra Ltd.
+ *  Modifications Copyright (C) 2022 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.
@@ -167,13 +168,13 @@ class CpsRestExceptionHandlerSpec extends Specification {
 
     def 'Post request with #exceptionThrown.class.simpleName returns HTTP Status Bad Request.'() {
         given: '#exception is thrown the service indicating data is not found'
-            mockCpsDataService.saveData(_, _, _, _, _) >> { throw exceptionThrown }
+            mockCpsDataService.saveData(*_) >> { throw exceptionThrown }
         when: 'data update request is performed'
             def response = mvc.perform(
                 post("$basePath/v1/dataspaces/dataspace-name/anchors/anchor-name/nodes")
                     .contentType(MediaType.APPLICATION_JSON)
                     .param('xpath', 'parent node xpath')
-                    .content(groovy.json.JsonOutput.toJson('{"some-key" : "some-value"}'))
+                    .content('{"some-key" : "some-value"}')
             ).andReturn().response
         then: 'response code indicates bad input parameters'
             response.status == BAD_REQUEST.value()
@@ -181,18 +182,6 @@ class CpsRestExceptionHandlerSpec extends Specification {
             exceptionThrown << [new DataNodeNotFoundException('', ''), new NotFoundInDataspaceException('', '')]
     }
 
-   def 'Post request with invalid JSON payload returns HTTP Status Bad Request.'() {
-        when: 'data post request is performed'
-            def response = mvc.perform(
-                post("$basePath/v1/dataspaces/dataspace-name/anchors/anchor-name/nodes")
-                    .contentType(MediaType.APPLICATION_JSON)
-                    .param('xpath', 'parent node xpath')
-                    .content('{')
-            ).andReturn().response
-        then: 'response code indicates bad input parameters'
-            response.status == BAD_REQUEST.value()
-    }
-
     /*
      * NB. The test uses 'get anchors' endpoint and associated service method invocation
      * to test the exception handling. The endpoint chosen is not a subject of test.
index 77f262c..70fa447 100644 (file)
@@ -4,6 +4,7 @@
   Copyright (C) 2021-2022 Nordix Foundation
   Modifications Copyright (C) 2021 Bell Canada.
   Modifications Copyright (C) 2021 Pantheon.tech
+  Modifications Copyright (C) 2022 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.
       <groupId>org.opendaylight.yangtools</groupId>
       <artifactId>yang-data-codec-gson</artifactId>
     </dependency>
+    <dependency>
+      <groupId>org.opendaylight.yangtools</groupId>
+      <artifactId>yang-data-codec-xml</artifactId>
+    </dependency>
     <dependency>
       <groupId>org.projectlombok</groupId>
       <artifactId>lombok</artifactId>
       <groupId>org.codehaus.janino</groupId>
       <artifactId>janino</artifactId>
     </dependency>
+    <dependency>
+      <groupId>org.onap.cps</groupId>
+      <artifactId>cps-path-parser</artifactId>
+    </dependency>
     <dependency>
       <!-- Hazelcast provide Distributed Caches -->
       <groupId>com.hazelcast</groupId>
index b2e8c5b..012d7f8 100644 (file)
@@ -3,6 +3,7 @@
  *  Copyright (C) 2020-2022 Nordix Foundation
  *  Modifications Copyright (C) 2021 Pantheon.tech
  *  Modifications Copyright (C) 2021-2022 Bell Canada
+ *  Modifications Copyright (C) 2022 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,6 +28,7 @@ import java.util.Collection;
 import java.util.Map;
 import org.onap.cps.spi.FetchDescendantsOption;
 import org.onap.cps.spi.model.DataNode;
+import org.onap.cps.utils.ContentType;
 
 /*
  * Datastore interface for handling CPS data.
@@ -38,10 +40,22 @@ public interface CpsDataService {
      *
      * @param dataspaceName dataspace name
      * @param anchorName    anchor name
-     * @param jsonData      json data
+     * @param nodeData      node data
      * @param observedTimestamp observedTimestamp
      */
-    void saveData(String dataspaceName, String anchorName, String jsonData, OffsetDateTime observedTimestamp);
+    void saveData(String dataspaceName, String anchorName, String nodeData, OffsetDateTime observedTimestamp);
+
+    /**
+     * Persists data for the given anchor and dataspace.
+     *
+     * @param dataspaceName dataspace name
+     * @param anchorName    anchor name
+     * @param nodeData      node data
+     * @param observedTimestamp observedTimestamp
+     * @param contentType       node data content type
+     */
+    void saveData(String dataspaceName, String anchorName, String nodeData, OffsetDateTime observedTimestamp,
+                  ContentType contentType);
 
     /**
      * Persists child data fragment under existing data node for the given anchor and dataspace.
@@ -49,11 +63,25 @@ public interface CpsDataService {
      * @param dataspaceName   dataspace name
      * @param anchorName      anchor name
      * @param parentNodeXpath parent node xpath
-     * @param jsonData        json data
+     * @param nodeData        node data
      * @param observedTimestamp observedTimestamp
      */
-    void saveData(String dataspaceName, String anchorName, String parentNodeXpath, String jsonData,
-        OffsetDateTime observedTimestamp);
+    void saveData(String dataspaceName, String anchorName, String parentNodeXpath, String nodeData,
+                  OffsetDateTime observedTimestamp);
+
+    /**
+     * Persists child data fragment under existing data node for the given anchor, dataspace and content type.
+     *
+     * @param dataspaceName     dataspace name
+     * @param anchorName        anchor name
+     * @param parentNodeXpath   parent node xpath
+     * @param nodeData          node data
+     * @param observedTimestamp observedTimestamp
+     * @param contentType       node data content type
+     *
+     */
+    void saveData(String dataspaceName, String anchorName, String parentNodeXpath, String nodeData,
+                  OffsetDateTime observedTimestamp, ContentType contentType);
 
     /**
      * Persists child data fragment representing one or more list elements under existing data node for the
index 732b494..c776e5b 100755 (executable)
@@ -4,6 +4,7 @@
  *  Modifications Copyright (C) 2020-2022 Bell Canada.
  *  Modifications Copyright (C) 2021 Pantheon.tech
  *  Modifications Copyright (C) 2022 TechMahindra Ltd.
+ *  Modifications Copyright (C) 2022 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 +47,7 @@ import org.onap.cps.spi.model.Anchor;
 import org.onap.cps.spi.model.DataNode;
 import org.onap.cps.spi.model.DataNodeBuilder;
 import org.onap.cps.spi.utils.CpsValidator;
+import org.onap.cps.utils.ContentType;
 import org.onap.cps.utils.YangUtils;
 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
@@ -66,21 +68,34 @@ public class CpsDataServiceImpl implements CpsDataService {
     private final CpsValidator cpsValidator;
 
     @Override
-    public void saveData(final String dataspaceName, final String anchorName, final String jsonData,
+    public void saveData(final String dataspaceName, final String anchorName, final String nodeData,
         final OffsetDateTime observedTimestamp) {
+        saveData(dataspaceName, anchorName, nodeData, observedTimestamp, ContentType.JSON);
+    }
+
+    @Override
+    public void saveData(final String dataspaceName, final String anchorName, final String nodeData,
+                         final OffsetDateTime observedTimestamp, final ContentType contentType) {
         cpsValidator.validateNameCharacters(dataspaceName, anchorName);
         final Collection<DataNode> dataNodes =
-                buildDataNodes(dataspaceName, anchorName, ROOT_NODE_XPATH, jsonData);
+                buildDataNodes(dataspaceName, anchorName, ROOT_NODE_XPATH, nodeData, contentType);
         cpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName, dataNodes);
         processDataUpdatedEventAsync(dataspaceName, anchorName, ROOT_NODE_XPATH, CREATE, observedTimestamp);
     }
 
     @Override
     public void saveData(final String dataspaceName, final String anchorName, final String parentNodeXpath,
-        final String jsonData, final OffsetDateTime observedTimestamp) {
+                         final String nodeData, final OffsetDateTime observedTimestamp) {
+        saveData(dataspaceName, anchorName, parentNodeXpath, nodeData, observedTimestamp, ContentType.JSON);
+    }
+
+    @Override
+    public void saveData(final String dataspaceName, final String anchorName, final String parentNodeXpath,
+                         final String nodeData, final OffsetDateTime observedTimestamp,
+                         final ContentType contentType) {
         cpsValidator.validateNameCharacters(dataspaceName, anchorName);
         final Collection<DataNode> dataNodes =
-                buildDataNodes(dataspaceName, anchorName, parentNodeXpath, jsonData);
+                buildDataNodes(dataspaceName, anchorName, parentNodeXpath, nodeData, contentType);
         cpsDataPersistenceService.addChildDataNodes(dataspaceName, anchorName, parentNodeXpath, dataNodes);
         processDataUpdatedEventAsync(dataspaceName, anchorName, parentNodeXpath, CREATE, observedTimestamp);
     }
@@ -90,7 +105,7 @@ public class CpsDataServiceImpl implements CpsDataService {
         final String parentNodeXpath, final String jsonData, final OffsetDateTime observedTimestamp) {
         cpsValidator.validateNameCharacters(dataspaceName, anchorName);
         final Collection<DataNode> listElementDataNodeCollection =
-            buildDataNodes(dataspaceName, anchorName, parentNodeXpath, jsonData);
+            buildDataNodes(dataspaceName, anchorName, parentNodeXpath, jsonData, ContentType.JSON);
         cpsDataPersistenceService.addListElements(dataspaceName, anchorName, parentNodeXpath,
             listElementDataNodeCollection);
         processDataUpdatedEventAsync(dataspaceName, anchorName, parentNodeXpath, UPDATE, observedTimestamp);
@@ -101,7 +116,7 @@ public class CpsDataServiceImpl implements CpsDataService {
             final Collection<String> jsonDataList, final OffsetDateTime observedTimestamp) {
         cpsValidator.validateNameCharacters(dataspaceName, anchorName);
         final Collection<Collection<DataNode>> listElementDataNodeCollections =
-                buildDataNodes(dataspaceName, anchorName, parentNodeXpath, jsonDataList);
+                buildDataNodes(dataspaceName, anchorName, parentNodeXpath, jsonDataList, ContentType.JSON);
         cpsDataPersistenceService.addMultipleLists(dataspaceName, anchorName, parentNodeXpath,
                 listElementDataNodeCollections);
         processDataUpdatedEventAsync(dataspaceName, anchorName, parentNodeXpath, UPDATE, observedTimestamp);
@@ -118,7 +133,7 @@ public class CpsDataServiceImpl implements CpsDataService {
     public void updateNodeLeaves(final String dataspaceName, final String anchorName, final String parentNodeXpath,
         final String jsonData, final OffsetDateTime observedTimestamp) {
         cpsValidator.validateNameCharacters(dataspaceName, anchorName);
-        final DataNode dataNode = buildDataNode(dataspaceName, anchorName, parentNodeXpath, jsonData);
+        final DataNode dataNode = buildDataNode(dataspaceName, anchorName, parentNodeXpath, jsonData, ContentType.JSON);
         cpsDataPersistenceService
             .updateDataLeaves(dataspaceName, anchorName, dataNode.getXpath(), dataNode.getLeaves());
         processDataUpdatedEventAsync(dataspaceName, anchorName, parentNodeXpath, UPDATE, observedTimestamp);
@@ -132,7 +147,7 @@ public class CpsDataServiceImpl implements CpsDataService {
         cpsValidator.validateNameCharacters(dataspaceName, anchorName);
         final Collection<DataNode> dataNodeUpdates =
             buildDataNodes(dataspaceName, anchorName,
-                parentNodeXpath, dataNodeUpdatesAsJson);
+                parentNodeXpath, dataNodeUpdatesAsJson, ContentType.JSON);
         for (final DataNode dataNodeUpdate : dataNodeUpdates) {
             processDataNodeUpdate(dataspaceName, anchorName, dataNodeUpdate);
         }
@@ -166,7 +181,7 @@ public class CpsDataServiceImpl implements CpsDataService {
                                              final OffsetDateTime observedTimestamp) {
         cpsValidator.validateNameCharacters(dataspaceName, anchorName);
         final Collection<DataNode> dataNodes =
-                buildDataNodes(dataspaceName, anchorName, parentNodeXpath, jsonData);
+                buildDataNodes(dataspaceName, anchorName, parentNodeXpath, jsonData, ContentType.JSON);
         final ArrayList<DataNode> nodes = new ArrayList<>(dataNodes);
         cpsDataPersistenceService.updateDataNodesAndDescendants(dataspaceName, anchorName, nodes);
         processDataUpdatedEventAsync(dataspaceName, anchorName, parentNodeXpath, UPDATE, observedTimestamp);
@@ -189,7 +204,7 @@ public class CpsDataServiceImpl implements CpsDataService {
             final String jsonData, final OffsetDateTime observedTimestamp) {
         cpsValidator.validateNameCharacters(dataspaceName, anchorName);
         final Collection<DataNode> newListElements =
-                buildDataNodes(dataspaceName, anchorName, parentNodeXpath, jsonData);
+                buildDataNodes(dataspaceName, anchorName, parentNodeXpath, jsonData, ContentType.JSON);
         replaceListContent(dataspaceName, anchorName, parentNodeXpath, newListElements, observedTimestamp);
     }
 
@@ -226,18 +241,20 @@ public class CpsDataServiceImpl implements CpsDataService {
     }
 
     private DataNode buildDataNode(final String dataspaceName, final String anchorName,
-                                   final String parentNodeXpath, final String jsonData) {
+                                   final String parentNodeXpath, final String nodeData,
+                                   final ContentType contentType) {
 
         final Anchor anchor = cpsAdminService.getAnchor(dataspaceName, anchorName);
         final SchemaContext schemaContext = getSchemaContext(dataspaceName, anchor.getSchemaSetName());
 
         if (ROOT_NODE_XPATH.equals(parentNodeXpath)) {
-            final ContainerNode containerNode = YangUtils.parseJsonData(jsonData, schemaContext);
+            final ContainerNode containerNode = YangUtils.parseData(contentType, nodeData, schemaContext);
             return new DataNodeBuilder().withContainerNode(containerNode).build();
         }
 
         final ContainerNode containerNode = YangUtils
-                .parseJsonData(jsonData, schemaContext, parentNodeXpath);
+                .parseData(contentType, nodeData, schemaContext, parentNodeXpath);
+
         return new DataNodeBuilder()
                 .withParentNodeXpath(parentNodeXpath)
                 .withContainerNode(containerNode)
@@ -248,18 +265,19 @@ public class CpsDataServiceImpl implements CpsDataService {
                                           final Map<String, String> nodesJsonData) {
         return nodesJsonData.entrySet().stream().map(nodeJsonData ->
             buildDataNode(dataspaceName, anchorName, nodeJsonData.getKey(),
-                nodeJsonData.getValue())).collect(Collectors.toList());
+                nodeJsonData.getValue(), ContentType.JSON)).collect(Collectors.toList());
     }
 
     private Collection<DataNode> buildDataNodes(final String dataspaceName,
                                                 final String anchorName,
                                                 final String parentNodeXpath,
-                                                final String jsonData) {
+                                                final String nodeData,
+                                                final ContentType contentType) {
 
         final Anchor anchor = cpsAdminService.getAnchor(dataspaceName, anchorName);
         final SchemaContext schemaContext = getSchemaContext(dataspaceName, anchor.getSchemaSetName());
         if (ROOT_NODE_XPATH.equals(parentNodeXpath)) {
-            final ContainerNode containerNode = YangUtils.parseJsonData(jsonData, schemaContext);
+            final ContainerNode containerNode = YangUtils.parseData(contentType, nodeData, schemaContext);
             final Collection<DataNode> dataNodes = new DataNodeBuilder()
                     .withContainerNode(containerNode)
                     .buildCollection();
@@ -268,7 +286,7 @@ public class CpsDataServiceImpl implements CpsDataService {
             }
             return dataNodes;
         }
-        final ContainerNode containerNode = YangUtils.parseJsonData(jsonData, schemaContext, parentNodeXpath);
+        final ContainerNode containerNode = YangUtils.parseData(contentType, nodeData, schemaContext, parentNodeXpath);
         final Collection<DataNode> dataNodes = new DataNodeBuilder()
             .withParentNodeXpath(parentNodeXpath)
             .withContainerNode(containerNode)
@@ -281,9 +299,9 @@ public class CpsDataServiceImpl implements CpsDataService {
     }
 
     private Collection<Collection<DataNode>> buildDataNodes(final String dataspaceName, final String anchorName,
-            final String parentNodeXpath, final Collection<String> jsonDataList) {
-        return jsonDataList.stream()
-                .map(jsonData -> buildDataNodes(dataspaceName, anchorName, parentNodeXpath, jsonData))
+            final String parentNodeXpath, final Collection<String> nodeDataList, final ContentType contentType) {
+        return nodeDataList.stream()
+                .map(nodeData -> buildDataNodes(dataspaceName, anchorName, parentNodeXpath, nodeData, contentType))
                 .collect(Collectors.toList());
     }
 
diff --git a/cps-service/src/main/java/org/onap/cps/utils/ContentType.java b/cps-service/src/main/java/org/onap/cps/utils/ContentType.java
new file mode 100644 (file)
index 0000000..f888504
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2022 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;
+
+public enum ContentType {
+    JSON,
+    XML
+}
diff --git a/cps-service/src/main/java/org/onap/cps/utils/XmlFileUtils.java b/cps-service/src/main/java/org/onap/cps/utils/XmlFileUtils.java
new file mode 100644 (file)
index 0000000..0946ae3
--- /dev/null
@@ -0,0 +1,165 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2022 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 java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.xml.XMLConstants;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+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.Document;
+import org.w3c.dom.Element;
+import org.xml.sax.SAXException;
+
+public class XmlFileUtils {
+
+    private static DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
+    private static final String DATA_ROOT_NODE_TAG_NAME = "data";
+    private static final String ROOT_NODE_NAMESPACE = "urn:ietf:params:xml:ns:netconf:base:1.0";
+    private static final Pattern XPATH_PROPERTY_REGEX = Pattern.compile(
+            "\\[@(\\S{1,100})=['\\\"](\\S{1,100})['\\\"]\\]");
+
+    /**
+     * Prepare XML content.
+     *
+     * @param xmlContent XML content sent to store
+     * @param schemaContext schema context
+     * @return XML content wrapped by root node (if needed)
+     */
+    public static String prepareXmlContent(final String xmlContent, final SchemaContext schemaContext) {
+
+        return addRootNodeToXmlContent(xmlContent, schemaContext.getModules().iterator().next().getName(),
+                ROOT_NODE_NAMESPACE);
+
+    }
+
+    /**
+     * Prepare XML content.
+     *
+     * @param xmlContent XML content sent to store
+     * @param parentSchemaNode Parent schema node
+     * @return XML content wrapped by root node (if needed)
+     */
+    public static String prepareXmlContent(final String xmlContent, final DataSchemaNode parentSchemaNode,
+                                           final String xpath) {
+        final String namespace = parentSchemaNode.getQName().getNamespace().toString();
+        final String parentXpathPart = xpath.substring(xpath.lastIndexOf('/') + 1);
+        final Matcher regexMatcher = XPATH_PROPERTY_REGEX.matcher(parentXpathPart);
+        if (regexMatcher.find()) {
+            final HashMap<String, String> rootNodePropertyMap = new HashMap<String, String>();
+            rootNodePropertyMap.put(regexMatcher.group(1), regexMatcher.group(2));
+            return addRootNodeToXmlContent(xmlContent, parentSchemaNode.getQName().getLocalName(), namespace,
+                    rootNodePropertyMap);
+        }
+
+        return addRootNodeToXmlContent(xmlContent, parentSchemaNode.getQName().getLocalName(), namespace);
+    }
+
+    /**
+     * Add root node to XML content.
+     *
+     * @param xmlContent xml content to add root node
+     * @param rootNodeTagName root node tag name
+     * @param namespace root node namespace
+     * @param rootNodeProperty root node properites map
+     * @return An edited content with added root node (if needed)
+     */
+    public static String addRootNodeToXmlContent(final String xmlContent, final String rootNodeTagName,
+                                                 final String namespace,
+                                                 final HashMap<String, String> rootNodeProperty) {
+        try {
+            final DocumentBuilder documentBuilder = dbFactory.newDocumentBuilder();
+            final StringBuilder xmlStringBuilder = new StringBuilder();
+            xmlStringBuilder.append(xmlContent);
+            Document xmlDoc = documentBuilder.parse(
+                    new ByteArrayInputStream(xmlStringBuilder.toString().getBytes("utf-8")));
+            final Element root = xmlDoc.getDocumentElement();
+            if (!root.getTagName().equals(rootNodeTagName) && !root.getTagName().equals(DATA_ROOT_NODE_TAG_NAME)) {
+                xmlDoc = addDataRootNode(root, rootNodeTagName, namespace, rootNodeProperty);
+                xmlDoc.setXmlStandalone(true);
+                final TransformerFactory transformerFactory = TransformerFactory.newInstance();
+                transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
+                transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");
+                final Transformer transformer = transformerFactory.newTransformer();
+                final StringWriter stringWriter = new StringWriter();
+                transformer.transform(new DOMSource(xmlDoc), new StreamResult(stringWriter));
+                return stringWriter.toString();
+            }
+            return xmlContent;
+        } catch (SAXException | IOException | ParserConfigurationException | TransformerException exception) {
+            throw new DataValidationException("Failed to parse XML data", "Invalid xml input " + exception.getMessage(),
+                    exception);
+        }
+    }
+
+    /**
+     * Add root node to XML content.
+     *
+     * @param xmlContent XML content to add root node into
+     * @param rootNodeTagName Root node tag name
+     * @return XML content with root node tag added (if needed)
+     */
+    public static String addRootNodeToXmlContent(final String xmlContent, final String rootNodeTagName,
+                                                 final String namespace) {
+        return addRootNodeToXmlContent(xmlContent, rootNodeTagName, namespace, new HashMap<String, String>());
+    }
+
+    /**
+     * Add root node into DOM element.
+     *
+     * @param node DOM element to add root node into
+     * @param tagName Root tag name to add
+     * @return DOM element with a root node
+     */
+    static Document addDataRootNode(final Element node, final String tagName, final String namespace,
+                                    final HashMap<String, String> rootNodeProperty) {
+        try {
+            final DocumentBuilder docBuilder = dbFactory.newDocumentBuilder();
+            final Document document = docBuilder.newDocument();
+            final Element rootElement = document.createElementNS(namespace, tagName);
+            for (final Map.Entry<String, String> entry : rootNodeProperty.entrySet()) {
+                final Element propertyElement = document.createElement(entry.getKey());
+                propertyElement.setTextContent(entry.getValue());
+                rootElement.appendChild(propertyElement);
+            }
+            rootElement.appendChild(document.adoptNode(node));
+            document.appendChild(rootElement);
+            return document;
+        } catch (final ParserConfigurationException exception) {
+            throw new DataValidationException("Can't parse XML", "XML can't be parsed", exception);
+        }
+    }
+}
index 9a61579..eb0c764 100644 (file)
@@ -4,6 +4,7 @@
  *  Modifications Copyright (C) 2021 Bell Canada.
  *  Modifications Copyright (C) 2021 Pantheon.tech
  *  Modifications Copyright (C) 2022 TechMahindra Ltd.
+ *  Modifications Copyright (C) 2022 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,27 +28,40 @@ import com.google.gson.JsonSyntaxException;
 import com.google.gson.stream.JsonReader;
 import java.io.IOException;
 import java.io.StringReader;
+import java.net.URISyntaxException;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
 import java.util.List;
+import java.util.Map;
 import java.util.Optional;
 import java.util.stream.Collectors;
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
 import lombok.AccessLevel;
 import lombok.NoArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.onap.cps.cpspath.parser.CpsPathUtil;
+import org.onap.cps.cpspath.parser.PathParsingException;
 import org.onap.cps.spi.exceptions.DataValidationException;
 import org.opendaylight.yangtools.yang.common.QName;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
+import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild;
+import org.opendaylight.yangtools.yang.data.api.schema.LeafNode;
+import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
 import org.opendaylight.yangtools.yang.data.api.schema.builder.DataContainerNodeBuilder;
 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
 import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactory;
 import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactorySupplier;
 import org.opendaylight.yangtools.yang.data.codec.gson.JsonParserStream;
+import org.opendaylight.yangtools.yang.data.codec.xml.XmlParserStream;
 import org.opendaylight.yangtools.yang.data.impl.schema.Builders;
 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNormalizedNodeStreamWriter;
+import org.opendaylight.yangtools.yang.data.impl.schema.NormalizedNodeResult;
 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
@@ -55,16 +69,52 @@ import org.opendaylight.yangtools.yang.model.api.EffectiveStatementInference;
 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
 import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier;
 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
+import org.xml.sax.SAXException;
 
 @Slf4j
 @NoArgsConstructor(access = AccessLevel.PRIVATE)
 public class YangUtils {
 
-    private static final String XPATH_DELIMITER_REGEX = "\\/";
-    private static final String XPATH_NODE_KEY_ATTRIBUTES_REGEX = "\\[.*?\\]";
+    /**
+     * Parses data into Collection of NormalizedNode according to given schema context.
+     *
+     * @param nodeData      data string
+     * @param schemaContext schema context describing associated data model
+     * @return the NormalizedNode object
+     */
+    public static ContainerNode parseData(final ContentType contentType, final String nodeData,
+                                           final SchemaContext schemaContext) {
+        if (contentType == ContentType.JSON) {
+            return parseJsonData(nodeData, schemaContext, Optional.empty());
+        }
+        return parseXmlData(XmlFileUtils.prepareXmlContent(nodeData, schemaContext), schemaContext,
+                Optional.empty());
+    }
 
     /**
-     * Parses jsonData into Collection of NormalizedNode according to given schema context.
+     * Parses data into NormalizedNode according to given schema context.
+     *
+     * @param nodeData      data string
+     * @param schemaContext schema context describing associated data model
+     * @return the NormalizedNode object
+     */
+    public static ContainerNode parseData(final ContentType contentType, final String nodeData,
+                                           final SchemaContext schemaContext, final String parentNodeXpath) {
+        final DataSchemaNode parentSchemaNode =
+                (DataSchemaNode) getDataSchemaNodeAndIdentifiersByXpath(parentNodeXpath, schemaContext)
+                        .get("dataSchemaNode");
+        final Collection<QName> dataSchemaNodeIdentifiers =
+                (Collection<QName>) getDataSchemaNodeAndIdentifiersByXpath(parentNodeXpath, schemaContext)
+                        .get("dataSchemaNodeIdentifiers");
+        if (contentType == ContentType.JSON) {
+            return parseJsonData(nodeData, schemaContext, Optional.of(dataSchemaNodeIdentifiers));
+        }
+        return parseXmlData(XmlFileUtils.prepareXmlContent(nodeData, parentSchemaNode, parentNodeXpath), schemaContext,
+                Optional.of(dataSchemaNodeIdentifiers));
+    }
+
+    /**
+     * Parses data into Collection of NormalizedNode according to given schema context.
      *
      * @param jsonData      json data as string
      * @param schemaContext schema context describing associated data model
@@ -85,7 +135,8 @@ public class YangUtils {
     public static ContainerNode parseJsonData(final String jsonData, final SchemaContext schemaContext,
         final String parentNodeXpath) {
         final Collection<QName> dataSchemaNodeIdentifiers =
-                getDataSchemaNodeIdentifiersByXpath(parentNodeXpath, schemaContext);
+                (Collection<QName>) getDataSchemaNodeAndIdentifiersByXpath(parentNodeXpath, schemaContext)
+                        .get("dataSchemaNodeIdentifiers");
         return parseJsonData(jsonData, schemaContext, Optional.of(dataSchemaNodeIdentifiers));
     }
 
@@ -105,7 +156,7 @@ public class YangUtils {
             final EffectiveModelContext effectiveModelContext = ((EffectiveModelContext) schemaContext);
             final EffectiveStatementInference effectiveStatementInference =
                     SchemaInferenceStack.of(effectiveModelContext,
-                    SchemaNodeIdentifier.Absolute.of(dataSchemaNodeIdentifiers.get())).toInference();
+                            SchemaNodeIdentifier.Absolute.of(dataSchemaNodeIdentifiers.get())).toInference();
             jsonParserStream =
                     JsonParserStream.create(normalizedNodeStreamWriter, jsonCodecFactory, effectiveStatementInference);
         } else {
@@ -115,22 +166,56 @@ public class YangUtils {
         try {
             jsonParserStream.parse(jsonReader);
             jsonParserStream.close();
-        } catch (final JsonSyntaxException exception) {
+        } catch (final IOException | JsonSyntaxException exception) {
             throw new DataValidationException(
-                "Failed to parse json data: " + jsonData, exception.getMessage(), exception);
-        } catch (final IOException | IllegalStateException illegalStateException) {
+                    "Failed to parse json data: " + jsonData, exception.getMessage(), exception);
+        } catch (final IllegalStateException | IllegalArgumentException exception) {
             throw new DataValidationException(
-                "Failed to parse json data. Unsupported xpath or json data:" + jsonData, illegalStateException
-                .getMessage(), illegalStateException);
+                    "Failed to parse json data. Unsupported xpath or json data:" + jsonData, exception
+                    .getMessage(), exception);
         }
         return dataContainerNodeBuilder.build();
     }
 
+    private static ContainerNode parseXmlData(final String xmlData, final SchemaContext schemaContext,
+                                               final Optional<Collection<QName>> dataSchemaNodeIdentifiers) {
+        final XMLInputFactory factory = XMLInputFactory.newInstance();
+        factory.setProperty(XMLInputFactory.SUPPORT_DTD, false);
+        final NormalizedNodeResult normalizedNodeResult = new NormalizedNodeResult();
+        final NormalizedNodeStreamWriter normalizedNodeStreamWriter = ImmutableNormalizedNodeStreamWriter
+                .from(normalizedNodeResult);
+
+        final XmlParserStream xmlParser;
+        final EffectiveModelContext effectiveModelContext = ((EffectiveModelContext) schemaContext);
+
+        if (dataSchemaNodeIdentifiers.isPresent()) {
+            final EffectiveStatementInference effectiveStatementInference =
+                    SchemaInferenceStack.of(effectiveModelContext,
+                            SchemaNodeIdentifier.Absolute.of(dataSchemaNodeIdentifiers.get())).toInference();
+            xmlParser = XmlParserStream.create(normalizedNodeStreamWriter, effectiveStatementInference);
+        } else {
+            xmlParser = XmlParserStream.create(normalizedNodeStreamWriter, effectiveModelContext);
+        }
+
+        try {
+            final XMLStreamReader reader = factory.createXMLStreamReader(new StringReader(xmlData));
+            xmlParser.parse(reader);
+            xmlParser.close();
+        } catch (final XMLStreamException | URISyntaxException | IOException
+                       | SAXException | NullPointerException exception) {
+            throw new DataValidationException(
+                    "Failed to parse xml data: " + xmlData, exception.getMessage(), exception);
+        }
+        final NormalizedNode normalizedNode = getFirstChildXmlRoot(normalizedNodeResult.getResult());
+        return Builders.containerBuilder().withChild((DataContainerChild) normalizedNode)
+                .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(schemaContext.getQName())).build();
+    }
+
     /**
      * Create an xpath form a Yang Tools NodeIdentifier (i.e. PathArgument).
      *
      * @param nodeIdentifier the NodeIdentifier
-     * @return an xpath
+     * @return a xpath
      */
     public static String buildXpath(final YangInstanceIdentifier.PathArgument nodeIdentifier) {
         final StringBuilder xpathBuilder = new StringBuilder();
@@ -138,20 +223,20 @@ public class YangUtils {
 
         if (nodeIdentifier instanceof YangInstanceIdentifier.NodeIdentifierWithPredicates) {
             xpathBuilder.append(getKeyAttributesStatement(
-                (YangInstanceIdentifier.NodeIdentifierWithPredicates) nodeIdentifier));
+                    (YangInstanceIdentifier.NodeIdentifierWithPredicates) nodeIdentifier));
         }
         return xpathBuilder.toString();
     }
 
 
     private static String getKeyAttributesStatement(
-        final YangInstanceIdentifier.NodeIdentifierWithPredicates nodeIdentifier) {
+            final YangInstanceIdentifier.NodeIdentifierWithPredicates nodeIdentifier) {
         final List<String> keyAttributes = nodeIdentifier.entrySet().stream().map(
-            entry -> {
-                final String name = entry.getKey().getLocalName();
-                final String value = String.valueOf(entry.getValue()).replace("'", "\\'");
-                return String.format("@%s='%s'", name, value);
-            }
+                entry -> {
+                    final String name = entry.getKey().getLocalName();
+                    final String value = String.valueOf(entry.getValue()).replace("'", "\\'");
+                    return String.format("@%s='%s'", name, value);
+                }
         ).collect(Collectors.toList());
 
         if (keyAttributes.isEmpty()) {
@@ -162,26 +247,23 @@ public class YangUtils {
         }
     }
 
-    private static Collection<QName> getDataSchemaNodeIdentifiersByXpath(final String parentNodeXpath,
-                                                                      final SchemaContext schemaContext) {
+    private static Map<String, Object> getDataSchemaNodeAndIdentifiersByXpath(final String parentNodeXpath,
+                                                                              final SchemaContext schemaContext) {
         final String[] xpathNodeIdSequence = xpathToNodeIdSequence(parentNodeXpath);
-        return findDataSchemaNodeIdentifiersByXpathNodeIdSequence(xpathNodeIdSequence, schemaContext.getChildNodes(),
+        return findDataSchemaNodeAndIdentifiersByXpathNodeIdSequence(xpathNodeIdSequence, schemaContext.getChildNodes(),
                 new ArrayList<>());
     }
 
     private static String[] xpathToNodeIdSequence(final String xpath) {
-        final String[] xpathNodeIdSequence = Arrays.stream(xpath
-                        .replaceAll(XPATH_NODE_KEY_ATTRIBUTES_REGEX, "")
-                        .split(XPATH_DELIMITER_REGEX))
-                .filter(identifier -> !identifier.isEmpty())
-                .toArray(String[]::new);
-        if (xpathNodeIdSequence.length < 1) {
-            throw new DataValidationException("Invalid xpath.", "Xpath contains no node identifiers.");
+        try {
+            return CpsPathUtil.getXpathNodeIdSequence(xpath);
+        } catch (final PathParsingException pathParsingException) {
+            throw new DataValidationException(pathParsingException.getMessage(), pathParsingException.getDetails(),
+                    pathParsingException);
         }
-        return xpathNodeIdSequence;
     }
 
-    private static Collection<QName> findDataSchemaNodeIdentifiersByXpathNodeIdSequence(
+    private static Map<String, Object> findDataSchemaNodeAndIdentifiersByXpathNodeIdSequence(
             final String[] xpathNodeIdSequence,
             final Collection<? extends DataSchemaNode> dataSchemaNodes,
             final Collection<QName> dataSchemaNodeIdentifiers) {
@@ -191,11 +273,15 @@ public class YangUtils {
             .findFirst().orElseThrow(() -> schemaNodeNotFoundException(currentXpathNodeId));
         dataSchemaNodeIdentifiers.add(currentDataSchemaNode.getQName());
         if (xpathNodeIdSequence.length <= 1) {
-            return dataSchemaNodeIdentifiers;
+            final Map<String, Object> dataSchemaNodeAndIdentifiers =
+                    new HashMap<>();
+            dataSchemaNodeAndIdentifiers.put("dataSchemaNode", currentDataSchemaNode);
+            dataSchemaNodeAndIdentifiers.put("dataSchemaNodeIdentifiers", dataSchemaNodeIdentifiers);
+            return dataSchemaNodeAndIdentifiers;
         }
         if (currentDataSchemaNode instanceof DataNodeContainer) {
-            return findDataSchemaNodeIdentifiersByXpathNodeIdSequence(
-                getNextLevelXpathNodeIdSequence(xpathNodeIdSequence),
+            return findDataSchemaNodeAndIdentifiersByXpathNodeIdSequence(
+                    getNextLevelXpathNodeIdSequence(xpathNodeIdSequence),
                     ((DataNodeContainer) currentDataSchemaNode).getChildNodes(),
                     dataSchemaNodeIdentifiers);
         }
@@ -212,4 +298,19 @@ public class YangUtils {
         return new DataValidationException("Invalid xpath.",
             String.format("No schema node was found for xpath identifier '%s'.", schemaNodeIdentifier));
     }
-}
+
+    private static NormalizedNode getFirstChildXmlRoot(final NormalizedNode parent) {
+        final String rootNodeType = parent.getIdentifier().getNodeType().getLocalName();
+        final Collection<DataContainerChild> children = (Collection<DataContainerChild>) parent.body();
+        final Iterator<DataContainerChild> iterator = children.iterator();
+        NormalizedNode child = null;
+        while (iterator.hasNext()) {
+            child = iterator.next();
+            if (!child.getIdentifier().getNodeType().getLocalName().equals(rootNodeType)
+                    && !(child instanceof LeafNode)) {
+                return child;
+            }
+        }
+        return getFirstChildXmlRoot(child);
+    }
+}
\ No newline at end of file
index b78ab8a..c81a50e 100644 (file)
@@ -4,7 +4,7 @@
  *  Modifications Copyright (C) 2021 Pantheon.tech
  *  Modifications Copyright (C) 2021-2022 Bell Canada.
  *  Modifications Copyright (C) 2022 TechMahindra Ltd.
- *  ================================================================================
+ *  Modifications Copyright (C) 2022 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
@@ -33,6 +33,7 @@ import org.onap.cps.spi.exceptions.DataValidationException
 import org.onap.cps.spi.model.Anchor
 import org.onap.cps.spi.model.DataNode
 import org.onap.cps.spi.model.DataNodeBuilder
+import org.onap.cps.utils.ContentType
 import org.onap.cps.yang.YangTextSchemaSourceSet
 import org.onap.cps.yang.YangTextSchemaSourceSetBuilder
 import spock.lang.Specification
@@ -61,7 +62,7 @@ class CpsDataServiceImplSpec extends Specification {
     def anchor = Anchor.builder().name(anchorName).schemaSetName(schemaSetName).build()
     def observedTimestamp = OffsetDateTime.now()
 
-    def 'Saving json data.'() {
+    def 'Saving multicontainer json data.'() {
         given: 'schema set for given anchor and dataspace references test-tree model'
             setupSchemaSetMocks('multipleDataTree.yang')
         when: 'save data method is invoked with test-tree json data'
@@ -81,6 +82,39 @@ class CpsDataServiceImplSpec extends Specification {
 
     }
 
+    def 'Saving #scenario data.'() {
+        given: 'schema set for given anchor and dataspace references test-tree model'
+            setupSchemaSetMocks('test-tree.yang')
+        when: 'save data method is invoked with test-tree #scenario data'
+            def data = TestUtils.getResourceFileContent(dataFile)
+            objectUnderTest.saveData(dataspaceName, anchorName, data, observedTimestamp, contentType)
+        then: 'the persistence service method is invoked with correct parameters'
+            1 * mockCpsDataPersistenceService.storeDataNodes(dataspaceName, anchorName,
+                    { dataNode -> dataNode.xpath[0] == '/test-tree' })
+        and: 'the CpsValidator is called on the dataspaceName and AnchorName'
+            1 * mockCpsValidator.validateNameCharacters(dataspaceName, anchorName)
+        and: 'data updated event is sent to notification service'
+            1 * mockNotificationService.processDataUpdatedEvent(dataspaceName, anchorName, '/', Operation.CREATE, observedTimestamp)
+        where: 'given parameters'
+            scenario | dataFile         | contentType
+            'json'   | 'test-tree.json' | ContentType.JSON
+            'xml'    | 'test-tree.xml'  | ContentType.XML
+    }
+
+    def 'Saving #scenarioDesired data with invalid data.'() {
+        given: 'schema set for given anchor and dataspace references test-tree model'
+        setupSchemaSetMocks('test-tree.yang')
+        when: 'save data method is invoked with test-tree json data'
+            objectUnderTest.saveData(dataspaceName, anchorName, invalidData, observedTimestamp, contentType)
+        then: 'a data validation exception is thrown'
+            thrown(DataValidationException)
+        where: 'given parameters'
+            scenarioDesired | invalidData             | contentType
+            'json'          | '{invalid  json'        | ContentType.XML
+            'xml'           | '<invalid xml'          | ContentType.JSON
+    }
+
+
     def 'Saving child data fragment under existing node.'() {
         given: 'schema set for given anchor and dataspace references test-tree model'
             setupSchemaSetMocks('test-tree.yang')
index e205a19..b70c437 100644 (file)
@@ -41,7 +41,7 @@ class JsonObjectMapperSpec extends Specification {
         then: 'the result is a valid json string (can be parsed)'
             def contentMap = new JsonSlurper().parseText(content)
         and: 'the parsed content is as expected'
-            assert contentMap.'test:bookstore'.'bookstore-name' == 'Chapters'
+            assert contentMap.'test:bookstore'.'bookstore-name' == 'Chapters/Easons'
     }
 
     def 'Map a structured object to json String error.'() {
diff --git a/cps-service/src/test/groovy/org/onap/cps/utils/XmlFileUtilsSpec.groovy b/cps-service/src/test/groovy/org/onap/cps/utils/XmlFileUtilsSpec.groovy
new file mode 100644 (file)
index 0000000..b044e2e
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+ * ============LICENSE_START=======================================================
+ *  Copyright (C) 2022 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 org.onap.cps.TestUtils
+import org.onap.cps.yang.YangTextSchemaSourceSetBuilder
+import spock.lang.Specification
+
+class XmlFileUtilsSpec 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).getSchemaContext()
+        when: 'the XML data is parsed'
+            def parsedXmlContent = XmlFileUtils.prepareXmlContent(xmlData, schemaContext)
+        then: 'the result XML is wrapped by root node defined in YANG schema'
+            assert parsedXmlContent == expectedOutput
+        where:
+            scenario                        | xmlData                                                                   || expectedOutput
+            'without root data node'        | '<?xml version="1.0" encoding="UTF-8"?><class> </class>'                  || '<?xml version="1.0" encoding="UTF-8"?><stores xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"><class> </class></stores>'
+            'with root data node'           | '<?xml version="1.0" encoding="UTF-8"?><stores><class> </class></stores>' || '<?xml version="1.0" encoding="UTF-8"?><stores><class> </class></stores>'
+            'no xml header'                 | '<stores><class> </class></stores>'                                       || '<stores><class> </class></stores>'
+    }
+
+    def 'Parse a xml content with XPath container #scenario'() {
+        given: 'YANG model schema context'
+            def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('test-tree.yang')
+            def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext()
+        and: 'Parent schema node by xPath'
+            def parentSchemaNode = YangUtils.getDataSchemaNodeAndIdentifiersByXpath(xPath, schemaContext)
+                    .get("dataSchemaNode")
+        when: 'the XML data is parsed'
+            def parsedXmlContent = XmlFileUtils.prepareXmlContent(xmlData, parentSchemaNode, xPath)
+        then: 'the result XML is wrapped by xPath defined parent root node'
+            assert parsedXmlContent == expectedOutput
+        where:
+            scenario                 | xmlData                                                                                                                                                                                    | xPath                                 || expectedOutput
+            'XML element test tree'  | '<?xml version="1.0" encoding="UTF-8"?><test-tree xmlns="org:onap:cps:test:test-tree"><branch><name>Left</name><nest><name>Small</name><birds>Sparrow</birds></nest></branch></test-tree>' | '/test-tree'                          || '<?xml version="1.0" encoding="UTF-8"?><test-tree xmlns="org:onap:cps:test:test-tree"><branch><name>Left</name><nest><name>Small</name><birds>Sparrow</birds></nest></branch></test-tree>'
+            '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>'
+
+
+    }
+
+}
index 990b718..bf6e134 100644 (file)
@@ -3,6 +3,7 @@
  *  Copyright (C) 2020-2022 Nordix Foundation
  *  Modifications Copyright (C) 2021 Pantheon.tech
  *  Modifications Copyright (C) 2022 TechMahindra Ltd.
+ *  Modifications Copyright (C) 2022 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,7 +31,7 @@ import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode
 import spock.lang.Specification
 
 class YangUtilsSpec extends Specification {
-    def 'Parsing a valid Json String.'() {
+    def 'Parsing a valid multicontainer Json String.'() {
         given: 'a yang model (file)'
             def jsonData = org.onap.cps.TestUtils.getResourceFileContent('multiple-object-data.json')
         and: 'a model for that data'
@@ -48,36 +49,62 @@ class YangUtilsSpec extends Specification {
             1       | 'last-container'
     }
 
+    def 'Parsing a valid #scenario String.'() {
+        given: 'a yang model (file)'
+            def fileData = org.onap.cps.TestUtils.getResourceFileContent(contentFile)
+        and: 'a model for that data'
+            def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('bookstore.yang')
+            def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext()
+        when: 'the data is parsed'
+            NormalizedNode result = YangUtils.parseData(contentType, fileData, schemaContext)
+        then: 'the result is a normalized node of the correct type'
+            if (revision) {
+                result.identifier.nodeType == QName.create(namespace, revision, localName)
+            } else {
+                result.identifier.nodeType == QName.create(namespace, localName)
+            }
+        where:
+            scenario | contentFile      | contentType      | namespace                                 | revision     | localName
+            'JSON'   | 'bookstore.json' | ContentType.JSON | 'org:onap:ccsdk:sample'                   | '2020-09-15' | 'bookstore'
+            'XML'    | 'bookstore.xml'  | ContentType.XML  | 'urn:ietf:params:xml:ns:netconf:base:1.0' | ''           | 'bookstore'
+    }
+
     def 'Parsing invalid data: #description.'() {
         given: 'a yang model (file)'
             def yangResourceNameToContent = TestUtils.getYangResourcesAsMap('bookstore.yang')
             def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourceNameToContent).getSchemaContext()
         when: 'invalid data is parsed'
-            YangUtils.parseJsonData(invalidJson, schemaContext)
+            YangUtils.parseData(contentType, invalidData, schemaContext)
         then: 'an exception is thrown'
             thrown(DataValidationException)
-        where: 'the following invalid json is provided'
-            invalidJson                                       | description
-            '{incomplete json'                                | 'incomplete json'
-            '{"test:bookstore": {"address": "Parnell st." }}' | 'json with un-modelled data'
-            '{" }'                                            | 'json with syntax exception'
+        where: 'the following invalid data is provided'
+            invalidData                                                                          | contentType      | description
+            '{incomplete json'                                                                   | ContentType.JSON | 'incomplete json'
+            '{"test:bookstore": {"address": "Parnell st." }}'                                    | ContentType.JSON | 'json with un-modelled data'
+            '{" }'                                                                               | ContentType.JSON | 'json with syntax exception'
+            '<data>'                                                                             | ContentType.XML  | 'incomplete xml'
+            '<data><bookstore><bookstore-anything>blabla</bookstore-anything></bookstore</data>' | ContentType.XML  | 'xml with invalid model'
+            ''                                                                                   | ContentType.XML  | 'empty xml'
     }
 
-    def 'Parsing json data fragment by xpath for #scenario.'() {
+    def 'Parsing data fragment by xpath for #scenario.'() {
         given: 'schema context'
             def yangResourcesMap = TestUtils.getYangResourcesAsMap('test-tree.yang')
             def schemaContext = YangTextSchemaSourceSetBuilder.of(yangResourcesMap).getSchemaContext()
         when: 'json string is parsed'
-            def result = YangUtils.parseJsonData(jsonData, schemaContext, parentNodeXpath)
+            def result = YangUtils.parseData(contentType, nodeData, schemaContext, parentNodeXpath)
         then: 'a ContainerNode holding collection of normalized nodes is returned'
             result.body().getAt(0) instanceof NormalizedNode == true
         then: 'result represents a node of expected type'
             result.body().getAt(0).getIdentifier().nodeType == QName.create('org:onap:cps:test:test-tree', '2020-02-02', nodeName)
         where:
-            scenario                    | jsonData                                                                      | parentNodeXpath                       || nodeName
-            'list element as container' | '{ "branch": { "name": "B", "nest": { "name": "N", "birds": ["bird"] } } }'   | '/test-tree'                          || 'branch'
-            'list element within list'  | '{ "branch": [{ "name": "B", "nest": { "name": "N", "birds": ["bird"] } }] }' | '/test-tree'                          || 'branch'
-            'container element'         | '{ "nest": { "name": "N", "birds": ["bird"] } }'                              | '/test-tree/branch[@name=\'Branch\']' || 'nest'
+            scenario                         | contentType      | nodeData                                                                                                                                                                                                      | parentNodeXpath                       || nodeName
+            'JSON list element as container' | ContentType.JSON | '{ "branch": { "name": "B", "nest": { "name": "N", "birds": ["bird"] } } }'                                                                                                                                   | '/test-tree'                          || 'branch'
+            'JSON list element within list'  | ContentType.JSON | '{ "branch": [{ "name": "B", "nest": { "name": "N", "birds": ["bird"] } }] }'                                                                                                                                 | '/test-tree'                          || 'branch'
+            'JSON container element'         | ContentType.JSON | '{ "nest": { "name": "N", "birds": ["bird"] } }'                                                                                                                                                              | '/test-tree/branch[@name=\'Branch\']' || 'nest'
+            'XML element test tree'          | ContentType.XML  | '<?xml version=\'1.0\' encoding=\'UTF-8\'?><branch xmlns="org:onap:cps:test:test-tree"><name>Left</name><nest><name>Small</name><birds>Sparrow</birds></nest></branch>'                                       | '/test-tree'                          || 'branch'
+            'XML element branch xpath'       | ContentType.XML  | '<?xml version=\'1.0\' encoding=\'UTF-8\'?><branch xmlns="org:onap:cps:test:test-tree"><name>Left</name><nest><name>Small</name><birds>Sparrow</birds><birds>Robin</birds></nest></branch>'                   | '/test-tree'                          || 'branch'
+            'XML container element'          | ContentType.XML  | '<?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\']' || 'nest'
     }
 
     def 'Parsing json data fragment by xpath error scenario: #scenario.'() {
@@ -135,5 +162,4 @@ class YangUtilsSpec extends Specification {
             'xpath contains list attribute'                | '/test-tree/branch[@name=\'Branch\']'                               || ['test-tree','branch']
             'xpath contains list attributes with /'        | '/test-tree/branch[@name=\'/Branch\']/categories[@id=\'/broken\']'  || ['test-tree','branch','categories']
     }
-
 }
index d1b8d68..459908b 100644 (file)
@@ -1,19 +1,19 @@
 {
    "test:bookstore":{
-      "bookstore-name": "Chapters",
+      "bookstore-name": "Chapters/Easons",
       "categories": [
          {
-            "code": "01",
+            "code": "01/1",
             "name": "SciFi",
             "books": [
                {
                   "authors": [
                      "Iain M. Banks"
                   ],
-                  "lang": "en",
+                  "lang": "en/it",
                   "price": "895",
                   "pub_year": "1994",
-                  "title": "Feersum Endjinn"
+                  "title": "Feersum Endjinn/Endjinn Feersum"
                },
                {
                   "authors": [
diff --git a/cps-service/src/test/resources/bookstore.xml b/cps-service/src/test/resources/bookstore.xml
new file mode 100644 (file)
index 0000000..dd45e16
--- /dev/null
@@ -0,0 +1,19 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<stores xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
+<bookstore xmlns="org:onap:ccsdk:sample">
+    <bookstore-name>Chapters</bookstore-name>
+    <categories>
+        <code>1</code>
+        <name>SciFi</name>
+        <books>
+            <title>2001: A Space Odyssey</title>
+            <lang>en</lang>
+            <authors>
+                Iain M. Banks
+            </authors>
+            <pub_year>1994</pub_year>
+            <price>895</price>
+        </books>
+    </categories>
+</bookstore>
+</stores>
\ No newline at end of file
diff --git a/cps-service/src/test/resources/bookstore_xpath.xml b/cps-service/src/test/resources/bookstore_xpath.xml
new file mode 100644 (file)
index 0000000..e206901
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<bookstore xmlns="org:onap:ccsdk:sample">
+    <bookstore-name>Chapters</bookstore-name>
+    <categories>
+        <code>1</code>
+        <name>SciFi</name>
+        <books>
+            <title>2001: A Space Odyssey</title>
+            <lang>en</lang>
+            <authors>
+                Iain M. Banks
+            </authors>
+            <pub_year>1994</pub_year>
+            <price>895</price>
+        </books>
+    </categories>
+</bookstore>
\ No newline at end of file
diff --git a/cps-service/src/test/resources/test-tree.xml b/cps-service/src/test/resources/test-tree.xml
new file mode 100644 (file)
index 0000000..3daa814
--- /dev/null
@@ -0,0 +1,27 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<data xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
+    <test-tree xmlns="org:onap:cps:test:test-tree">
+        <branch>
+            <name>Left</name>
+            <nest>
+                <name>Small</name>
+                <birds>Sparrow</birds>
+                <birds>Robin</birds>
+                <birds>Finch</birds>
+            </nest>
+        </branch>
+        <branch>
+            <name>Right</name>
+            <nest>
+                <name>Big</name>
+                <birds>Owl</birds>
+                <birds>Raven</birds>
+                <birds>Crow</birds>
+            </nest>
+        </branch>
+        <fruit>
+            <name>Apple</name>
+            <color>Green</color>
+        </fruit>
+    </test-tree>
+</data>
index 89d6784..8f4b522 100644 (file)
@@ -2,11 +2,11 @@
   "test-tree": {
     "branch": [
       {
-        "name": "Left",
+        "name": "LEFT/left",
         "nest": {
-          "name": "Small",
+          "name": "SMALL/small",
           "birds": [
-            "Sparrow",
+            "SPARROW/sparrow",
             "Robin",
             "Finch"
           ]
index dde9616..78f0fbd 100755 (executable)
@@ -57,7 +57,8 @@ else
     rm -f ${WORKSPACE}/env.properties
     cd /tmp
     git clone "https://gerrit.onap.org/r/ci-management"
-    source /tmp/ci-management/jjb/integration/include-raw-integration-install-robotframework-py3.sh
+#    source /tmp/ci-management/jjb/integration/include-raw-integration-install-robotframework-py3.sh
+    source ${WORKSPACE}/install-robotframework.sh
 fi
 
 # install eteutils
index 4952616..9fee634 100644 (file)
@@ -6,7 +6,7 @@ pyhocon
 requests
 robotframework-httplibrary
 robotframework-requests==0.9.3
-robotframework-selenium2library
+robotframework-selenium2library==3.0.0
 robotframework-extendedselenium2library
 robotframework-sshlibrary
 scapy
index 2da2b73..096bd07 100644 (file)
@@ -44,10 +44,10 @@ Create Data Node
 
 Get Data Node by XPath
     ${uri}=             Set Variable        ${basePath}/v1/dataspaces/${dataspaceName}/anchors/${anchorName}/node
-    ${params}=          Create Dictionary   xpath=/test-tree/branch[@name='Left']/nest
+    ${params}=          Create Dictionary   xpath=/test-tree/branch[@name='LEFT/left']/nest
     ${headers}=         Create Dictionary   Authorization=${auth}
     ${response}=        Get On Session      CPS_URL   ${uri}   params=${params}   headers=${headers}   expected_status=200
     ${responseJson}=    Set Variable        ${response.json()['tree:nest']}
-    Should Be Equal As Strings              ${responseJson['name']}   Small
+    Should Be Equal As Strings              ${responseJson['name']}   SMALL/small
 
 
index 3221900..9aaf050 100755 (executable)
@@ -50,6 +50,8 @@ Bug Fixes
 3.2.0
    - `CPS-1312 <https://jira.onap.org/browse/CPS-1312>`_  CPS(/NCMP) does not have version control
    - `CPS-1350 <https://jira.onap.org/browse/CPS-1350>`_  [CPS/NCMP] Add Basic Auth to CPS/NCMP OpenAPI Definitions
+   - `CPS-1433 <https://jira.onap.org/browse/CPS-1433>`_  [CPS/NCMP] Fix to allow posting data with '/'
+   - `CPS-1409 <https://jira.onap.org/browse/CPS-1409>`_  [CPS/NCMP] Fix Delete uses case with '/' in path
 
 Known Limitations, Issues and Workarounds
 -----------------------------------------