Support for patch command 90/65690/5
authorBalaji Varadaraju <bvaradar@brocade.com>
Sat, 18 Nov 2017 02:50:10 +0000 (20:50 -0600)
committerJakub Toth <jakub.toth@pantheon.tech>
Thu, 30 Nov 2017 13:14:49 +0000 (13:14 +0000)
The internal JSON libary did not have support for HTTP PATCH command.
This patch provides the support for the PATCH command. Support is
provided in both deprecated and new RESTCONF libraries.

Change-Id: I2bb81fe46bab5ec49fac3bff156e04f9a1b7c227
Signed-off-by: Balaji Varadaraju <bvaradar@brocade.com>
restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/api/JSONRestconfService.java
restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/JSONRestconfServiceImpl.java
restconf/restconf-nb-bierman02/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/JSONRestconfServiceImplTest.java
restconf/restconf-nb-bierman02/src/test/resources/full-versions/testCont1DataPatch.json [new file with mode: 0644]
restconf/restconf-nb-bierman02/src/test/resources/parts/ietf-interfaces_interfaces_patch.json [new file with mode: 0644]
restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/services/api/JSONRestconfService.java
restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/JSONRestconfServiceRfc8040Impl.java
restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/JSONRestconfServiceRfc8040ImplTest.java
restconf/restconf-nb-rfc8040/src/test/resources/full-versions/testCont1DataPatch.json [new file with mode: 0644]
restconf/restconf-nb-rfc8040/src/test/resources/parts/ietf-interfaces_interfaces_patch.json [new file with mode: 0644]

index df3e3f1afbece575ad37d504c7c5665e9c5aeb8d..5e080da064ff6ebfb08a7974badd63f49e596698 100644 (file)
@@ -73,4 +73,15 @@ public interface JSONRestconfService {
      * @throws OperationFailedException if the request fails.
      */
     Optional<String> invokeRpc(@Nonnull String uriPath, Optional<String> input) throws OperationFailedException;
+
+    /**
+     * Issues a restconf PATCH request to the configuration data store.
+     *
+     * @param uriPath the yang instance identifier path, eg "opendaylight-inventory:nodes/node/device-id".
+     *       To specify the root, use {@link ROOT_PATH}.
+     * @param payload the payload data in JSON format.
+     * @return an Optional containing the patch response data in JSON format.
+     * @throws OperationFailedException if the request fails.
+     */
+    Optional<String> patch(@Nonnull String uriPath, @Nonnull String payload) throws OperationFailedException;
 }
index 3b5ef86b374dfe12efc6a135ac62ca303e5c83f5..ec7b33dc09bf8e6f12e0fd296969282ee6e5b4e4 100644 (file)
@@ -19,12 +19,16 @@ import java.util.List;
 import javax.ws.rs.core.MediaType;
 import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType;
 import org.opendaylight.netconf.sal.rest.impl.JsonNormalizedNodeBodyReader;
+import org.opendaylight.netconf.sal.rest.impl.JsonToPatchBodyReader;
 import org.opendaylight.netconf.sal.rest.impl.NormalizedNodeJsonBodyWriter;
+import org.opendaylight.netconf.sal.rest.impl.PatchJsonBodyWriter;
 import org.opendaylight.netconf.sal.restconf.api.JSONRestconfService;
 import org.opendaylight.restconf.common.context.NormalizedNodeContext;
 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
 import org.opendaylight.restconf.common.errors.RestconfError;
 import org.opendaylight.restconf.common.errors.RestconfError.ErrorTag;
+import org.opendaylight.restconf.common.patch.PatchContext;
+import org.opendaylight.restconf.common.patch.PatchStatusContext;
 import org.opendaylight.restconf.common.util.SimpleUriInfo;
 import org.opendaylight.yangtools.yang.common.OperationFailedException;
 import org.opendaylight.yangtools.yang.common.RpcError;
