Bug 5509 - HTTP Patch in Restconf doesn't support general absolute or relative target... 95/38195/44
authorIvan Hrasko <ivan.hrasko@pantheon.tech>
Thu, 28 Apr 2016 14:23:53 +0000 (16:23 +0200)
committerIvan Hrasko <ihrasko@cisco.com>
Mon, 18 Jul 2016 13:54:43 +0000 (15:54 +0200)
- yang patch for xml media now supports relative or absolute xpaths
- only create, merge, replace and insert operations require value to be present
- if value is not present with create, merge, replace or insert operation
then error 400 is returned
- if value is used with other operations then error 400 is returned
- deleting sublist values instead of full parent
- support for multiple values
- added unit tests

Change-Id: I9828ceb7368760642b22b3a336884b949f0edfcc
Signed-off-by: Ivan Hrasko <ivan.hrasko@pantheon.tech>
opendaylight/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/rest/impl/StringModuleInstanceIdentifierCodec.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/AbstractBodyReaderTest.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/xml/xmlPATCHdataAbsoluteTargetPath.xml [new file with mode: 0644]
opendaylight/restconf/sal-rest-connector/src/test/resources/instanceidentifier/xml/xmlPATCHdataValueMissing.xml [new file with mode: 0644]
opendaylight/restconf/sal-rest-connector/src/test/resources/instanceidentifier/xml/xmlPATCHdataValueNotSupported.xml [new file with mode: 0644]

index c62f032ae184245a3b6e34a2d199cf23cb47b939..a5067718ab21dc88033d599542ce2e36b52cc8f7 100644 (file)
@@ -21,15 +21,28 @@ final class StringModuleInstanceIdentifierCodec extends AbstractModuleStringInst
 
     private final DataSchemaContextTree dataContextTree;
     private final SchemaContext context;
+    private final String defaultPrefix;
 
     StringModuleInstanceIdentifierCodec(SchemaContext context) {
         this.context = Preconditions.checkNotNull(context);
         this.dataContextTree = DataSchemaContextTree.from(context);
+        this.defaultPrefix = "";
     }
 
+    StringModuleInstanceIdentifierCodec(SchemaContext context, @Nonnull String defaultPrefix) {
+        this.context = Preconditions.checkNotNull(context);
+        this.dataContextTree = DataSchemaContextTree.from(context);
+        this.defaultPrefix = defaultPrefix;
+    }
+
+
     @Override
     protected Module moduleForPrefix(@Nonnull String prefix) {
-        return context.findModuleByName(prefix, null);
+        if (prefix.isEmpty() && !this.defaultPrefix.isEmpty()) {
+            return context.findModuleByName(this.defaultPrefix, null);
+        } else {
+            return context.findModuleByName(prefix, null);
+        }
     }
 
     @Nonnull
index fe63f3137338e0d6a8ca1b0d85c1bc919178838b..74a6cc81e55ee57231e21ff56226ee7a4628ca13 100644 (file)
@@ -8,14 +8,17 @@
 
 package org.opendaylight.netconf.sal.rest.impl;
 
+import com.google.common.base.Splitter;
 import com.google.common.collect.ImmutableList;
 import java.io.IOException;
 import java.io.InputStream;
 import java.lang.annotation.Annotation;
 import java.lang.reflect.Type;
+import java.net.URI;
 import java.util.ArrayList;
-import java.util.Collections;
+import java.util.Iterator;
 import java.util.List;
+import javax.annotation.Nonnull;
 import javax.ws.rs.Consumes;
 import javax.ws.rs.WebApplicationException;
 import javax.ws.rs.core.MediaType;
@@ -25,30 +28,33 @@ import javax.ws.rs.ext.Provider;
 import javax.xml.parsers.DocumentBuilder;
 import javax.xml.parsers.DocumentBuilderFactory;
 import javax.xml.parsers.ParserConfigurationException;
-import org.opendaylight.netconf.sal.restconf.impl.PATCHEntity;
-import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
-import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
-import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
-import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
-import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
-import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
-import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.w3c.dom.Document;
-import org.w3c.dom.Element;
-import org.w3c.dom.NodeList;
 import org.opendaylight.netconf.sal.rest.api.Draft02.MediaTypes;
 import org.opendaylight.netconf.sal.rest.api.RestconfService;
 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.common.QName;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
