YANG Patch is a quite simple request, easily defined by RestconfServer.
This allows us to rehost JAX-RS methods to RestconfImpl.
JIRA: NETCONF-773
Change-Id: I8532685a52f7b4989125f52c758c956faebe41ac
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
import org.opendaylight.restconf.common.errors.RestconfFuture;
import org.opendaylight.restconf.common.errors.SettableRestconfFuture;
+import org.opendaylight.restconf.common.patch.PatchContext;
+import org.opendaylight.restconf.common.patch.PatchStatusContext;
import org.opendaylight.restconf.nb.rfc8040.ReadDataParams;
import org.opendaylight.restconf.nb.rfc8040.databind.DatabindContext;
import org.opendaylight.restconf.nb.rfc8040.databind.DatabindProvider;
import org.opendaylight.restconf.nb.rfc8040.databind.OperationInputBody;
+import org.opendaylight.restconf.nb.rfc8040.databind.PatchBody;
import org.opendaylight.restconf.nb.rfc8040.databind.ResourceBody;
import org.opendaylight.restconf.nb.rfc8040.databind.jaxrs.QueryParams;
import org.opendaylight.restconf.nb.rfc8040.legacy.InstanceIdentifierContext;
return req.strategy().merge(req.path(), req.data());
}
+ @Override
+ public RestconfFuture<PatchStatusContext> dataPATCH(final PatchBody body) {
+ return dataPATCH(bindRequestRoot(), body);
+ }
+
+ @Override
+ public RestconfFuture<PatchStatusContext> dataPATCH(final String identifier, final PatchBody body) {
+ return dataPATCH(bindRequestPath(identifier), body);
+ }
+
+ private @NonNull RestconfFuture<PatchStatusContext> dataPATCH(final InstanceIdentifierContext reqPath,
+ final PatchBody body) {
+ final var modelContext = reqPath.getSchemaContext();
+ final PatchContext patch;
+ try {
+ patch = body.toPatchContext(modelContext, reqPath.getInstanceIdentifier());
+ } catch (IOException e) {
+ LOG.debug("Error parsing YANG Patch input", e);
+ return RestconfFuture.failed(new RestconfDocumentedException("Error parsing input: " + e.getMessage(),
+ ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE, e));
+ }
+ return getRestconfStrategy(modelContext, reqPath.getMountPoint()).patchData(patch);
+ }
+
// FIXME: should follow the same pattern as operationsPOST() does
RestconfFuture<DOMActionResult> dataInvokePOST(final InstanceIdentifierContext reqPath,
final OperationInputBody body) {
import static java.util.Objects.requireNonNull;
-import com.google.common.annotations.VisibleForTesting;
-import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.List;
import javax.ws.rs.Consumes;
import javax.ws.rs.Encoded;
-import javax.ws.rs.PATCH;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
-import javax.ws.rs.Produces;
import javax.ws.rs.container.AsyncResponse;
import javax.ws.rs.container.Suspended;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriInfo;
-import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.opendaylight.mdsal.dom.api.DOMActionResult;
import org.opendaylight.mdsal.dom.api.DOMMountPoint;
-import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
-import org.opendaylight.restconf.common.errors.RestconfError;
-import org.opendaylight.restconf.common.patch.PatchContext;
-import org.opendaylight.restconf.common.patch.PatchStatusContext;
import org.opendaylight.restconf.nb.rfc8040.MediaTypes;
import org.opendaylight.restconf.nb.rfc8040.databind.ChildBody;
import org.opendaylight.restconf.nb.rfc8040.databind.DatabindProvider;
import org.opendaylight.restconf.nb.rfc8040.databind.JsonChildBody;
import org.opendaylight.restconf.nb.rfc8040.databind.JsonOperationInputBody;
-import org.opendaylight.restconf.nb.rfc8040.databind.JsonPatchBody;
import org.opendaylight.restconf.nb.rfc8040.databind.JsonResourceBody;
import org.opendaylight.restconf.nb.rfc8040.databind.OperationInputBody;
-import org.opendaylight.restconf.nb.rfc8040.databind.PatchBody;
import org.opendaylight.restconf.nb.rfc8040.databind.ResourceBody;
import org.opendaylight.restconf.nb.rfc8040.databind.XmlChildBody;
import org.opendaylight.restconf.nb.rfc8040.databind.XmlOperationInputBody;
-import org.opendaylight.restconf.nb.rfc8040.databind.XmlPatchBody;
import org.opendaylight.restconf.nb.rfc8040.databind.XmlResourceBody;
import org.opendaylight.restconf.nb.rfc8040.databind.jaxrs.QueryParams;
-import org.opendaylight.restconf.nb.rfc8040.legacy.ErrorTags;
import org.opendaylight.restconf.nb.rfc8040.legacy.InstanceIdentifierContext;
import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload;
import org.opendaylight.restconf.nb.rfc8040.rests.transactions.RestconfStrategy.CreateOrReplaceResult;
import org.opendaylight.restconf.nb.rfc8040.utils.parser.IdentifierCodec;
import org.opendaylight.yangtools.yang.common.Empty;
-import org.opendaylight.yangtools.yang.common.ErrorTag;
-import org.opendaylight.yangtools.yang.common.ErrorType;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
import org.opendaylight.yangtools.yang.data.api.schema.MapNode;
import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
import org.opendaylight.yangtools.yang.model.api.SchemaContext;
import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.Inference;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
/**
* The "{+restconf}/data" subtree represents the datastore resource type, which is a collection of configuration data
*/
@Path("/")
public final class RestconfDataServiceImpl {
- private static final Logger LOG = LoggerFactory.getLogger(RestconfDataServiceImpl.class);
-
private final DatabindProvider databindProvider;
private final MdsalRestconfServer server;
return uriInfo.getBaseUriBuilder().path("data").path(IdentifierCodec.serialize(path, schemaContext)).build();
}
- /**
- * Ordered list of edits that are applied to the target datastore by the server, as defined in
- * <a href="https://www.rfc-editor.org/rfc/rfc8072#section-2">RFC8072, section 2</a>.
- *
- * @param identifier path to target
- * @param body YANG Patch body
- * @param ar {@link AsyncResponse} which needs to be completed with a {@link PatchStatusContext}
- */
- @PATCH
- @Path("/data/{identifier:.+}")
- @Consumes(MediaTypes.APPLICATION_YANG_PATCH_XML)
- @Produces({
- MediaTypes.APPLICATION_YANG_DATA_JSON,
- MediaTypes.APPLICATION_YANG_DATA_XML
- })
- public void yangPatchDataXML(@Encoded @PathParam("identifier") final String identifier, final InputStream body,
- @Suspended final AsyncResponse ar) {
- try (var xmlBody = new XmlPatchBody(body)) {
- yangPatchData(identifier, xmlBody, ar);
- }
- }
-
- /**
- * Ordered list of edits that are applied to the datastore by the server, as defined in
- * <a href="https://www.rfc-editor.org/rfc/rfc8072#section-2">RFC8072, section 2</a>.
- *
- * @param body YANG Patch body
- * @param ar {@link AsyncResponse} which needs to be completed with a {@link PatchStatusContext}
- */
- @PATCH
- @Path("/data")
- @Consumes(MediaTypes.APPLICATION_YANG_PATCH_XML)
- @Produces({
- MediaTypes.APPLICATION_YANG_DATA_JSON,
- MediaTypes.APPLICATION_YANG_DATA_XML
- })
- public void yangPatchDataXML(final InputStream body, @Suspended final AsyncResponse ar) {
- try (var xmlBody = new XmlPatchBody(body)) {
- yangPatchData(xmlBody, ar);
- }
- }
-
- /**
- * Ordered list of edits that are applied to the target datastore by the server, as defined in
- * <a href="https://www.rfc-editor.org/rfc/rfc8072#section-2">RFC8072, section 2</a>.
- *
- * @param identifier path to target
- * @param body YANG Patch body
- * @param ar {@link AsyncResponse} which needs to be completed with a {@link PatchStatusContext}
- */
- @PATCH
- @Path("/data/{identifier:.+}")
- @Consumes(MediaTypes.APPLICATION_YANG_PATCH_JSON)
- @Produces({
- MediaTypes.APPLICATION_YANG_DATA_JSON,
- MediaTypes.APPLICATION_YANG_DATA_XML
- })
- public void yangPatchDataJSON(@Encoded @PathParam("identifier") final String identifier,
- final InputStream body, @Suspended final AsyncResponse ar) {
- try (var jsonBody = new JsonPatchBody(body)) {
- yangPatchData(identifier, jsonBody, ar);
- }
- }
-
- /**
- * Ordered list of edits that are applied to the datastore by the server, as defined in
- * <a href="https://www.rfc-editor.org/rfc/rfc8072#section-2">RFC8072, section 2</a>.
- *
- * @param body YANG Patch body
- * @param ar {@link AsyncResponse} which needs to be completed with a {@link PatchStatusContext}
- */
- @PATCH
- @Path("/data")
- @Consumes(MediaTypes.APPLICATION_YANG_PATCH_JSON)
- @Produces({
- MediaTypes.APPLICATION_YANG_DATA_JSON,
- MediaTypes.APPLICATION_YANG_DATA_XML
- })
- public void yangPatchDataJSON(final InputStream body, @Suspended final AsyncResponse ar) {
- try (var jsonBody = new JsonPatchBody(body)) {
- yangPatchData(jsonBody, ar);
- }
- }
-
- private void yangPatchData(final @NonNull PatchBody body, final AsyncResponse ar) {
- final var context = server.bindRequestRoot().getSchemaContext();
- yangPatchData(context, parsePatchBody(context, YangInstanceIdentifier.of(), body), null, ar);
- }
-
- private void yangPatchData(final String identifier, final @NonNull PatchBody body,
- final AsyncResponse ar) {
- final var reqPath = server.bindRequestPath(identifier);
- final var modelContext = reqPath.getSchemaContext();
- yangPatchData(modelContext, parsePatchBody(modelContext, reqPath.getInstanceIdentifier(), body),
- reqPath.getMountPoint(), ar);
- }
-
- @VisibleForTesting
- void yangPatchData(final @NonNull EffectiveModelContext modelContext,
- final @NonNull PatchContext patch, final @Nullable DOMMountPoint mountPoint, final AsyncResponse ar) {
- server.getRestconfStrategy(modelContext, mountPoint).patchData(patch)
- .addCallback(new JaxRsRestconfCallback<>(ar) {
- @Override
- Response transform(final PatchStatusContext result) {
- return Response.status(statusOf(result)).entity(result).build();
- }
-
- private static Status statusOf(final PatchStatusContext result) {
- if (result.ok()) {
- return Status.OK;
- }
- final var globalErrors = result.globalErrors();
- if (globalErrors != null && !globalErrors.isEmpty()) {
- return statusOfFirst(globalErrors);
- }
- for (var edit : result.editCollection()) {
- if (!edit.isOk()) {
- final var editErrors = edit.getEditErrors();
- if (editErrors != null && !editErrors.isEmpty()) {
- return statusOfFirst(editErrors);
- }
- }
- }
- return Status.INTERNAL_SERVER_ERROR;
- }
-
- private static Status statusOfFirst(final List<RestconfError> error) {
- return ErrorTags.statusOf(error.get(0).getErrorTag());
- }
- });
- }
-
- private static @NonNull PatchContext parsePatchBody(final @NonNull EffectiveModelContext context,
- final @NonNull YangInstanceIdentifier urlPath, final @NonNull PatchBody body) {
- try {
- return body.toPatchContext(context, urlPath);
- } catch (IOException e) {
- LOG.debug("Error parsing YANG Patch input", e);
- throw new RestconfDocumentedException("Error parsing input: " + e.getMessage(), ErrorType.PROTOCOL,
- ErrorTag.MALFORMED_MESSAGE, e);
- }
- }
-
/**
* Invoke Action operation.
*
import java.time.Clock;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
+import java.util.List;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.Encoded;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriInfo;
import org.eclipse.jdt.annotation.NonNull;
+import org.opendaylight.restconf.common.errors.RestconfError;
import org.opendaylight.restconf.common.errors.RestconfFuture;
+import org.opendaylight.restconf.common.patch.PatchStatusContext;
import org.opendaylight.restconf.nb.rfc8040.MediaTypes;
import org.opendaylight.restconf.nb.rfc8040.ReadDataParams;
import org.opendaylight.restconf.nb.rfc8040.databind.JsonOperationInputBody;
+import org.opendaylight.restconf.nb.rfc8040.databind.JsonPatchBody;
import org.opendaylight.restconf.nb.rfc8040.databind.JsonResourceBody;
import org.opendaylight.restconf.nb.rfc8040.databind.OperationInputBody;
import org.opendaylight.restconf.nb.rfc8040.databind.XmlOperationInputBody;
+import org.opendaylight.restconf.nb.rfc8040.databind.XmlPatchBody;
import org.opendaylight.restconf.nb.rfc8040.databind.XmlResourceBody;
import org.opendaylight.restconf.nb.rfc8040.databind.jaxrs.QueryParams;
+import org.opendaylight.restconf.nb.rfc8040.legacy.ErrorTags;
import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload;
import org.opendaylight.restconf.server.api.OperationsContent;
import org.opendaylight.restconf.server.api.RestconfServer;
});
}
+ /**
+ * Ordered list of edits that are applied to the datastore by the server, as defined in
+ * <a href="https://www.rfc-editor.org/rfc/rfc8072#section-2">RFC8072, section 2</a>.
+ *
+ * @param body YANG Patch body
+ * @param ar {@link AsyncResponse} which needs to be completed with a {@link PatchStatusContext}
+ */
+ @PATCH
+ @Path("/data")
+ @Consumes(MediaTypes.APPLICATION_YANG_PATCH_JSON)
+ @Produces({
+ MediaTypes.APPLICATION_YANG_DATA_JSON,
+ MediaTypes.APPLICATION_YANG_DATA_XML
+ })
+ public void dataYangJsonPATCH(final InputStream body, @Suspended final AsyncResponse ar) {
+ try (var jsonBody = new JsonPatchBody(body)) {
+ completeDataYangPATCH(server.dataPATCH(jsonBody), ar);
+ }
+ }
+
+ /**
+ * Ordered list of edits that are applied to the target datastore by the server, as defined in
+ * <a href="https://www.rfc-editor.org/rfc/rfc8072#section-2">RFC8072, section 2</a>.
+ *
+ * @param identifier path to target
+ * @param body YANG Patch body
+ * @param ar {@link AsyncResponse} which needs to be completed with a {@link PatchStatusContext}
+ */
+ @PATCH
+ @Path("/data/{identifier:.+}")
+ @Consumes(MediaTypes.APPLICATION_YANG_PATCH_JSON)
+ @Produces({
+ MediaTypes.APPLICATION_YANG_DATA_JSON,
+ MediaTypes.APPLICATION_YANG_DATA_XML
+ })
+ public void dataYangJsonPATCH(@Encoded @PathParam("identifier") final String identifier,
+ final InputStream body, @Suspended final AsyncResponse ar) {
+ try (var jsonBody = new JsonPatchBody(body)) {
+ completeDataYangPATCH(server.dataPATCH(identifier, jsonBody), ar);
+ }
+ }
+
+ /**
+ * Ordered list of edits that are applied to the datastore by the server, as defined in
+ * <a href="https://www.rfc-editor.org/rfc/rfc8072#section-2">RFC8072, section 2</a>.
+ *
+ * @param body YANG Patch body
+ * @param ar {@link AsyncResponse} which needs to be completed with a {@link PatchStatusContext}
+ */
+ @PATCH
+ @Path("/data")
+ @Consumes(MediaTypes.APPLICATION_YANG_PATCH_XML)
+ @Produces({
+ MediaTypes.APPLICATION_YANG_DATA_JSON,
+ MediaTypes.APPLICATION_YANG_DATA_XML
+ })
+ public void dataYangXmlPATCH(final InputStream body, @Suspended final AsyncResponse ar) {
+ try (var xmlBody = new XmlPatchBody(body)) {
+ completeDataYangPATCH(server.dataPATCH(xmlBody), ar);
+ }
+ }
+
+ /**
+ * Ordered list of edits that are applied to the target datastore by the server, as defined in
+ * <a href="https://www.rfc-editor.org/rfc/rfc8072#section-2">RFC8072, section 2</a>.
+ *
+ * @param identifier path to target
+ * @param body YANG Patch body
+ * @param ar {@link AsyncResponse} which needs to be completed with a {@link PatchStatusContext}
+ */
+ @PATCH
+ @Path("/data/{identifier:.+}")
+ @Consumes(MediaTypes.APPLICATION_YANG_PATCH_XML)
+ @Produces({
+ MediaTypes.APPLICATION_YANG_DATA_JSON,
+ MediaTypes.APPLICATION_YANG_DATA_XML
+ })
+ public void dataYangXmlPATCH(@Encoded @PathParam("identifier") final String identifier, final InputStream body,
+ @Suspended final AsyncResponse ar) {
+ try (var xmlBody = new XmlPatchBody(body)) {
+ completeDataYangPATCH(server.dataPATCH(identifier, xmlBody), ar);
+ }
+ }
+
+ private static void completeDataYangPATCH(final RestconfFuture<PatchStatusContext> future, final AsyncResponse ar) {
+ future.addCallback(new JaxRsRestconfCallback<>(ar) {
+ @Override
+ Response transform(final PatchStatusContext result) {
+ return Response.status(statusOf(result)).entity(result).build();
+ }
+
+ private static Status statusOf(final PatchStatusContext result) {
+ if (result.ok()) {
+ return Status.OK;
+ }
+ final var globalErrors = result.globalErrors();
+ if (globalErrors != null && !globalErrors.isEmpty()) {
+ return statusOfFirst(globalErrors);
+ }
+ for (var edit : result.editCollection()) {
+ if (!edit.isOk()) {
+ final var editErrors = edit.getEditErrors();
+ if (editErrors != null && !editErrors.isEmpty()) {
+ return statusOfFirst(editErrors);
+ }
+ }
+ }
+ return Status.INTERNAL_SERVER_ERROR;
+ }
+
+ private static Status statusOfFirst(final List<RestconfError> error) {
+ return ErrorTags.statusOf(error.get(0).getErrorTag());
+ }
+ });
+ }
+
/**
* List RPC and action operations in RFC7951 format.
*
import org.eclipse.jdt.annotation.Nullable;
import org.opendaylight.restconf.api.ApiPath;
import org.opendaylight.restconf.common.errors.RestconfFuture;
+import org.opendaylight.restconf.common.patch.PatchStatusContext;
import org.opendaylight.restconf.nb.rfc8040.ReadDataParams;
import org.opendaylight.restconf.nb.rfc8040.databind.OperationInputBody;
+import org.opendaylight.restconf.nb.rfc8040.databind.PatchBody;
import org.opendaylight.restconf.nb.rfc8040.databind.ResourceBody;
import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload;
import org.opendaylight.restconf.server.spi.OperationOutput;
*/
RestconfFuture<Empty> dataPATCH(String identifier, ResourceBody body);
+ /**
+ * Ordered list of edits that are applied to the datastore by the server, as defined in
+ * <a href="https://www.rfc-editor.org/rfc/rfc8072#section-2">RFC8072, section 2</a>.
+ *
+ * @param body YANG Patch body
+ * @return A {@link RestconfFuture} of the {@link PatchStatusContext} content
+ */
+ RestconfFuture<PatchStatusContext> dataPATCH(PatchBody body);
+
+ /**
+ * Ordered list of edits that are applied to the datastore by the server, as defined in
+ * <a href="https://www.rfc-editor.org/rfc/rfc8072#section-2">RFC8072, section 2</a>.
+ *
+ * @param identifier path to target
+ * @param body YANG Patch body
+ * @return A {@link RestconfFuture} of the {@link PatchStatusContext} content
+ */
+ RestconfFuture<PatchStatusContext> dataPATCH(String identifier, PatchBody body);
+
/**
* Return the set of supported RPCs supported by {@link #operationsPOST(URI, String, OperationInputBody)}.
*
*/
package org.opendaylight.restconf.nb.rfc8040;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
import org.opendaylight.yangtools.yang.common.Decimal64;
import org.opendaylight.yangtools.yang.common.QName;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
protected static final EffectiveModelContext JUKEBOX_SCHEMA =
YangParserTestUtils.parseYangResourceDirectory("/jukebox");
+
+ protected static final InputStream stringInputStream(final String str) {
+ return new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
+ }
}
--- /dev/null
+/*
+ * Copyright (c) 2023 PANTHEON.tech, s.r.o. 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.restconf.nb.rfc8040.rests.services.impl;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.opendaylight.yangtools.util.concurrent.FluentFutures.immediateFalseFluentFuture;
+import static org.opendaylight.yangtools.util.concurrent.FluentFutures.immediateTrueFluentFuture;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.opendaylight.mdsal.common.api.CommitInfo;
+import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
+import org.opendaylight.mdsal.dom.api.DOMDataTreeReadWriteTransaction;
+import org.opendaylight.restconf.common.patch.PatchStatusContext;
+import org.opendaylight.yangtools.yang.common.ErrorTag;
+import org.opendaylight.yangtools.yang.common.ErrorType;
+
+@ExtendWith(MockitoExtension.class)
+class RestconfDataPatchTest extends AbstractRestconfTest {
+ @Mock
+ private DOMDataTreeReadWriteTransaction tx;
+
+ @BeforeEach
+ void beforeEach() {
+ doReturn(tx).when(dataBroker).newReadWriteTransaction();
+ }
+
+ @Test
+ void testPatchData() {
+ doNothing().when(tx).delete(LogicalDatastoreType.CONFIGURATION, GAP_IID);
+ doReturn(immediateFalseFluentFuture()).when(tx).exists(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID);
+ doReturn(immediateTrueFluentFuture()).when(tx).exists(LogicalDatastoreType.CONFIGURATION, GAP_IID);
+ doReturn(CommitInfo.emptyFluentFuture()).when(tx).commit();
+ doReturn(true).when(asyncResponse).resume(responseCaptor.capture());
+ restconf.dataYangJsonPATCH(stringInputStream("""
+ {
+ "ietf-yang-patch:yang-patch" : {
+ "patch-id" : "test patch id",
+ "edit" : [
+ {
+ "edit-id" : "create data",
+ "operation" : "create",
+ "target" : "/example-jukebox:jukebox",
+ "value" : {
+ "jukebox" : {
+ "player" : {
+ "gap" : "0.2"
+ }
+ }
+ }
+ },
+ {
+ "edit-id" : "replace data",
+ "operation" : "replace",
+ "target" : "/example-jukebox:jukebox",
+ "value" : {
+ "jukebox" : {
+ "player" : {
+ "gap" : "0.3"
+ }
+ }
+ }
+ },
+ {
+ "edit-id" : "delete data",
+ "operation" : "delete",
+ "target" : "/example-jukebox:jukebox/player/gap"
+ }
+ ]
+ }"""), asyncResponse);
+ final var response = responseCaptor.getValue();
+ assertEquals(200, response.getStatus());
+ final var status = assertInstanceOf(PatchStatusContext.class, response.getEntity());
+ assertTrue(status.ok());
+ final var edits = status.editCollection();
+ assertEquals(3, edits.size());
+ assertTrue(edits.get(0).isOk());
+ assertTrue(edits.get(1).isOk());
+ assertTrue(edits.get(2).isOk());
+ }
+
+ @Test
+ void testPatchDataDeleteNotExist() {
+ doNothing().when(tx).delete(LogicalDatastoreType.CONFIGURATION, GAP_IID);
+ doReturn(immediateFalseFluentFuture()).when(tx).exists(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID);
+ doReturn(immediateFalseFluentFuture()).when(tx).exists(LogicalDatastoreType.CONFIGURATION, GAP_IID);
+ doReturn(true).when(tx).cancel();
+
+ doReturn(true).when(asyncResponse).resume(responseCaptor.capture());
+ restconf.dataYangJsonPATCH(stringInputStream("""
+ {
+ "ietf-yang-patch:yang-patch" : {
+ "patch-id" : "test patch id",
+ "edit" : [
+ {
+ "edit-id" : "create data",
+ "operation" : "create",
+ "target" : "/example-jukebox:jukebox",
+ "value" : {
+ "jukebox" : {
+ "player" : {
+ "gap" : "0.2"
+ }
+ }
+ }
+ },
+ {
+ "edit-id" : "remove data",
+ "operation" : "remove",
+ "target" : "/example-jukebox:jukebox/player/gap"
+ },
+ {
+ "edit-id" : "delete data",
+ "operation" : "delete",
+ "target" : "/example-jukebox:jukebox/player/gap"
+ }
+ ]
+ }
+ }"""), asyncResponse);
+ final var response = responseCaptor.getValue();
+ assertEquals(409, response.getStatus());
+ final var status = assertInstanceOf(PatchStatusContext.class, response.getEntity());
+ assertFalse(status.ok());
+ final var edits = status.editCollection();
+ assertEquals(3, edits.size());
+ assertTrue(edits.get(0).isOk());
+ assertTrue(edits.get(1).isOk());
+ final var edit = edits.get(2);
+ assertFalse(edit.isOk());
+ final var errors = edit.getEditErrors();
+ assertEquals(1, errors.size());
+ final var error = errors.get(0);
+ assertEquals("Data does not exist", error.getErrorMessage());
+ assertEquals(ErrorType.PROTOCOL, error.getErrorType());
+ assertEquals(ErrorTag.DATA_MISSING, error.getErrorTag());
+ assertEquals(GAP_IID, error.getErrorPath());
+ }
+
+ @Test
+ void testPatchDataMountPoint() throws Exception {
+ doNothing().when(tx).delete(LogicalDatastoreType.CONFIGURATION, GAP_IID);
+ doReturn(immediateFalseFluentFuture()).when(tx).exists(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID);
+ doReturn(immediateTrueFluentFuture()).when(tx).exists(LogicalDatastoreType.CONFIGURATION, GAP_IID);
+ doReturn(CommitInfo.emptyFluentFuture()).when(tx).commit();
+
+ doReturn(true).when(asyncResponse).resume(responseCaptor.capture());
+ restconf.dataYangXmlPATCH(stringInputStream("""
+ <yang-patch xmlns="urn:ietf:params:xml:ns:yang:ietf-yang-patch">
+ <patch-id>test patch id</patch-id>
+ <edit>
+ <edit-id>create data</edit-id>
+ <operation>create</operation>
+ <target>/example-jukebox:jukebox</target>
+ <value>
+ <jukebox xmlns="http://example.com/ns/example-jukebox">
+ <player>
+ <gap>0.2</gap>
+ </player>
+ </jukebox>
+ </value>
+ </edit>
+ <edit>
+ <edit-id>replace data</edit-id>
+ <operation>replace</operation>
+ <target>/example-jukebox:jukebox</target>
+ <value>
+ <jukebox xmlns="http://example.com/ns/example-jukebox">
+ <player>
+ <gap>0.3</gap>
+ </player>
+ </jukebox>
+ </value>
+ </edit>
+ <edit>
+ <edit-id>delete data</edit-id>
+ <operation>delete</operation>
+ <target>/example-jukebox:jukebox/player/gap</target>
+ </edit>
+ </yang-patch>"""), asyncResponse);
+ final var response = responseCaptor.getValue();
+ assertEquals(200, response.getStatus());
+ final var status = assertInstanceOf(PatchStatusContext.class, response.getEntity());
+ assertTrue(status.ok());
+ assertNull(status.globalErrors());
+ final var edits = status.editCollection();
+ assertEquals(3, edits.size());
+ assertTrue(edits.get(0).isOk());
+ assertTrue(edits.get(1).isOk());
+ assertTrue(edits.get(2).isOk());
+ }
+}
package org.opendaylight.restconf.nb.rfc8040.rests.services.impl;
import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertInstanceOf;
-import static org.junit.jupiter.api.Assertions.assertNull;
-import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.opendaylight.yangtools.util.concurrent.FluentFutures.immediateFalseFluentFuture;
import static org.opendaylight.yangtools.util.concurrent.FluentFutures.immediateTrueFluentFuture;
-import java.io.ByteArrayInputStream;
-import java.io.InputStream;
import java.net.URI;
-import java.nio.charset.StandardCharsets;
-import java.util.List;
import java.util.Optional;
import java.util.Set;
import javax.ws.rs.container.AsyncResponse;
import org.opendaylight.mdsal.dom.api.DOMSchemaService;
import org.opendaylight.mdsal.dom.spi.FixedDOMSchemaService;
import org.opendaylight.netconf.dom.api.NetconfDataTreeService;
-import org.opendaylight.restconf.common.patch.PatchContext;
-import org.opendaylight.restconf.common.patch.PatchEntity;
-import org.opendaylight.restconf.common.patch.PatchStatusContext;
import org.opendaylight.restconf.nb.rfc8040.AbstractJukeboxTest;
import org.opendaylight.restconf.nb.rfc8040.databind.DatabindContext;
import org.opendaylight.restconf.nb.rfc8040.databind.DatabindProvider;
-import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.yang.patch.rev170222.yang.patch.yang.patch.Edit.Operation;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
import org.opendaylight.yangtools.yang.data.impl.schema.Builders;
assertEquals(Response.Status.NO_CONTENT.getStatusCode(), response.getStatus());
}
- private static InputStream stringInputStream(final String str) {
- return new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
- }
-
@Test
public void testPostData() {
doReturn(new MultivaluedHashMap<>()).when(uriInfo).getQueryParameters();
response.getLocation());
}
- @Test
- public void testPatchData() {
- final var patch = new PatchContext("test patch id", List.of(
- new PatchEntity("create data", Operation.Create, JUKEBOX_IID, EMPTY_JUKEBOX),
- new PatchEntity("replace data", Operation.Replace, JUKEBOX_IID, EMPTY_JUKEBOX),
- new PatchEntity("delete data", Operation.Delete, GAP_IID)));
-
- doNothing().when(readWrite).delete(LogicalDatastoreType.CONFIGURATION, GAP_IID);
- doReturn(immediateFalseFluentFuture())
- .when(readWrite).exists(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID);
- doReturn(immediateTrueFluentFuture())
- .when(readWrite).exists(LogicalDatastoreType.CONFIGURATION, GAP_IID);
- doReturn(true).when(asyncResponse).resume(responseCaptor.capture());
- dataService.yangPatchData(JUKEBOX_SCHEMA, patch, null, asyncResponse);
- final var response = responseCaptor.getValue();
- assertEquals(200, response.getStatus());
- final var status = assertInstanceOf(PatchStatusContext.class, response.getEntity());
-
- assertTrue(status.ok());
- assertEquals(3, status.editCollection().size());
- assertEquals("replace data", status.editCollection().get(1).getEditId());
- }
-
- @Test
- public void testPatchDataMountPoint() throws Exception {
- final var patch = new PatchContext("test patch id", List.of(
- new PatchEntity("create data", Operation.Create, JUKEBOX_IID, EMPTY_JUKEBOX),
- new PatchEntity("replace data", Operation.Replace, JUKEBOX_IID, EMPTY_JUKEBOX),
- new PatchEntity("delete data", Operation.Delete, GAP_IID)));
-
- doNothing().when(readWrite).delete(LogicalDatastoreType.CONFIGURATION, GAP_IID);
- doReturn(immediateFalseFluentFuture())
- .when(readWrite).exists(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID);
- doReturn(immediateTrueFluentFuture()).when(readWrite).exists(LogicalDatastoreType.CONFIGURATION, GAP_IID);
-
- doReturn(true).when(asyncResponse).resume(responseCaptor.capture());
- dataService.yangPatchData(JUKEBOX_SCHEMA, patch, mountPoint, asyncResponse);
- final var response = responseCaptor.getValue();
- assertEquals(200, response.getStatus());
- final var status = assertInstanceOf(PatchStatusContext.class, response.getEntity());
-
- assertTrue(status.ok());
- assertEquals(3, status.editCollection().size());
- assertNull(status.globalErrors());
- }
-
- @Test
- public void testPatchDataDeleteNotExist() {
- final var patch = new PatchContext("test patch id", List.of(
- new PatchEntity("create data", Operation.Create, JUKEBOX_IID, EMPTY_JUKEBOX),
- new PatchEntity("remove data", Operation.Remove, GAP_IID),
- new PatchEntity("delete data", Operation.Delete, GAP_IID)));
-
- doNothing().when(readWrite).delete(LogicalDatastoreType.CONFIGURATION, GAP_IID);
- doReturn(immediateFalseFluentFuture())
- .when(readWrite).exists(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID);
- doReturn(immediateFalseFluentFuture())
- .when(readWrite).exists(LogicalDatastoreType.CONFIGURATION, GAP_IID);
- doReturn(true).when(readWrite).cancel();
-
- doReturn(true).when(asyncResponse).resume(responseCaptor.capture());
- dataService.yangPatchData(JUKEBOX_SCHEMA, patch, null, asyncResponse);
- final var response = responseCaptor.getValue();
- assertEquals(409, response.getStatus());
- final var status = assertInstanceOf(PatchStatusContext.class, response.getEntity());
-
- assertFalse(status.ok());
- assertEquals(3, status.editCollection().size());
- assertTrue(status.editCollection().get(0).isOk());
- assertTrue(status.editCollection().get(1).isOk());
- assertFalse(status.editCollection().get(2).isOk());
- assertFalse(status.editCollection().get(2).getEditErrors().isEmpty());
- final String errorMessage = status.editCollection().get(2).getEditErrors().get(0).getErrorMessage();
- assertEquals("Data does not exist", errorMessage);
- }
}
import static org.mockito.Mockito.verify;
import com.google.common.util.concurrent.Futures;
-import java.io.ByteArrayInputStream;
import java.net.URI;
-import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
prepNNC(result);
final var ar = mock(AsyncResponse.class);
final var captor = ArgumentCaptor.forClass(Response.class);
- restconf.operationsXmlPOST("invoke-rpc-module:rpc-test", new ByteArrayInputStream("""
+ restconf.operationsXmlPOST("invoke-rpc-module:rpc-test", stringInputStream("""
<input xmlns="invoke:rpc:module"/>
- """.getBytes(StandardCharsets.UTF_8)), mock(UriInfo.class), ar);
+ """), mock(UriInfo.class), ar);
verify(ar).resume(captor.capture());
final var response = captor.getValue();
prepNNC(result);
final var ar = mock(AsyncResponse.class);
final var response = ArgumentCaptor.forClass(Response.class);
- restconf.operationsJsonPOST("invoke-rpc-module:rpc-test", new ByteArrayInputStream("""
+ restconf.operationsJsonPOST("invoke-rpc-module:rpc-test", stringInputStream("""
{
"invoke-rpc-module:input" : {
}
}
- """.getBytes(StandardCharsets.UTF_8)), mock(UriInfo.class), ar);
+ """), mock(UriInfo.class), ar);
verify(ar).resume(response.capture());
assertEquals(204, response.getValue().getStatus());