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>
* @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;
}
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;
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();
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;
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;
}
}
+ @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)
--- /dev/null
+{
+ "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
--- /dev/null
+{
+ "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
* @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;
}
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;
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() {
}
}
}
+ 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();
}
}
+ @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(
--- /dev/null
+{
+ "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
--- /dev/null
+{
+ "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