@@ -165,10 +169,46 @@ public class JSONRestconfServiceImpl implements JSONRestconfService, AutoCloseab
         return Optional.fromNullable(output);
     }
 
+    @SuppressWarnings("checkstyle:IllegalCatch")
+    @Override
+    public Optional<String> patch(final String uriPath, final String payload)
+            throws OperationFailedException {
+
+        String output = null;
+        Preconditions.checkNotNull(payload, "payload can't be null");
+
+        LOG.debug("patch: uriPath: {}, payload: {}", uriPath, payload);
+
+        final InputStream entityStream = new ByteArrayInputStream(payload.getBytes(StandardCharsets.UTF_8));
+
+        JsonToPatchBodyReader jsonToPatchBodyReader = new JsonToPatchBodyReader();
+        final PatchContext context = jsonToPatchBodyReader.readFrom(uriPath, entityStream);
+
+        LOG.debug("Parsed YangInstanceIdentifier: {}", context.getInstanceIdentifierContext().getInstanceIdentifier());
+        LOG.debug("Parsed NormalizedNode: {}", context.getData());
+
+        try {
+            PatchStatusContext patchStatusContext = RestconfImpl.getInstance()
+                .patchConfigurationData(context, new SimpleUriInfo(uriPath));
+            output = toJson(patchStatusContext);
+        } catch (final Exception e) {
+            propagateExceptionAs(uriPath, e, "PATCH");
+        }
+        return Optional.fromNullable(output);
+    }
+
     @Override
     public void close() {
     }
 
+    private  String toJson(final PatchStatusContext patchStatusContext) throws IOException {
+        final PatchJsonBodyWriter writer = new PatchJsonBodyWriter();
+        final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+        writer.writeTo(patchStatusContext, PatchStatusContext.class, null, EMPTY_ANNOTATIONS,
+                MediaType.APPLICATION_JSON_TYPE, null, outputStream);
+        return outputStream.toString(StandardCharsets.UTF_8.name());
+    }
+
     private static String toJson(final NormalizedNodeContext readData) throws IOException {
         final NormalizedNodeJsonBodyWriter writer = new NormalizedNodeJsonBodyWriter();
         final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
index 89d5412ef55d5e03f0d804d7bcfeeb658b657d08..31807c84e8a8c3828706c6839e10989c75c24441 100644 (file)
@@ -29,6 +29,7 @@ import com.google.common.util.concurrent.Futures;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import javax.ws.rs.core.Response.Status;
@@ -50,6 +51,9 @@ import org.opendaylight.netconf.sal.restconf.impl.ControllerContext;
 import org.opendaylight.netconf.sal.restconf.impl.JSONRestconfServiceImpl;
 import org.opendaylight.netconf.sal.restconf.impl.PutResult;
 import org.opendaylight.netconf.sal.restconf.impl.RestconfImpl;
+import org.opendaylight.restconf.common.patch.PatchContext;
+import org.opendaylight.restconf.common.patch.PatchStatusContext;
+import org.opendaylight.restconf.common.patch.PatchStatusEntity;
 import org.opendaylight.yangtools.yang.common.OperationFailedException;
 import org.opendaylight.yangtools.yang.common.QName;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
@@ -291,6 +295,74 @@ public class JSONRestconfServiceImplTest {
         }
     }
 
