Introduce RestconfServer methods for YANG Patch 14/109014/2
authorRobert Varga <robert.varga@pantheon.tech>
Fri, 17 Nov 2023 19:50:11 +0000 (20:50 +0100)
committerRobert Varga <robert.varga@pantheon.tech>
Sat, 18 Nov 2023 08:00:58 +0000 (09:00 +0100)
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>
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/MdsalRestconfServer.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/RestconfDataServiceImpl.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/RestconfImpl.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/RestconfServer.java
restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/rfc8040/AbstractJukeboxTest.java
restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/RestconfDataPatchTest.java [new file with mode: 0644]
restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/RestconfDataServiceImplTest.java
restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/RestconfOperationsPostTest.java

index 0fe82776ae7bddb64ace15181891be796c01a08c..f97ee6312cf55ae44aecf39665e47bbf93637750 100644 (file)
@@ -44,10 +44,13 @@ import org.opendaylight.mdsal.dom.spi.SimpleDOMActionResult;
 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;
@@ -191,6 +194,30 @@ public final class MdsalRestconfServer implements RestconfServer {
         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) {
index 2eb2ba56e9a36f58568a01313cd4737db737599e..413b28d62d833c6fdd7e9bbba76eef81c59ed4fc 100644 (file)
@@ -9,19 +9,15 @@ package org.opendaylight.restconf.nb.rfc8040.rests.services.impl;
 
 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;
@@ -29,37 +25,26 @@ import javax.ws.rs.core.MediaType;
 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;
@@ -68,8 +53,6 @@ import org.opendaylight.yangtools.yang.model.api.ActionDefinition;
 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
@@ -77,8 +60,6 @@ import org.slf4j.LoggerFactory;
  */
 @Path("/")
 public final class RestconfDataServiceImpl {
-    private static final Logger LOG = LoggerFactory.getLogger(RestconfDataServiceImpl.class);
-
     private final DatabindProvider databindProvider;
     private final MdsalRestconfServer server;
 
@@ -337,149 +318,6 @@ public final class RestconfDataServiceImpl {
         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.
      *
index 8039fcb25bbcd90cc8eb49c7c2fb046d6d5bab9d..c75152a14ee3b24b52b1b0275720dafe8fe83fc7 100644 (file)
@@ -13,6 +13,7 @@ import java.io.InputStream;
 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;
@@ -31,15 +32,20 @@ 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.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;
@@ -234,6 +240,122 @@ public final class RestconfImpl {
         });
     }
 
+    /**
+     * 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.
      *
index a8bd8e0e0deba1e73e15ccffe0b57f9afb175bdf..09b66bf2c82e3c108b9bf6ae9e720b74c9e9882c 100644 (file)
@@ -12,8 +12,10 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
 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;
@@ -70,6 +72,25 @@ public interface RestconfServer {
      */
     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)}.
      *
index 89d7d35ae98b86307bcefa84c8aa9dfb3d4051c6..077e04ca7968d185b30147161490b3bcf9e75c07 100644 (file)
@@ -7,6 +7,9 @@
  */
 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;
@@ -66,4 +69,8 @@ public abstract class AbstractJukeboxTest {
 
     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));
+    }
 }
diff --git a/restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/RestconfDataPatchTest.java b/restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/RestconfDataPatchTest.java
new file mode 100644 (file)
index 0000000..1a9b953
--- /dev/null
@@ -0,0 +1,205 @@
+/*
+ * 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());
+    }
+}
index f544f9547c20c00a2a9af75eaabb477419594068..34d458c4ee958e77b6f27a4dd12f0bc19cadc061 100644 (file)
@@ -8,10 +8,6 @@
 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;
@@ -19,11 +15,7 @@ import static org.mockito.Mockito.mock;
 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;
@@ -51,13 +43,9 @@ import org.opendaylight.mdsal.dom.api.DOMRpcService;
 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;
@@ -153,10 +141,6 @@ public class RestconfDataServiceImplTest extends AbstractJukeboxTest {
         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();
@@ -200,79 +184,4 @@ public class RestconfDataServiceImplTest extends AbstractJukeboxTest {
             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);
-    }
 }
index 1868f618dcfa5604f36e2140b7aa650e9e486efb..be76a401641a05d718a58e50527015174bbd5c22 100644 (file)
@@ -18,9 +18,7 @@ import static org.mockito.Mockito.mock;
 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;
@@ -88,9 +86,9 @@ class RestconfOperationsPostTest extends AbstractRestconfTest {
         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();
@@ -107,12 +105,12 @@ class RestconfOperationsPostTest extends AbstractRestconfTest {
         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());