Bug 5730 - Delete subset of list items using PATCH? 22/38022/19
authorIvan Hrasko <ivan.hrasko@pantheon.tech>
Fri, 22 Apr 2016 14:04:01 +0000 (16:04 +0200)
committerIvan Hrasko <ivan.hrasko@pantheon.tech>
Mon, 23 May 2016 07:48:50 +0000 (09:48 +0200)
- fix: edit is not ignored when end of input is reached
- fix: only create, merge, replace and insert operations require value to be present
- fix: if value is not present with create, merge, replace or insert operation then error 400 is returned
- fix: if value is used with other operations error 400 is returned
- fix: deleting sublist values instead of full parent
- fix: order of patch edit fields is not important anymore (target, value)
- move class StringModuleInstanceIdentifierCodec to own file

Change-Id: I4c3f407858034b49795034da6c93b41f9d891b62
Signed-off-by: Ivan Hrasko <ivan.hrasko@pantheon.tech>
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/StringModuleInstanceIdentifierCodec.java [new file with mode: 0644]
opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/PATCHEditOperation.java
opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/PATCHEntity.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/resources/instanceidentifier/json/jsonPATCHdataCreateAndDelete.json [new file with mode: 0644]
opendaylight/restconf/sal-rest-connector/src/test/resources/instanceidentifier/json/jsonPATCHdataValueMissing.json [new file with mode: 0644]
opendaylight/restconf/sal-rest-connector/src/test/resources/instanceidentifier/json/jsonPATCHdataValueNotSupported.json [new file with mode: 0644]

index 721900ec55165f7c58ff6efd12c3b27e805599f9..dd5ced4a37cb6f8f09e29baeee5e7e3b27fd9b73 100644 (file)
@@ -8,19 +8,18 @@
 
 package org.opendaylight.netconf.sal.rest.impl;
 
-import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
 import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonToken;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
+import java.io.StringReader;
 import java.lang.annotation.Annotation;
 import java.lang.reflect.Type;
-import java.net.URI;
 import java.util.ArrayList;
 import java.util.List;
 import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
 import javax.ws.rs.Consumes;
 import javax.ws.rs.WebApplicationException;
 import javax.ws.rs.core.MediaType;
@@ -32,20 +31,18 @@ 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.schema.NormalizedNode;
 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
 import org.opendaylight.yangtools.yang.data.codec.gson.JsonParserStream;
 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNormalizedNodeStreamWriter;
 import org.opendaylight.yangtools.yang.data.impl.schema.NormalizedNodeResult;
 import org.opendaylight.yangtools.yang.data.impl.schema.ResultAlreadySetException;
-import org.opendaylight.yangtools.yang.data.util.AbstractModuleStringInstanceIdentifierCodec;
-import org.opendaylight.yangtools.yang.data.util.DataSchemaContextTree;
-import org.opendaylight.yangtools.yang.model.api.Module;
-import org.opendaylight.yangtools.yang.model.api.SchemaContext;
 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
 import org.opendaylight.yangtools.yang.model.util.SchemaContextUtil;
 import org.slf4j.Logger;
@@ -109,15 +106,10 @@ public class JsonToPATCHBodyReader extends AbstractIdentifierAwareJaxRsProvider
         return new PATCHContext(path, resultList, patchId);
     }
 