+    @SuppressWarnings("rawtypes")
+    @Test
+    public void testPatch() throws Exception {
+        final PatchStatusContext result = mock(PatchStatusContext.class);
+        when(brokerFacade.patchConfigurationDataWithinTransaction(notNull(PatchContext.class)))
+            .thenReturn(result);
+
+        List<PatchStatusEntity> patchSTatus = new ArrayList<>();
+
+        PatchStatusEntity entity = new PatchStatusEntity("edit1", true, null);
+
+        patchSTatus.add(entity);
+
+        when(result.getEditCollection())
+                .thenReturn(patchSTatus);
+        when(result.getGlobalErrors()).thenReturn(new ArrayList<>());
+        when(result.getPatchId()).thenReturn("1");
+        final String uriPath = "ietf-interfaces:interfaces/interface/eth0";
+        final String payload = loadData("/parts/ietf-interfaces_interfaces_patch.json");
+        final Optional<String> patchResult = this.service.patch(uriPath, payload);
+
+        assertTrue(patchResult.get().contains("\"ok\":[null]"));
+    }
+
+    @SuppressWarnings("rawtypes")
+    @Test
+    public void testPatchBehindMountPoint() throws Exception {
+        final DOMMountPoint mockMountPoint = setupTestMountPoint();
+        final PatchStatusContext result = mock(PatchStatusContext.class);
+        when(brokerFacade.patchConfigurationDataWithinTransaction(notNull(PatchContext.class)))
+            .thenReturn(result);
+
+        List<PatchStatusEntity> patchSTatus = new ArrayList<>();
+
+        PatchStatusEntity entity = new PatchStatusEntity("edit1", true, null);
+
+        patchSTatus.add(entity);
+
+        when(result.getEditCollection())
+                .thenReturn(patchSTatus);
+        when(result.getGlobalErrors()).thenReturn(new ArrayList<>());
+        when(result.getPatchId()).thenReturn("1");
+
+        final String uriPath = "ietf-interfaces:interfaces/yang-ext:mount/test-module:cont/cont1";
+        final String payload = loadData("/full-versions/testCont1DataPatch.json");
+
+        final Optional<String> patchResult = this.service.patch(uriPath, payload);
+
+        assertTrue(patchResult.get().contains("\"ok\":[null]"));
+    }
+
+    @Test(expected = OperationFailedException.class)
+    @SuppressWarnings("checkstyle:IllegalThrows")
+    public void testPatchFailure() throws Throwable {
+        final PatchStatusContext result = mock(PatchStatusContext.class);
+        when(brokerFacade.patchConfigurationDataWithinTransaction(notNull(PatchContext.class)))
+            .thenThrow(new TransactionCommitFailedException("Transaction failed"));
+
+        final String uriPath = "ietf-interfaces:interfaces/interface/eth0";
+        final String payload = loadData("/parts/ietf-interfaces_interfaces_patch.json");
+
+        final Optional<String> patchResult = this.service.patch(uriPath, payload);
+
+        assertTrue("Patch output is not null", patchResult.isPresent());
+        String patch = patchResult.get();
+        assertTrue(patch.contains("TransactionCommitFailedException"));
+    }
+
     @Test
     public void testDelete() throws Exception {
         doReturn(Futures.immediateCheckedFuture(null)).when(brokerFacade)
diff --git a/restconf/restconf-nb-bierman02/src/test/resources/full-versions/testCont1DataPatch.json b/restconf/restconf-nb-bierman02/src/test/resources/full-versions/testCont1DataPatch.json
new file mode 100644 (file)
index 0000000..a8d6799
--- /dev/null
@@ -0,0 +1,20 @@
+{
+  "ietf-restconf:yang-patch" : {
+    "patch-id" : "0",
+    "edit" : [
+      {
+        "edit-id" : "edit1",
+        "operation" : "create",
+        "target" : "",
+        "value" :
+        {
+          "cont1":
+          {
+            "lf11": "lf11 data",
+            "lf12": "lf12 data"
+          }
+        }
+       }
+     ]
+  }
+}
\ No newline at end of file
diff --git a/restconf/restconf-nb-bierman02/src/test/resources/parts/ietf-interfaces_interfaces_patch.json b/restconf/restconf-nb-bierman02/src/test/resources/parts/ietf-interfaces_interfaces_patch.json
new file mode 100644 (file)
index 0000000..26b8f8d
--- /dev/null
@@ -0,0 +1,22 @@
+{
+  "ietf-restconf:yang-patch" : {
+    "patch-id" : "0",
+    "edit" : [
+      {
+        "edit-id" : "edit1",
+        "operation" : "create",
+        "target" : "",
+        "value" : {
+         "interface":[
+         {
+            "name":"eth0",
+            "type":"ethernetCsmacd",
+            "enabled":"false",
+            "description": "some interface"
+         }
+        ]
+      }
+     }
+    ]
+  }
+}
\ No newline at end of file
index 39fceed50543773fb6aa6672219278e006e11590..b34afc2409f7f21d54aaa3a3279427ae8ea8e74c 100644 (file)
@@ -73,4 +73,15 @@ public interface JSONRestconfService {
      * @throws OperationFailedException if the request fails.
      */
     Optional<String> invokeRpc(@Nonnull String uriPath, Optional<String> input) throws OperationFailedException;
+
+    /**
+     * Issues a restconf PATCH request to the configuration data store.
+     *
+     * @param uriPath the yang instance identifier path, eg "opendaylight-inventory:nodes/node/device-id".
+     *       To specify the root, use {@link ROOT_PATH}.
+     * @param payload the payload data in JSON format.
+     * @return an Optional containing the patch response data in JSON format.
+     * @throws OperationFailedException if the request fails.
+     */
+    Optional<String> patch(@Nonnull String uriPath, @Nonnull String payload) throws OperationFailedException;
 }
index baad47fcbe41251690ee005286c649c140e6c6e4..8f80fb3bbafc1bd7e7f3a70e147abdd57e9071d9 100644 (file)
@@ -26,12 +26,16 @@ import org.opendaylight.restconf.common.context.NormalizedNodeContext;
 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
 import org.opendaylight.restconf.common.errors.RestconfError;
 import org.opendaylight.restconf.common.errors.RestconfError.ErrorTag;
+import org.opendaylight.restconf.common.patch.PatchContext;
+import org.opendaylight.restconf.common.patch.PatchStatusContext;
 import org.opendaylight.restconf.common.util.MultivaluedHashMap;
 import org.opendaylight.restconf.common.util.SimpleUriInfo;
 import org.opendaylight.restconf.nb.rfc8040.handlers.DOMMountPointServiceHandler;
 import org.opendaylight.restconf.nb.rfc8040.handlers.SchemaContextHandler;
 import org.opendaylight.restconf.nb.rfc8040.jersey.providers.JsonNormalizedNodeBodyReader;
 import org.opendaylight.restconf.nb.rfc8040.jersey.providers.NormalizedNodeJsonBodyWriter;
+import org.opendaylight.restconf.nb.rfc8040.jersey.providers.patch.JsonToPatchBodyReader;
+import org.opendaylight.restconf.nb.rfc8040.jersey.providers.patch.PatchJsonBodyWriter;
 import org.opendaylight.restconf.nb.rfc8040.rests.services.api.JSONRestconfService;
 import org.opendaylight.restconf.nb.rfc8040.rests.services.api.TransactionServicesWrapper;
 import org.opendaylight.restconf.nb.rfc8040.rests.utils.RestconfDataServiceConstant;
@@ -174,6 +178,33 @@ public class JSONRestconfServiceRfc8040Impl implements JSONRestconfService, Auto
         return Optional.fromNullable(output);
     }
 
