Bug 5915 - PATCH with "target":"/" error 91/40991/24
authorIvan Hrasko <ihrasko@cisco.com>
Wed, 29 Jun 2016 07:54:38 +0000 (09:54 +0200)
committerIvan Hrasko <ihrasko@cisco.com>
Mon, 18 Jul 2016 13:55:21 +0000 (13:55 +0000)
- fixed bug + added unit test (example of usage)
for json and xml
- improved solution of bug5730

Change-Id: I0d8042ffdced97918ff936817513fc30163be577
Signed-off-by: Ivan Hrasko <ihrasko@cisco.com>
opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/rest/impl/JsonToPATCHBodyReader.java
opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/rest/impl/XmlToPATCHBodyReader.java
opendaylight/restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/rest/impl/test/providers/TestJsonPATCHBodyReader.java
opendaylight/restconf/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/rest/impl/test/providers/TestXmlPATCHBodyReader.java
opendaylight/restconf/sal-rest-connector/src/test/resources/instanceidentifier/json/jsonPATCHdataCompleteTargetInURI.json [new file with mode: 0644]
opendaylight/restconf/sal-rest-connector/src/test/resources/instanceidentifier/xml/xmlPATCHdataCompleteTargetInURI.xml [new file with mode: 0644]

index dd5ced4a37cb6f8f09e29baeee5e7e3b27fd9b73..3e91d79b3a8a04d9f98788dd1e5189b3b6683d19 100644 (file)
@@ -8,6 +8,8 @@
 
 package org.opendaylight.netconf.sal.rest.impl;
 
+import static org.opendaylight.netconf.sal.restconf.impl.PATCHEditOperation.isPatchOperationWithValue;
+
 import com.google.common.collect.ImmutableList;
 import com.google.gson.stream.JsonReader;
 import com.google.gson.stream.JsonToken;
@@ -31,12 +33,12 @@ import org.opendaylight.netconf.sal.rest.api.RestconfService;
 import org.opendaylight.netconf.sal.restconf.impl.ControllerContext;
 import org.opendaylight.netconf.sal.restconf.impl.InstanceIdentifierContext;
 import org.opendaylight.netconf.sal.restconf.impl.PATCHContext;
-import org.opendaylight.netconf.sal.restconf.impl.PATCHEditOperation;
 import org.opendaylight.netconf.sal.restconf.impl.PATCHEntity;
 import org.opendaylight.netconf.sal.restconf.impl.RestconfDocumentedException;
 import org.opendaylight.netconf.sal.restconf.impl.RestconfError.ErrorTag;
 import org.opendaylight.netconf.sal.restconf.impl.RestconfError.ErrorType;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
 import org.opendaylight.yangtools.yang.data.codec.gson.JsonParserStream;
@@ -214,10 +216,18 @@ public class JsonToPATCHBodyReader extends AbstractIdentifierAwareJaxRsProvider
                     edit.setOperation(in.nextString());
                     break;
                 case "target" :
-                    edit.setTarget(codec.deserialize(codec.serialize(path.getInstanceIdentifier()) + in.nextString()));
-                    edit.setTargetSchemaNode(SchemaContextUtil.findDataSchemaNode(path.getSchemaContext(),
-                            codec.getDataContextTree().getChild(edit.getTarget()).getDataSchemaNode().getPath()
-                                    .getParent()));
+                    // target can be specified completely in request URI
+                    final String target = in.nextString();
+                    if (target.equals("/")) {
+                        edit.setTarget(path.getInstanceIdentifier());
+                        edit.setTargetSchemaNode(path.getSchemaContext());
+                    } else {
+                        edit.setTarget(codec.deserialize(codec.serialize(path.getInstanceIdentifier()).concat(target)));
+                        edit.setTargetSchemaNode(SchemaContextUtil.findDataSchemaNode(path.getSchemaContext(),
+                                codec.getDataContextTree().getChild(edit.getTarget()).getDataSchemaNode().getPath()
+                                        .getParent()));
+                    }
+
                     break;
                 case "value" :
                     // save data defined in value node for next (later) processing, because target needs to be read