+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;
+import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.Module;
+import org.opendaylight.yangtools.yang.model.api.SchemaNode;
+import org.opendaylight.yangtools.yang.model.util.SchemaContextUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
 
 @Provider
 @Consumes({MediaTypes.PATCH + RestconfService.XML})
@@ -117,47 +123,170 @@ public class XmlToPATCHBodyReader extends AbstractIdentifierAwareJaxRsProvider i
         final List<PATCHEntity> resultCollection = new ArrayList<>();
         final String patchId = doc.getElementsByTagName("patch-id").item(0).getFirstChild().getNodeValue();
         final NodeList editNodes = doc.getElementsByTagName("edit");
-        final DataSchemaNode schemaNode = (DataSchemaNode) pathContext.getSchemaNode();
         final DomToNormalizedNodeParserFactory parserFactory =
                 DomToNormalizedNodeParserFactory.getInstance(XmlUtils.DEFAULT_XML_CODEC_PROVIDER,
                         pathContext.getSchemaContext());
 
         for (int i = 0; i < editNodes.getLength(); i++) {
-            Element element = (Element) editNodes.item(i);
+            DataSchemaNode schemaNode = (DataSchemaNode) pathContext.getSchemaNode();
+            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();
-            DataSchemaNode targetNode = ((DataNodeContainer)(pathContext.getSchemaNode())).getDataChildByName
-                    (target.replace("/", ""));
+            final String target = element.getElementsByTagName("target").item(0).getFirstChild().getNodeValue()
+                    .replaceFirst("/", "");
+            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) ?
+                    schemaNode.getQName().getNamespace().toString() : firstValueElement.getNamespaceURI();
+
+            // find module according to namespace
+            final Module module = pathContext.getSchemaContext().findModuleByNamespace(
+                    URI.create(namespace)).iterator().next();
+
+            // initialize codec + set default prefix derived from module name
+            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())));
+
+            // move schema node and get target node
+            schemaNode = (DataSchemaNode) SchemaContextUtil.findDataSchemaNode(pathContext.getSchemaContext(),
+                    codec.getDataContextTree().getChild(targetII).getDataSchemaNode().getPath());
+
+            final SchemaNode targetNode = SchemaContextUtil.findDataSchemaNode(pathContext.getSchemaContext(),
+                    codec.getDataContextTree().getChild(targetII).getDataSchemaNode().getPath().getParent());
+
             if (targetNode == null) {
                 LOG.debug("Target node {} not found in path {} ", target, pathContext.getSchemaNode());
                 throw new RestconfDocumentedException("Error parsing input", ErrorType.PROTOCOL,
                         ErrorTag.MALFORMED_MESSAGE);
             } else {
-                final YangInstanceIdentifier targetII = pathContext.getInstanceIdentifier().node(targetNode.getQName());
-                final NodeList valueNodes = element.getElementsByTagName("value").item(0).getChildNodes();
-                Element value = null;
-                for (int j = 0; j < valueNodes.getLength(); j++) {
-                    if (valueNodes.item(j) instanceof Element) {
-                        value = (Element) valueNodes.item(j);
-                        break;
+                if (PATCHEditOperation.isPatchOperationWithValue(operation)) {
+                    NormalizedNode<?, ?> parsed = null;
+                    if (schemaNode instanceof ContainerSchemaNode) {
+                        parsed = parserFactory.getContainerNodeParser().parse(values, (ContainerSchemaNode) schemaNode);
+                    } else if (schemaNode instanceof ListSchemaNode) {
+                        parsed = parserFactory.getMapNodeParser().parse(values, (ListSchemaNode) schemaNode);
                     }
-                }
-                NormalizedNode<?, ?> parsed = null;
-                if (schemaNode instanceof ContainerSchemaNode) {
-                    parsed = parserFactory.getContainerNodeParser().parse(Collections.singletonList(value),
-                            (ContainerSchemaNode) targetNode);
-                } else if (schemaNode instanceof ListSchemaNode) {
-                    NormalizedNode<?, ?> parsedValue = parserFactory.getMapEntryNodeParser().parse(Collections
-                            .singletonList(value), (ListSchemaNode) targetNode);
-                    parsed = ImmutableNodes.mapNodeBuilder().withNodeIdentifier(new NodeIdentifier
-                            (targetNode.getQName())).withChild((MapEntryNode) parsedValue).build();
-                }
 
-                resultCollection.add(new PATCHEntity(editId, operation, targetII, parsed));
+                    resultCollection.add(new PATCHEntity(editId, operation, targetII.getParent(), parsed));
+                } else {
+                    resultCollection.add(new PATCHEntity(editId, operation, targetII));
+                }
             }
         }
 
         return new PATCHContext(pathContext, ImmutableList.copyOf(resultCollection), patchId);
     }