+    @SuppressWarnings("checkstyle:IllegalCatch")
+    @Override
+    public Optional<String> patch(final String uriPath, final String payload)
+            throws OperationFailedException {
+
+        String output = null;
+        Preconditions.checkNotNull(payload, "payload can't be null");
+
+        LOG.debug("patch: uriPath: {}, payload: {}", uriPath, payload);
+
+        final InputStream entityStream = new ByteArrayInputStream(payload.getBytes(StandardCharsets.UTF_8));
+
+        JsonToPatchBodyReader jsonToPatchBodyReader = new JsonToPatchBodyReader();
+        final PatchContext context = jsonToPatchBodyReader.readFrom(uriPath, entityStream);
+
+        LOG.debug("Parsed YangInstanceIdentifier: {}", context.getInstanceIdentifierContext().getInstanceIdentifier());
+        LOG.debug("Parsed NormalizedNode: {}", context.getData());
+
+        try {
+            PatchStatusContext patchStatusContext = services.patchData(context, new SimpleUriInfo(uriPath));
+            output = toJson(patchStatusContext);
+        } catch (final Exception e) {
+            propagateExceptionAs(uriPath, e, "PATCH");
+        }
+        return Optional.fromNullable(output);
+    }
+
     @Override
     public void close() {
     }