@@ -281,7 +291,28 @@ public class JsonToPATCHBodyReader extends AbstractIdentifierAwareJaxRsProvider
         while (in.hasNext()) {
             value.append("\"" + in.nextName() + "\"");
             value.append(":");
-            value.append("\"" + in.nextString() + "\"");
+
+            if (in.peek() == JsonToken.STRING) {
+                value.append("\"" + in.nextString() + "\"");
+            } else {
+                if (in.peek() == JsonToken.BEGIN_ARRAY) {
+                    in.beginArray();
+                    value.append("[");
+
+                    while (in.hasNext()) {
+                        readValueObject(value, in);
+                        if (in.peek() != JsonToken.END_ARRAY) {
+                            value.append(",");
+                        }
+                    }
+
+                    in.endArray();
+                    value.append("]");
+                } else {
+                    readValueObject(value, in);
+                }
+            }
+
             if (in.peek() != JsonToken.END_OBJECT) {
                 value.append(",");
             }
@@ -314,7 +345,15 @@ public class JsonToPATCHBodyReader extends AbstractIdentifierAwareJaxRsProvider
         if (edit.getOperation() != null && edit.getTargetSchemaNode() != null
                 && checkDataPresence(edit.getOperation(), (edit.getData() != null))) {
             if (isPatchOperationWithValue(edit.getOperation())) {
-                return new PATCHEntity(edit.getId(), edit.getOperation(), edit.getTarget().getParent(), edit.getData());
+                // for lists allow to manipulate with list items through their parent
+                YangInstanceIdentifier targetNode;
+                if (edit.getTarget().getLastPathArgument() instanceof NodeIdentifierWithPredicates) {
+                    targetNode = edit.getTarget().getParent();
+                } else {
+                    targetNode = edit.getTarget();
+                }
+
+                return new PATCHEntity(edit.getId(), edit.getOperation(), targetNode, edit.getData());
             } else {
                 return new PATCHEntity(edit.getId(), edit.getOperation(), edit.getTarget());
             }
@@ -346,23 +385,6 @@ public class JsonToPATCHBodyReader extends AbstractIdentifierAwareJaxRsProvider
         }
     }
 
-    /**
-     * Check if operation requires data to be specified
-     * @param operation Name of the operation to be checked
-     * @return true if operation requires data, false otherwise
-     */
-    private boolean isPatchOperationWithValue(@Nonnull final String operation) {
-        switch (PATCHEditOperation.valueOf(operation.toUpperCase())) {
-            case CREATE:
-            case MERGE:
-            case REPLACE:
-            case INSERT:
-                return true;
-            default:
-                return false;
-        }
-    }
-
     /**
      * Helper class representing one patch edit
      */
index 74a6cc81e55ee57231e21ff56226ee7a4628ca13..595ee9c02f1750652c9b31e05cf793126f6ef94a 100644 (file)
@@ -39,6 +39,7 @@ import org.opendaylight.netconf.sal.restconf.impl.RestconfError.ErrorTag;
 import org.opendaylight.netconf.sal.restconf.impl.RestconfError.ErrorType;
 import org.opendaylight.yangtools.yang.common.QName;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
 import org.opendaylight.yangtools.yang.data.impl.codec.xml.XmlUtils;
 import org.opendaylight.yangtools.yang.data.impl.schema.transform.dom.parser.DomToNormalizedNodeParserFactory;
@@ -132,13 +133,12 @@ public class XmlToPATCHBodyReader extends AbstractIdentifierAwareJaxRsProvider i
             final Element element = (Element) editNodes.item(i);
             final String operation = element.getElementsByTagName("operation").item(0).getFirstChild().getNodeValue();
             final String editId = element.getElementsByTagName("edit-id").item(0).getFirstChild().getNodeValue();