-    private List<PATCHEntity> read(final JsonReader in, InstanceIdentifierContext path) throws
-            IOException {
-
-        boolean inEdit = false;
-        boolean inValue = false;
-        String operation = null;
-        String target = null;
-        String editId = null;
+    private List<PATCHEntity> read(final JsonReader in, InstanceIdentifierContext path) throws IOException {
         List<PATCHEntity> resultCollection = new ArrayList<>();
+        StringModuleInstanceIdentifierCodec codec = new StringModuleInstanceIdentifierCodec(path.getSchemaContext());
+        JsonToPATCHBodyReader.PatchEdit edit = new JsonToPATCHBodyReader.PatchEdit();
 
         while (in.hasNext()) {
             switch (in.peek()) {
@@ -135,94 +127,298 @@ public class JsonToPATCHBodyReader extends AbstractIdentifierAwareJaxRsProvider
                     in.beginArray();
                     break;
                 case BEGIN_OBJECT:
-                    if (inEdit && operation != null & target != null & inValue) {
-                        StringModuleInstanceIdentifierCodec codec = new StringModuleInstanceIdentifierCodec(path
-                              .getSchemaContext());
-
-                        YangInstanceIdentifier targetII = codec.deserialize(codec.serialize(path
-                                .getInstanceIdentifier()) + target);
-                        SchemaNode targetSchemaNode = SchemaContextUtil.findDataSchemaNode(path.getSchemaContext(),
-                                codec.getDataContextTree().getChild(targetII).getDataSchemaNode().getPath()
-                                        .getParent());
-
-                        final NormalizedNodeResult resultHolder = new NormalizedNodeResult();
-                        final NormalizedNodeStreamWriter writer = ImmutableNormalizedNodeStreamWriter.from(resultHolder);
-
-                        //keep on parsing json from place where target points
-                        final JsonParserStream jsonParser = JsonParserStream.create(writer, path.getSchemaContext(),
-                                targetSchemaNode);
-                        jsonParser.parse(in);
-                        resultCollection.add(new PATCHEntity(editId, operation, targetII.getParent(), resultHolder.getResult()));
-                        inValue = false;
-
-                        operation = null;
-                        target = null;
-                        in.endObject();
-                    } else {
-                        in.beginObject();
-                    }
+                    in.beginObject();
                     break;
                 case END_DOCUMENT:
                     break;
                 case NAME:
-                    final String name = in.nextName();
-
-                    switch (name) {
-                        case "edit" : inEdit = true;
-                            break;
-                        case "operation" : operation = in.nextString();
-                            break;
-                        case "target" : target = in.nextString();
-                            break;
-                        case "value" : inValue = true;
-                            break;
-                        case "patch-id" : patchId = in.nextString();
-                            break;
-                        case "edit-id" : editId = in.nextString();
-                            break;
-                    }
+                    parseByName(in.nextName(), edit, in, path, codec, resultCollection);
                     break;
                 case END_OBJECT:
                     in.endObject();
                     break;
-            case END_ARRAY:
-                in.endArray();
-                break;
+                case END_ARRAY:
+                    in.endArray();
+                    break;
+
+                default:
+                    break;
+            }
+        }
+
+        return ImmutableList.copyOf(resultCollection);
+    }
+
+    /**
+     * Switch value of parsed JsonToken.NAME and read edit definition or patch id
+     * @param name value of token
+     * @param edit PatchEdit instance
+     * @param in JsonReader reader
+     * @param path InstanceIdentifierContext context
+     * @param codec StringModuleInstanceIdentifierCodec codec
+     * @param resultCollection collection of parsed edits
+     * @throws IOException
+     */
+    private void parseByName(@Nonnull final String name, @Nonnull final PatchEdit edit,
+                             @Nonnull final JsonReader in, @Nonnull final InstanceIdentifierContext path,
+                             @Nonnull final StringModuleInstanceIdentifierCodec codec,
+                             @Nonnull final List<PATCHEntity> resultCollection) throws IOException {
+        switch (name) {
+            case "edit" :
+                if (in.peek() == JsonToken.BEGIN_ARRAY) {
+                    in.beginArray();
+
+                    while (in.hasNext()) {
+                        readEditDefinition(edit, in, path, codec);
+                        resultCollection.add(prepareEditOperation(edit));
+                        edit.clear();
+                    }
+
+                    in.endArray();
+                } else {
+                    readEditDefinition(edit, in, path, codec);
+                    resultCollection.add(prepareEditOperation(edit));
+                    edit.clear();
+                }
 
+                break;
+            case "patch-id" :
+                this.patchId = in.nextString();
+                break;
             default:
                 break;
+        }
+    }
+
+    /**
+     * Read one patch edit object from Json input
+     * @param edit PatchEdit instance to be filled with read data
+     * @param in JsonReader reader
+     * @param path InstanceIdentifierContext path context
+     * @param codec StringModuleInstanceIdentifierCodec codec
+     * @throws IOException
+     */
+    private void readEditDefinition(@Nonnull final PatchEdit edit, @Nonnull final JsonReader in,
+                                    @Nonnull final InstanceIdentifierContext path,
+                                    @Nonnull final StringModuleInstanceIdentifierCodec codec) throws IOException {
+        StringBuffer value = new StringBuffer();
+        in.beginObject();
+
+        while (in.hasNext()) {
+            final String editDefinition = in.nextName();
+            switch (editDefinition) {
+                case "edit-id" :
+                    edit.setId(in.nextString());
+                    break;
+                case "operation" :
+                    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()));
+                    break;
+                case "value" :
+                    // save data defined in value node for next (later) processing, because target needs to be read
+                    // always first and there is no ordering in Json input
+                    readValueNode(value, in);
+                    break;
+                default:
+                    break;
             }
         }
 