+
+    /**
+     * Read value nodes
+     * @param element Element of current edit operation
+     * @param operation Name of current operation
+     * @return List of value elements
+     */
+    private List<Element> readValueNodes(@Nonnull final Element element, @Nonnull final String operation) {
+        final Node valueNode = element.getElementsByTagName("value").item(0);
+
+        if (PATCHEditOperation.isPatchOperationWithValue(operation) && valueNode == null) {
+            throw new RestconfDocumentedException("Error parsing input",
+                    ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
+        }
+
+        if (!PATCHEditOperation.isPatchOperationWithValue(operation) && valueNode != null) {
+            throw new RestconfDocumentedException("Error parsing input",
+                    ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
+        }
+
+        if (valueNode == null) {
+            return null;
+        }
+
+        final List<Element> result = new ArrayList<>();
+        final NodeList childNodes = valueNode.getChildNodes();
+        for (int i = 0; i < childNodes.getLength(); i++) {
+            if (childNodes.item(i) instanceof Element) {
+                result.add((Element) childNodes.item(i));
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * Prepare non-conditional XPath suitable for deserialization with {@link StringModuleInstanceIdentifierCodec}
+     * @param schemaNode Top schema node
+     * @param target Edit operation target
+     * @param value Element with value
+     * @param namespace Module namespace
+     * @param revision Module revision
+     * @return Non-conditional XPath
+     */
+    private String prepareNonCondXpath(@Nonnull final DataSchemaNode schemaNode, @Nonnull final String target,
+                                       @Nonnull final Element value, @Nonnull final String namespace,
+                                       @Nonnull String revision) {
+        final Iterator<String> args = Splitter.on("/").split(target.substring(target.indexOf(':') + 1)).iterator();
+
+        final StringBuilder nonCondXpath = new StringBuilder();
+        SchemaNode childNode = schemaNode;
+
+        while (args.hasNext()) {
+            final String s = args.next();
+            nonCondXpath.append("/");
+            nonCondXpath.append(s);
+            childNode = ((DataNodeContainer) childNode).getDataChildByName(QName.create(namespace, revision, s));
+
+            if (childNode instanceof ListSchemaNode && args.hasNext()) {
+                appendKeys(nonCondXpath, ((ListSchemaNode) childNode).getKeyDefinition().iterator(), args);
+            }
+        }
+
+        if (childNode instanceof ListSchemaNode && value != null) {
+            final Iterator<String> keyValues = readKeyValues(value,
+                    ((ListSchemaNode) childNode).getKeyDefinition().iterator());
+            appendKeys(nonCondXpath, ((ListSchemaNode) childNode).getKeyDefinition().iterator(), keyValues);
+        }
+
+        return nonCondXpath.toString();
+    }
+
+    /**
+     * Read value for every list key
+     * @param value Value element
+     * @param keys Iterator of list keys names
+     * @return Iterator of list keys values
+     */
+    private Iterator<String> readKeyValues(@Nonnull final Element value, @Nonnull final Iterator<QName> keys) {
+        final List<String> result = new ArrayList<>();
+
+        while (keys.hasNext()) {
+            result.add(value.getElementsByTagName(keys.next().getLocalName()).item(0).getFirstChild().getNodeValue());
+        }
+
+        return result.iterator();
+    }
+
+    /**
+     * Append key name - key value pairs for every list key to {@code nonCondXpath}
+     * @param nonCondXpath Builder for creating non-conditional XPath
+     * @param keyNames Iterator of list keys names
+     * @param keyValues Iterator of list keys values
+     */
+    private void appendKeys(@Nonnull final StringBuilder nonCondXpath, @Nonnull final Iterator<QName> keyNames,
+                            @Nonnull final Iterator<String> keyValues) {
+        while (keyNames.hasNext()) {
+            nonCondXpath.append("[");
+            nonCondXpath.append(keyNames.next().getLocalName());
+            nonCondXpath.append("=");
+            nonCondXpath.append("'");
+            nonCondXpath.append(keyValues.next());
+            nonCondXpath.append("'");
+            nonCondXpath.append("]");
+        }
+    }
 }
index 6a728dc40c3055ceac51328eb4d40a68a886f54f..bc37738464ebea89bcfbd547b917e11448b4ea0e 100644 (file)
@@ -69,20 +69,24 @@ public abstract class AbstractBodyReaderTest {
             final boolean isPost) throws NoSuchFieldException,
             SecurityException, IllegalArgumentException, IllegalAccessException {
         final UriInfo uriInfoMock = mock(UriInfo.class);
-        final MultivaluedMap<String, String> pathParm = new MultivaluedHashMap<>(
-                1);
-        pathParm.put(RestconfConstants.IDENTIFIER,
-                Collections.singletonList(identifier));
+        final MultivaluedMap<String, String> pathParm = new MultivaluedHashMap<>(1);
+
+        if (!identifier.isEmpty()) {
+            pathParm.put(RestconfConstants.IDENTIFIER, Collections.singletonList(identifier));
+        }
+
         when(uriInfoMock.getPathParameters()).thenReturn(pathParm);
         when(uriInfoMock.getPathParameters(false)).thenReturn(pathParm);
         when(uriInfoMock.getPathParameters(true)).thenReturn(pathParm);
         uriField.set(normalizedNodeProvider, uriInfoMock);
+
         final Request request = mock(Request.class);
         if (isPost) {
             when(request.getMethod()).thenReturn("POST");
         } else {
             when(request.getMethod()).thenReturn("PUT");
         }
+
         requestField.set(normalizedNodeProvider, request);
     }
 
index 9b98dd8cafe8c0322cd7655af43ca38b205090e2..8aa11e17368b65b8b03343d87a7add7988283b3c 100644 (file)
@@ -8,12 +8,16 @@
 
 package org.opendaylight.controller.sal.rest.impl.test.providers;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
 import java.io.InputStream;
 import javax.ws.rs.core.MediaType;
 import org.junit.BeforeClass;
 import org.junit.Test;
 import org.opendaylight.netconf.sal.rest.impl.XmlToPATCHBodyReader;
 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 TestXmlPATCHBodyReader extends AbstractBodyReaderTest {
@@ -47,4 +51,54 @@ public class TestXmlPATCHBodyReader extends AbstractBodyReaderTest {
                 .readFrom(null, null, null, mediaType, null, inputStream);
         checkPATCHContext(returnValue);
     }
+
+    /**
+     * Test trying to use PATCH create operation which requires value without value. Error code 400 should be returned.
+     */
+    @Test
+    public void moduleDataValueMissingNegativeTest() throws Exception {
+        final String uri = "instance-identifier-patch-module:patch-cont/my-list1/leaf1";
+        mockBodyReader(uri, xmlPATCHBodyReader, false);
+        final InputStream inputStream = TestXmlBodyReader.class
+                .getResourceAsStream("/instanceidentifier/xml/xmlPATCHdataValueMissing.xml");
+        try {
+            xmlPATCHBodyReader.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 (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. Error code 400 should be
+     * returned.
+     */
+    @Test
+    public void moduleDataNotValueNotSupportedNegativeTest() throws Exception {
+        final String uri = "instance-identifier-patch-module:patch-cont/my-list1/leaf1";
+        mockBodyReader(uri, xmlPATCHBodyReader, false);
+        final InputStream inputStream = TestXmlBodyReader.class
+                .getResourceAsStream("/instanceidentifier/xml/xmlPATCHdataValueNotSupported.xml");
+        try {
+            xmlPATCHBodyReader.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 (final RestconfDocumentedException e) {
+            assertEquals("Error code 400 expected", 400, e.getErrors().get(0).getErrorTag().getStatusCode());
+        }
+    }
+
+
+    /**
+     * Test of Yang PATCH with absolute target path.
+     */
+    @Test
+    public void moduleDataAbsoluteTargetPathTest() throws Exception {
+        final String uri = "";
+        mockBodyReader(uri, xmlPATCHBodyReader, false);
+        final InputStream inputStream = TestXmlBodyReader.class
+                .getResourceAsStream("/instanceidentifier/xml/xmlPATCHdataAbsoluteTargetPath.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/xml/xmlPATCHdataAbsoluteTargetPath.xml b/opendaylight/restconf/sal-rest-connector/src/test/resources/instanceidentifier/xml/xmlPATCHdataAbsoluteTargetPath.xml
new file mode 100644 (file)
index 0000000..6e84c47
--- /dev/null
@@ -0,0 +1,35 @@
+<!--
+  ~ 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 patch with absolute target path</comment>
+    <edit>
+        <edit-id>edit1</edit-id>
+        <operation>create</operation>
+        <target>/instance-identifier-patch-module:patch-cont/my-list1/leaf1/my-list2</target>
+        <value>
+            <my-list2 xmlns="instance:identifier:patch:module">
+                <name>my-leaf20</name>
+                <my-leaf21>I am leaf21-0</my-leaf21>
+                <my-leaf22>I am leaf22-0</my-leaf22>
+            </my-list2>
+        </value>
+    </edit>
+    <edit>
+        <edit-id>edit2</edit-id>
+        <operation>create</operation>
+        <target>/instance-identifier-patch-module:patch-cont/my-list1/leaf1/my-list2</target>
+        <value>
+            <my-list2 xmlns="instance:identifier:patch:module">
+                <name>my-leaf21</name>
+                <my-leaf21>I am leaf21-1</my-leaf21>
+                <my-leaf22>I am leaf22-1</my-leaf22>
+            </my-list2>
+        </value>
+    </edit>
+</yang-patch>
\ No newline at end of file
diff --git a/opendaylight/restconf/sal-rest-connector/src/test/resources/instanceidentifier/xml/xmlPATCHdataValueMissing.xml b/opendaylight/restconf/sal-rest-connector/src/test/resources/instanceidentifier/xml/xmlPATCHdataValueMissing.xml
new file mode 100644 (file)
index 0000000..eeec5ad
--- /dev/null
@@ -0,0 +1,16 @@
+<!--
+  ~ 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 patch with missing value node for create operation</comment>
+    <edit>
+        <edit-id>edit1</edit-id>
+        <operation>create</operation>
+        <target>/my-list2</target>
+    </edit>
+</yang-patch>
\ No newline at end of file
diff --git a/opendaylight/restconf/sal-rest-connector/src/test/resources/instanceidentifier/xml/xmlPATCHdataValueNotSupported.xml b/opendaylight/restconf/sal-rest-connector/src/test/resources/instanceidentifier/xml/xmlPATCHdataValueNotSupported.xml
new file mode 100644 (file)
index 0000000..8817094
--- /dev/null
@@ -0,0 +1,23 @@
+<!--
+  ~ 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 patch with not allowed value node for delete operation</comment>
+    <edit>
+        <edit-id>edit1</edit-id>
+        <operation>delete</operation>
+        <target>/my-list2/my-leaf21</target>
+        <value>
+            <my-list2 xmlns="instance:identifier:patch:module">
+                <name>my-leaf20</name>
+                <my-leaf21>I am leaf21-0</my-leaf21>
+                <my-leaf22>I am leaf22-0</my-leaf22>
+            </my-list2>
+        </value>
+    </edit>
+</yang-patch>
\ No newline at end of file