-            final String target = element.getElementsByTagName("target").item(0).getFirstChild().getNodeValue()
-                    .replaceFirst("/", "");
+            final String target = element.getElementsByTagName("target").item(0).getFirstChild().getNodeValue();
             final List<Element> values = readValueNodes(element, operation);
             final Element firstValueElement = values != null ? values.get(0) : null;
 
             // get namespace according to schema node from path context or value
-            String namespace = (firstValueElement == null) ?
+            final String namespace = (firstValueElement == null) ?
                     schemaNode.getQName().getNamespace().toString() : firstValueElement.getNamespaceURI();
 
             // find module according to namespace
@@ -149,17 +149,25 @@ public class XmlToPATCHBodyReader extends AbstractIdentifierAwareJaxRsProvider i
             final StringModuleInstanceIdentifierCodec codec = new StringModuleInstanceIdentifierCodec(
                     pathContext.getSchemaContext(), module.getName());
 
-            // find complete path to target
-            final YangInstanceIdentifier targetII = codec.deserialize(codec.serialize(pathContext
-                    .getInstanceIdentifier()).concat(prepareNonCondXpath(schemaNode, target, firstValueElement,
-                    namespace, module.getQNameModule().getFormattedRevision())));
+            // find complete path to target and target schema node
+            // target can be also empty (only slash)
+            YangInstanceIdentifier targetII;
+            SchemaNode targetNode;
+            if (target.equals("/")) {
+                targetII = pathContext.getInstanceIdentifier();
+                targetNode = pathContext.getSchemaContext();
+            } else {
+                targetII = codec.deserialize(codec.serialize(pathContext.getInstanceIdentifier())
+                        .concat(prepareNonCondXpath(schemaNode, target.replaceFirst("/", ""), firstValueElement,
+                                namespace, module.getQNameModule().getFormattedRevision())));
 
-            // move schema node and get target node
-            schemaNode = (DataSchemaNode) SchemaContextUtil.findDataSchemaNode(pathContext.getSchemaContext(),
-                    codec.getDataContextTree().getChild(targetII).getDataSchemaNode().getPath());
+                targetNode = SchemaContextUtil.findDataSchemaNode(pathContext.getSchemaContext(),
+                        codec.getDataContextTree().getChild(targetII).getDataSchemaNode().getPath().getParent());
 
-            final SchemaNode targetNode = SchemaContextUtil.findDataSchemaNode(pathContext.getSchemaContext(),
-                    codec.getDataContextTree().getChild(targetII).getDataSchemaNode().getPath().getParent());
+                // move schema node
+                schemaNode = (DataSchemaNode) SchemaContextUtil.findDataSchemaNode(pathContext.getSchemaContext(),
+                        codec.getDataContextTree().getChild(targetII).getDataSchemaNode().getPath());
+            }
 
             if (targetNode == null) {
                 LOG.debug("Target node {} not found in path {} ", target, pathContext.getSchemaNode());
@@ -174,7 +182,12 @@ public class XmlToPATCHBodyReader extends AbstractIdentifierAwareJaxRsProvider i
                         parsed = parserFactory.getMapNodeParser().parse(values, (ListSchemaNode) schemaNode);
                     }
 
-                    resultCollection.add(new PATCHEntity(editId, operation, targetII.getParent(), parsed));
+                    // for lists allow to manipulate with list items through their parent
+                    if (targetII.getLastPathArgument() instanceof NodeIdentifierWithPredicates) {
+                        targetII = targetII.getParent();
+                    }
+
+                    resultCollection.add(new PATCHEntity(editId, operation, targetII, parsed));
                 } else {
                     resultCollection.add(new PATCHEntity(editId, operation, targetII));
                 }
index 35552cd2897260a235c0ffc6499c4676e372e05f..ab48d5a02f37fdf08e4e80250c355d51ab937615 100644 (file)
@@ -55,6 +55,9 @@ public class TestJsonPATCHBodyReader extends AbstractBodyReaderTest {
         checkPATCHContext(returnValue);
     }
 