-        return ImmutableList.copyOf(resultCollection);
+        in.endObject();
+
+        // read saved data to normalized node when target schema is already known
+        edit.setData(readEditData(new JsonReader(new StringReader(value.toString())), edit.getTargetSchemaNode(), path));
     }
 
-    private class StringModuleInstanceIdentifierCodec extends AbstractModuleStringInstanceIdentifierCodec {
+    /**
+     * Parse data defined in value node and saves it to buffer
+     * @param value Buffer to read value node
+     * @param in JsonReader reader
+     * @throws IOException
+     */
+    private void readValueNode(@Nonnull final StringBuffer value, @Nonnull final JsonReader in) throws IOException {
+        in.beginObject();
+        value.append("{");
+
+        value.append("\"" + in.nextName() + "\"" + ":");
+
+        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);
+        }
+
+        in.endObject();
+        value.append("}");
+    }
 
-        private final DataSchemaContextTree dataContextTree;
-        private final SchemaContext context;
+    /**
+     * Parse one value object of data and saves it to buffer
+     * @param value Buffer to read value object
+     * @param in JsonReader reader
+     * @throws IOException
+     */
+    private void readValueObject(@Nonnull final StringBuffer value, @Nonnull final JsonReader in) throws IOException {
+        in.beginObject();
+        value.append("{");
+
+        while (in.hasNext()) {
+            value.append("\"" + in.nextName() + "\"");
+            value.append(":");
+            value.append("\"" + in.nextString() + "\"");
+            if (in.peek() != JsonToken.END_OBJECT) {
+                value.append(",");
+            }
+        }
+
+        in.endObject();
+        value.append("}");
+    }
+
+    /**
+     * Read patch edit data defined in value node to NormalizedNode
+     * @param in reader JsonReader reader
+     * @return NormalizedNode representing data
+     */
+    private NormalizedNode readEditData(@Nonnull final JsonReader in, @Nonnull SchemaNode targetSchemaNode,
+                                        @Nonnull InstanceIdentifierContext path) {
+        final NormalizedNodeResult resultHolder = new NormalizedNodeResult();
+        final NormalizedNodeStreamWriter writer = ImmutableNormalizedNodeStreamWriter.from(resultHolder);
+        JsonParserStream.create(writer, path.getSchemaContext(), targetSchemaNode).parse(in);
+
+        return resultHolder.getResult();
+    }
+
+    /**
+     * Prepare PATCHEntity from PatchEdit instance when it satisfies conditions, otherwise throws exception
+     * @param edit Instance of PatchEdit
+     * @return PATCHEntity
+     */
+    private PATCHEntity prepareEditOperation(@Nonnull final PatchEdit edit) {
+        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());
+            } else {
+                return new PATCHEntity(edit.getId(), edit.getOperation(), edit.getTarget());
+            }
+        }
+
+        throw new RestconfDocumentedException("Error parsing input", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
+    }
+
+    /**
+     * Check if data is present when operation requires it and not present when operation data is not allowed
+     * @param operation Name of operation
+     * @param hasData Data in edit are present/not present
+     * @return true if data is present when operation requires it or if there are no data when operation does not
+     * allow it, false otherwise
+     */
+    private boolean checkDataPresence(@Nonnull final String operation, final boolean hasData) {
+        if (isPatchOperationWithValue(operation)) {
+            if (hasData) {
+                return true;
+            } else {
+                return false;
+            }
+        } else  {
+            if (!hasData) {
+                return true;
+            } else {
+                return false;
+            }
+        }
+    }
+
+    /**
+     * 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
+     */
+    private static final class PatchEdit {
+        private String id;
+        private String operation;
+        private YangInstanceIdentifier target;
+        private SchemaNode targetSchemaNode;
+        private NormalizedNode data;
+
+        public String getId() {
+            return id;
+        }
+
+        public void setId(String id) {
+            this.id = id;
+        }
+
+        public String getOperation() {
+            return operation;
+        }
+
+        public void setOperation(String operation) {
+            this.operation = operation;
+        }
+
+        public YangInstanceIdentifier getTarget() {
+            return target;
+        }
+
+        public void setTarget(YangInstanceIdentifier target) {
+            this.target = target;
+        }
+
+        public SchemaNode getTargetSchemaNode() {
+            return targetSchemaNode;
+        }
 
-        StringModuleInstanceIdentifierCodec(SchemaContext context) {
-            this.context = Preconditions.checkNotNull(context);
-            this.dataContextTree = DataSchemaContextTree.from(context);
+        public void setTargetSchemaNode(SchemaNode targetSchemaNode) {
+            this.targetSchemaNode = targetSchemaNode;
         }
 
-        @Override
-        protected Module moduleForPrefix(@Nonnull String prefix) {
-            return context.findModuleByName(prefix, null);
+        public NormalizedNode getData() {
+            return data;
         }
 
-        @Nonnull
-        @Override
-        protected DataSchemaContextTree getDataContextTree() {
-            return dataContextTree;
+        public void setData(NormalizedNode data) {
+            this.data = data;
         }
 
-        @Nullable
-        @Override
-        protected String prefixForNamespace(@Nonnull URI namespace) {
-            final Module module = context.findModuleByNamespaceAndRevision(namespace, null);
-            return module == null ? null : module.getName();
+        public void clear() {
+            id = null;
+            operation = null;
+            target = null;
+            targetSchemaNode = null;
+            data = null;
         }
     }
 }
diff --git a/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/rest/impl/StringModuleInstanceIdentifierCodec.java b/opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/rest/impl/StringModuleInstanceIdentifierCodec.java
new file mode 100644 (file)
index 0000000..c62f032
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * 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
+ */
+
+package org.opendaylight.netconf.sal.rest.impl;
+
+import com.google.common.base.Preconditions;
+import java.net.URI;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import org.opendaylight.yangtools.yang.data.util.AbstractModuleStringInstanceIdentifierCodec;
+import org.opendaylight.yangtools.yang.data.util.DataSchemaContextTree;
+import org.opendaylight.yangtools.yang.model.api.Module;
+import org.opendaylight.yangtools.yang.model.api.SchemaContext;
+
+final class StringModuleInstanceIdentifierCodec extends AbstractModuleStringInstanceIdentifierCodec {
+
+    private final DataSchemaContextTree dataContextTree;
+    private final SchemaContext context;
+
+    StringModuleInstanceIdentifierCodec(SchemaContext context) {
+        this.context = Preconditions.checkNotNull(context);
+        this.dataContextTree = DataSchemaContextTree.from(context);
+    }
+
+    @Override
+    protected Module moduleForPrefix(@Nonnull String prefix) {
+        return context.findModuleByName(prefix, null);
+    }
+
+    @Nonnull
+    @Override
+    protected DataSchemaContextTree getDataContextTree() {
+        return dataContextTree;
+    }
+
+    @Nullable
+    @Override
+    protected String prefixForNamespace(@Nonnull URI namespace) {
+        final Module module = context.findModuleByNamespaceAndRevision(namespace, null);
+        return module == null ? null : module.getName();
+    }
+}
\ No newline at end of file
index e0bdfd5893863848c2a991e0449f904b014a8d38..d7b4ebd1dd0af7abb4b06286050e297e33114b3e 100644 (file)
@@ -15,7 +15,7 @@ package org.opendaylight.netconf.sal.restconf.impl;
  * operations, but also includes some new operations.
  *
  */
-enum PATCHEditOperation {
+public enum PATCHEditOperation {
     CREATE,  //post
     DELETE,  //delete
     INSERT,  //post
index ae16edc9d46b63d244031d94c2c2103678c37cdd..b857c8f82e71b0ab699360a46c3ac5df509ec6c0 100644 (file)
@@ -19,6 +19,13 @@ public class PATCHEntity {
     private final YangInstanceIdentifier targetNode;
     private final NormalizedNode<?,?> node;
 
+    /**
+     * Constructor to create PATCHEntity for PATCH operations which require value leaf representing data to be present.
+     * @param editId Id of PATCH edit
+     * @param operation PATCH edit operation
+     * @param targetNode Target node for PATCH edit operation
+     * @param node Data defined by value leaf used by edit operation
+     */
     public PATCHEntity(final String editId, final String operation, final YangInstanceIdentifier targetNode, final
     NormalizedNode<?, ?> node) {
         this.editId = Preconditions.checkNotNull(editId);
@@ -27,6 +34,20 @@ public class PATCHEntity {
         this.node = Preconditions.checkNotNull(node);
     }
 
+    /**
+     * Constructor to create PATCHEntity for PATCH operations which do not allow value leaf representing data to be
+     * present. <code>node</code> is set to <code>null</code> meaning that data are not allowed for edit operation.
+     * @param editId Id of PATCH edit
+     * @param operation PATCH edit operation
+     * @param targetNode Target node for PATCH edit operation
+     */
+    public PATCHEntity(final String editId, final String operation, final YangInstanceIdentifier targetNode) {
+        this.editId = Preconditions.checkNotNull(editId);
+        this.operation = Preconditions.checkNotNull(operation);
+        this.targetNode = Preconditions.checkNotNull(targetNode);
+        this.node = null;
+    }
+
     public String getOperation() {
         return operation;
     }
index bcceb072c41e7240b07cb50153e671de9afd5915..35552cd2897260a235c0ffc6499c4676e372e05f 100644 (file)
@@ -9,6 +9,8 @@
 package org.opendaylight.controller.sal.rest.impl.test.providers;
 
 import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
 
 import java.io.InputStream;
 import javax.ws.rs.core.MediaType;
@@ -16,6 +18,7 @@ import org.junit.BeforeClass;
 import org.junit.Test;
 import org.opendaylight.netconf.sal.rest.impl.JsonToPATCHBodyReader;
 import org.opendaylight.netconf.sal.restconf.impl.PATCHContext;
+import org.opendaylight.netconf.sal.restconf.impl.RestconfDocumentedException;
 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
 
 public class TestJsonPATCHBodyReader extends AbstractBodyReaderTest {
@@ -51,4 +54,49 @@ public class TestJsonPATCHBodyReader extends AbstractBodyReaderTest {
                 .readFrom(null, null, null, mediaType, null, inputStream);
         checkPATCHContext(returnValue);
     }
+
+    @Test
+    public void modulePATCHCreateAndDeleteTest() throws Exception {
+        final String uri = "instance-identifier-patch-module:patch-cont/my-list1/leaf1";
+        mockBodyReader(uri, jsonPATCHBodyReader, false);
+
+        final InputStream inputStream = TestJsonBodyReader.class
+                .getResourceAsStream("/instanceidentifier/json/jsonPATCHdataCreateAndDelete.json");
+
+        final PATCHContext returnValue = jsonPATCHBodyReader
+                .readFrom(null, null, null, mediaType, null, inputStream);
+        checkPATCHContext(returnValue);
+    }
+
+    @Test
+    public void modulePATCHValueMissingTest() throws Exception {
+        final String uri = "instance-identifier-patch-module:patch-cont/my-list1/leaf1";
+        mockBodyReader(uri, jsonPATCHBodyReader, false);
+
+        final InputStream inputStream = TestJsonBodyReader.class
+                .getResourceAsStream("/instanceidentifier/json/jsonPATCHdataValueMissing.json");
+
+        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) {
+            assertEquals("Error code 400 expected", 400, e.getErrors().get(0).getErrorTag().getStatusCode());
+        }
+    }
+
+    @Test
+    public void modulePATCHValueNotSupportedTest() throws Exception {
+        final String uri = "instance-identifier-patch-module:patch-cont/my-list1/leaf1";
+        mockBodyReader(uri, jsonPATCHBodyReader, false);
+
+        final InputStream inputStream = TestJsonBodyReader.class
+                .getResourceAsStream("/instanceidentifier/json/jsonPATCHdataValueNotSupported.json");
+
+        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) {
+            assertEquals("Error code 400 expected", 400, e.getErrors().get(0).getErrorTag().getStatusCode());
+        }
+    }
 }
diff --git a/opendaylight/restconf/sal-rest-connector/src/test/resources/instanceidentifier/json/jsonPATCHdataCreateAndDelete.json b/opendaylight/restconf/sal-rest-connector/src/test/resources/instanceidentifier/json/jsonPATCHdataCreateAndDelete.json
new file mode 100644 (file)
index 0000000..4455038
--- /dev/null
@@ -0,0 +1,31 @@
+{
+  "ietf-yang-patch:yang-patch" : {
+    "patch-id" : "test-patch",
+    "comment" : "this is test patch",
+    "edit" : [
+      {
+        "edit-id": "edit1",
+        "value": {
+          "my-list2": [
+            {
+              "name": "my-leaf20",
+              "my-leaf21": "I am leaf20"
+            },
+            {
+              "name": "my-leaf21",
+              "my-leaf21": "I am leaf21-1",
+              "my-leaf22": "I am leaf21-2"
+            }
+          ]
+        },
+        "target": "/instance-identifier-patch-module:my-list2[instance-identifier-patch-module:name='my-leaf20']",
+        "operation": "create"
+      },
+      {
+        "edit-id": "edit2",
+        "operation": "delete",
+        "target": "/instance-identifier-patch-module:my-list2[instance-identifier-patch-module:name='my-leaf20']"
+      }
+    ]
+  }
+}
\ No newline at end of file
diff --git a/opendaylight/restconf/sal-rest-connector/src/test/resources/instanceidentifier/json/jsonPATCHdataValueMissing.json b/opendaylight/restconf/sal-rest-connector/src/test/resources/instanceidentifier/json/jsonPATCHdataValueMissing.json
new file mode 100644 (file)
index 0000000..eaf1b37
--- /dev/null
@@ -0,0 +1,13 @@
+{
+  "ietf-yang-patch:yang-patch" : {
+    "patch-id" : "test-patch",
+    "comment" : "this is test patch",
+    "edit" : [
+      {
+        "edit-id": "edit1",
+        "target": "/instance-identifier-patch-module:my-list2[instance-identifier-patch-module:name='my-leaf20']",
+        "operation": "create"
+      }
+    ]
+  }
+}
\ No newline at end of file
diff --git a/opendaylight/restconf/sal-rest-connector/src/test/resources/instanceidentifier/json/jsonPATCHdataValueNotSupported.json b/opendaylight/restconf/sal-rest-connector/src/test/resources/instanceidentifier/json/jsonPATCHdataValueNotSupported.json
new file mode 100644 (file)
index 0000000..1ad52fb
--- /dev/null
@@ -0,0 +1,20 @@
+{
+  "ietf-yang-patch:yang-patch" : {
+    "patch-id" : "test-patch",
+    "comment" : "this is test patch",
+    "edit" : [
+      {
+        "edit-id": "edit2",
+        "operation": "delete",
+        "target": "/instance-identifier-patch-module:my-list2[instance-identifier-patch-module:name='my-leaf20']",
+        "value": {
+          "my-list2": [
+            {
+              "name": "my-leaf20"
+            }
+          ]
+        }
+      }
+    ]
+  }
+}
\ No newline at end of file