@@ -197,6 +228,14 @@ public class JSONRestconfServiceRfc8040Impl implements JSONRestconfService, Auto
         }
     }
 
+    private  String toJson(final PatchStatusContext patchStatusContext) throws IOException {
+        final PatchJsonBodyWriter writer = new PatchJsonBodyWriter();
+        final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+        writer.writeTo(patchStatusContext, PatchStatusContext.class, null, EMPTY_ANNOTATIONS,
+                MediaType.APPLICATION_JSON_TYPE, null, outputStream);
+        return outputStream.toString(StandardCharsets.UTF_8.name());
+    }
+
     private static String toJson(final NormalizedNodeContext readData) throws IOException {
         final NormalizedNodeJsonBodyWriter writer = new NormalizedNodeJsonBodyWriter();
         final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
index 4909e0ffbb2ab49636013fd451aecdce2433d8f9..2ed12a543fc2f88388af74825227debcc86a5f11 100644 (file)
@@ -337,6 +337,78 @@ public class JSONRestconfServiceRfc8040ImplTest {
         }
     }
 
+    @SuppressWarnings("rawtypes")
+    @Test
+    public void testPatch() throws Exception {
+        final String uriPath = "ietf-interfaces:interfaces/interface=eth0";
+        final String payload = loadData("/parts/ietf-interfaces_interfaces_patch.json");
+
+        final Optional<String> patchResult = this.service.patch(uriPath, payload);
+
+        final ArgumentCaptor<YangInstanceIdentifier> capturedPath =
+                ArgumentCaptor.forClass(YangInstanceIdentifier.class);
+        final ArgumentCaptor<NormalizedNode> capturedNode = ArgumentCaptor.forClass(NormalizedNode.class);
+
+        verify(mockReadWriteTx).put(eq(LogicalDatastoreType.CONFIGURATION), capturedPath.capture(),
+                capturedNode.capture());
+
+        verifyPath(capturedPath.getValue(), INTERFACES_QNAME, INTERFACE_QNAME,
+                new Object[]{INTERFACE_QNAME, NAME_QNAME, "eth0"});
+
+        assertTrue("Expected MapEntryNode. Actual " + capturedNode.getValue().getClass(),
+                capturedNode.getValue() instanceof MapEntryNode);
+        final MapEntryNode actualNode = (MapEntryNode) capturedNode.getValue();
+        assertEquals("MapEntryNode node type", INTERFACE_QNAME, actualNode.getNodeType());
+        verifyLeafNode(actualNode, NAME_QNAME, "eth0");
+        verifyLeafNode(actualNode, TYPE_QNAME, "ethernetCsmacd");
+        verifyLeafNode(actualNode, ENABLED_QNAME, Boolean.FALSE);
+        verifyLeafNode(actualNode, DESC_QNAME, "some interface");
+        assertTrue(patchResult.get().contains("\"ok\":[null]"));
+    }
+
+    @SuppressWarnings("rawtypes")
+    @Test
+    public void testPatchBehindMountPoint() throws Exception {
+        setupTestMountPoint();
+
+        final String uriPath = "ietf-interfaces:interfaces/yang-ext:mount/test-module:cont/cont1";
+        final String payload = loadData("/full-versions/testCont1DataPatch.json");
+
+        final Optional<String> patchResult = this.service.patch(uriPath, payload);
+
+        final ArgumentCaptor<YangInstanceIdentifier> capturedPath =
+                ArgumentCaptor.forClass(YangInstanceIdentifier.class);
+        final ArgumentCaptor<NormalizedNode> capturedNode = ArgumentCaptor.forClass(NormalizedNode.class);
+
+        verify(mockReadWriteTx).put(eq(LogicalDatastoreType.CONFIGURATION), capturedPath.capture(),
+                capturedNode.capture());
+
+        verifyPath(capturedPath.getValue(), TEST_CONT_QNAME, TEST_CONT1_QNAME);
+
+        assertTrue("Expected ContainerNode", capturedNode.getValue() instanceof ContainerNode);
+        final ContainerNode actualNode = (ContainerNode) capturedNode.getValue();
+        assertEquals("ContainerNode node type", TEST_CONT1_QNAME, actualNode.getNodeType());
+        verifyLeafNode(actualNode, TEST_LF11_QNAME, "lf11 data");
+        verifyLeafNode(actualNode, TEST_LF12_QNAME, "lf12 data");
+        assertTrue(patchResult.get().contains("\"ok\":[null]"));
+    }
+
+    @Test
+    @SuppressWarnings("checkstyle:IllegalThrows")
+    public void testPatchFailure() throws Throwable {
+        doReturn(Futures.immediateFailedCheckedFuture(new TransactionCommitFailedException("mock")))
+                .when(mockReadWriteTx).submit();
+
+        final String uriPath = "ietf-interfaces:interfaces/interface=eth0";
+
+        final String payload = loadData("/parts/ietf-interfaces_interfaces_patch.json");
+
+        final Optional<String> patchResult = this.service.patch(uriPath, payload);
+        assertTrue("Patch output is not null", patchResult.isPresent());
+        String patch = patchResult.get();
+        assertTrue(patch.contains("TransactionCommitFailedException"));
+    }
+
     @Test
     public void testDelete() throws Exception {
         doReturn(Futures.immediateCheckedFuture(Boolean.TRUE)).when(mockReadWriteTx).exists(
diff --git a/restconf/restconf-nb-rfc8040/src/test/resources/full-versions/testCont1DataPatch.json b/restconf/restconf-nb-rfc8040/src/test/resources/full-versions/testCont1DataPatch.json
new file mode 100644 (file)
index 0000000..a8d6799
--- /dev/null
@@ -0,0 +1,20 @@
+{
+  "ietf-restconf:yang-patch" : {
+    "patch-id" : "0",
+    "edit" : [
+      {
+        "edit-id" : "edit1",
+        "operation" : "create",
+        "target" : "",
+        "value" :
+        {
+          "cont1":
+          {
+            "lf11": "lf11 data",
+            "lf12": "lf12 data"
+          }
+        }
+       }
+     ]
+  }
+}
\ No newline at end of file
diff --git a/restconf/restconf-nb-rfc8040/src/test/resources/parts/ietf-interfaces_interfaces_patch.json b/restconf/restconf-nb-rfc8040/src/test/resources/parts/ietf-interfaces_interfaces_patch.json
new file mode 100644 (file)
index 0000000..dfafcf4
--- /dev/null
@@ -0,0 +1,22 @@
+{
+  "ietf-restconf:yang-patch" : {
+    "patch-id" : "0",
+    "edit" : [
+      {
+        "edit-id" : "edit1",
+        "operation" : "create",
+        "target" : "",
+        "value" : {
+         "interface":[
+         {
+            "name":"eth0",
+            "type":"ethernetCsmacd",
+            "enabled":false,
+            "description": "some interface"
+         }
+        ]
+      }
+     }
+    ]
+  }
+}
\ No newline at end of file