+    /**
+     * Test of successful PATCH consisting of create and delete PATCH operations.
+     */
     @Test
     public void modulePATCHCreateAndDeleteTest() throws Exception {
         final String uri = "instance-identifier-patch-module:patch-cont/my-list1/leaf1";
@@ -68,8 +71,12 @@ public class TestJsonPATCHBodyReader extends AbstractBodyReaderTest {
         checkPATCHContext(returnValue);
     }
 
+    /**
+     * Test trying to use PATCH create operation which requires value without value. Test should fail with
+     * {@link RestconfDocumentedException} with error code 400.
+     */
     @Test
-    public void modulePATCHValueMissingTest() throws Exception {
+    public void modulePATCHValueMissingNegativeTest() throws Exception {
         final String uri = "instance-identifier-patch-module:patch-cont/my-list1/leaf1";
         mockBodyReader(uri, jsonPATCHBodyReader, false);
 
@@ -79,13 +86,17 @@ public class TestJsonPATCHBodyReader extends AbstractBodyReaderTest {
         try {
             jsonPATCHBodyReader.readFrom(null, null, null, mediaType, null, inputStream);
             fail("Test should return error 400 due to missing value node when attempt to invoke create operation");
-        } catch (RestconfDocumentedException e) {
+        } catch (final RestconfDocumentedException e) {
             assertEquals("Error code 400 expected", 400, e.getErrors().get(0).getErrorTag().getStatusCode());
         }
     }
 
+    /**
+     * Test trying to use value with PATCH delete operation which does not support value. Test should fail with
+     * {@link RestconfDocumentedException} with error code 400.
+     */
     @Test
-    public void modulePATCHValueNotSupportedTest() throws Exception {
+    public void modulePATCHValueNotSupportedNegativeTest() throws Exception {
         final String uri = "instance-identifier-patch-module:patch-cont/my-list1/leaf1";
         mockBodyReader(uri, jsonPATCHBodyReader, false);
 
@@ -95,8 +106,24 @@ public class TestJsonPATCHBodyReader extends AbstractBodyReaderTest {
         try {
             jsonPATCHBodyReader.readFrom(null, null, null, mediaType, null, inputStream);
             fail("Test should return error 400 due to present value node when attempt to invoke delete operation");
-        } catch (RestconfDocumentedException e) {
+        } catch (final RestconfDocumentedException e) {
             assertEquals("Error code 400 expected", 400, e.getErrors().get(0).getErrorTag().getStatusCode());
         }
     }
+
+    /**
+     * Test using PATCH when target is completely specified in request URI and thus target leaf contains only '/' sign.
+     */
+    @Test
+    public void modulePATCHCompleteTargetInURITest() throws Exception {
+        final String uri = "instance-identifier-patch-module:patch-cont";
+        mockBodyReader(uri, jsonPATCHBodyReader, false);
+
+        final InputStream inputStream = TestJsonBodyReader.class
+                .getResourceAsStream("/instanceidentifier/json/jsonPATCHdataCompleteTargetInURI.json");
+
+        final PATCHContext returnValue = jsonPATCHBodyReader
+                .readFrom(null, null, null, mediaType, null, inputStream);
+        checkPATCHContext(returnValue);
+    }
 }
index 8aa11e17368b65b8b03343d87a7add7988283b3c..3eef699171c12ba67ec531c522aa131f9fdf06b0 100644 (file)
@@ -101,4 +101,18 @@ public class TestXmlPATCHBodyReader extends AbstractBodyReaderTest {
                 .readFrom(null, null, null, mediaType, null, inputStream);
         checkPATCHContext(returnValue);
     }
+
+    /**
+     * Test using PATCH when target is completely specified in request URI and thus target leaf contains only '/' sign.
+     */
+    @Test
+    public void modulePATCHCompleteTargetInURITest() throws Exception {
+        final String uri = "instance-identifier-patch-module:patch-cont";
+        mockBodyReader(uri, xmlPATCHBodyReader, false);
+        final InputStream inputStream = TestXmlBodyReader.class
+                .getResourceAsStream("/instanceidentifier/xml/xmlPATCHdataCompleteTargetInURI.xml");
+        final PATCHContext returnValue = xmlPATCHBodyReader
+                .readFrom(null, null, null, mediaType, null, inputStream);
+        checkPATCHContext(returnValue);
+    }
 }
diff --git a/opendaylight/restconf/sal-rest-connector/src/test/resources/instanceidentifier/json/jsonPATCHdataCompleteTargetInURI.json b/opendaylight/restconf/sal-rest-connector/src/test/resources/instanceidentifier/json/jsonPATCHdataCompleteTargetInURI.json
new file mode 100644 (file)
index 0000000..1b170c7
--- /dev/null
@@ -0,0 +1,44 @@
+{
+  "ietf-yang-patch:yang-patch" : {
+
+    "patch-id" : "test-patch",
+    "comment" : "Test to create and replace data in container directly using / sign as a target",
+    "edit" : [
+      {
+        "edit-id": "edit1",
+        "operation": "create",
+        "target": "/",
+        "value": {
+          "patch-cont": {
+            "my-list1": [
+              {
+                "name": "my-list1 - A",
+                "my-leaf11": "I am leaf11-0",
+                "my-leaf12": "I am leaf12-1"
+              },
+              {
+                "name": "my-list1 - B",
+                "my-leaf11": "I am leaf11-0",
+                "my-leaf12": "I am leaf12-1"
+              }
+            ]
+          }
+        }
+      },
+      {
+        "edit-id": "edit2",
+        "operation": "replace",
+        "target": "/",
+        "value": {
+          "patch-cont": {
+            "my-list1": {
+              "name": "my-list1 - Replacing",
+              "my-leaf11": "I am leaf11-0",
+              "my-leaf12": "I am leaf12-1"
+            }
+          }
+        }
+      }
+    ]
+  }
+}
\ No newline at end of file
diff --git a/opendaylight/restconf/sal-rest-connector/src/test/resources/instanceidentifier/xml/xmlPATCHdataCompleteTargetInURI.xml b/opendaylight/restconf/sal-rest-connector/src/test/resources/instanceidentifier/xml/xmlPATCHdataCompleteTargetInURI.xml
new file mode 100644 (file)
index 0000000..23d2ce0
--- /dev/null
@@ -0,0 +1,44 @@
+<!--
+  ~ Copyright (c) 2016 Cisco Systems, Inc. and others.  All rights reserved.
+  ~
+  ~ This program and the accompanying materials are made available under the
+  ~ terms of the Eclipse Public License v1.0 which accompanies this distribution,
+  ~ and is available at http://www.eclipse.org/legal/epl-v10.html
+  -->
+<yang-patch xmlns="urn:ietf:params:xml:ns:yang:ietf-yang-patch">
+    <patch-id>test-patch</patch-id>
+    <comment>Test to create and replace data in container directly using / sign as a target</comment>
+    <edit>
+        <edit-id>edit1</edit-id>
+        <operation>create</operation>
+        <target>/</target>
+        <value>
+            <patch-cont xmlns="instance:identifier:patch:module">
+                <my-list1>
+                    <name>my-list1 - A</name>
+                    <my-leaf11>I am leaf11-0</my-leaf11>
+                    <my-leaf12>I am leaf12-1</my-leaf12>
+                </my-list1>
+                <my-list1>
+                    <name>my-list1 - B</name>
+                    <my-leaf11>I am leaf11-0</my-leaf11>
+                    <my-leaf12>I am leaf12-1</my-leaf12>
+                </my-list1>
+            </patch-cont>
+        </value>
+    </edit>
+    <edit>
+        <edit-id>edit2</edit-id>
+        <operation>replace</operation>
+        <target>/</target>
+        <value>
+            <patch-cont xmlns="instance:identifier:patch:module">
+                <my-list1>
+                    <name>my-list1 - Replacing</name>
+                    <my-leaf11>I am leaf11-0</my-leaf11>
+                    <my-leaf12>I am leaf12-1</my-leaf12>
+                </my-list1>
+            </patch-cont>
+        </value>
+    </edit>
+</yang-patch>
\ No newline at end of file