Introduce RestconfServer.dataPOST() 18/109018/3
authorRobert Varga <robert.varga@pantheon.tech>
Sat, 18 Nov 2023 02:01:23 +0000 (03:01 +0100)
committerRobert Varga <nite@hq.sk>
Sat, 18 Nov 2023 14:31:40 +0000 (14:31 +0000)
POST on /data has two modes, Invoke Operation and Create Resource,
which we can neatly capture in RestconfServer.

This eliminates the final bits of RestconfDataServiceImpl, completing
indirection through RestconfServer.

JIRA: NETCONF-773
Change-Id: I7338d6f49688bc2769a1fc5e952553478566965a
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
16 files changed:
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/JaxRsNorthbound.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/RestconfApplication.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/databind/AbstractBody.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/databind/DataPostBody.java [new file with mode: 0644]
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/databind/JsonDataPostBody.java [new file with mode: 0644]
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/databind/XmlDataPostBody.java [new file with mode: 0644]
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/databind/jaxrs/QueryParams.java
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 [deleted file]
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/nb/rfc8040/rests/transactions/RestconfStrategy.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/DataPostResult.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/databind/jaxrs/QueryParamsTest.java
restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/Netconf799Test.java
restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/RestconfDataServiceImplTest.java

index 73d53e5fbb2286a3ed59ceaba8e3df0f4d385211..6ab5c8b8cbbd082e3efa524cc13e8bfea9ca32c9 100644 (file)
@@ -20,8 +20,8 @@ import org.opendaylight.aaa.web.servlet.ServletSupport;
 import org.opendaylight.mdsal.dom.api.DOMMountPointService;
 import org.opendaylight.mdsal.dom.api.DOMSchemaService;
 import org.opendaylight.restconf.nb.rfc8040.databind.DatabindProvider;
-import org.opendaylight.restconf.nb.rfc8040.rests.services.impl.MdsalRestconfServer;
 import org.opendaylight.restconf.nb.rfc8040.streams.RestconfStreamServletFactory;
+import org.opendaylight.restconf.server.api.RestconfServer;
 import org.opendaylight.yangtools.concepts.Registration;
 import org.osgi.service.component.annotations.Activate;
 import org.osgi.service.component.annotations.Component;
@@ -42,7 +42,7 @@ public final class JaxRsNorthbound implements AutoCloseable {
             @Reference final ServletSupport servletSupport,
             @Reference final CustomFilterAdapterConfiguration filterAdapterConfiguration,
             @Reference final DOMMountPointService mountPointService, @Reference final DOMSchemaService schemaService,
-            @Reference final DatabindProvider databindProvider, @Reference final MdsalRestconfServer server,
+            @Reference final DatabindProvider databindProvider, @Reference final RestconfServer server,
             @Reference final RestconfStreamServletFactory servletFactory) throws ServletException {
         final var restconfBuilder = WebContext.builder()
             .name("RFC8040 RESTCONF")
index 070ac1305d93dea99acf8ce01dab02a42a76d9b2..44c386404a14182b5ed0123a21b0e74fc0970dd1 100644 (file)
@@ -19,19 +19,17 @@ import org.opendaylight.restconf.nb.rfc8040.jersey.providers.XmlPatchStatusBodyW
 import org.opendaylight.restconf.nb.rfc8040.jersey.providers.YangSchemaExportBodyWriter;
 import org.opendaylight.restconf.nb.rfc8040.jersey.providers.YinSchemaExportBodyWriter;
 import org.opendaylight.restconf.nb.rfc8040.jersey.providers.errors.RestconfDocumentedExceptionMapper;
-import org.opendaylight.restconf.nb.rfc8040.rests.services.impl.MdsalRestconfServer;
-import org.opendaylight.restconf.nb.rfc8040.rests.services.impl.RestconfDataServiceImpl;
 import org.opendaylight.restconf.nb.rfc8040.rests.services.impl.RestconfImpl;
 import org.opendaylight.restconf.nb.rfc8040.rests.services.impl.RestconfSchemaServiceImpl;
+import org.opendaylight.restconf.server.api.RestconfServer;
 
 final class RestconfApplication extends Application {
     private final Set<Object> singletons;
 
-    RestconfApplication(final DatabindProvider databindProvider, final MdsalRestconfServer server,
+    RestconfApplication(final DatabindProvider databindProvider, final RestconfServer server,
             final DOMMountPointService mountPointService, final DOMSchemaService domSchemaService) {
         singletons = Set.of(
             new RestconfDocumentedExceptionMapper(databindProvider),
-            new RestconfDataServiceImpl(databindProvider, server),
             new RestconfImpl(server),
             new RestconfSchemaServiceImpl(domSchemaService, mountPointService));
     }
index 5c73ccea178d706f54d94b40df68b2a1889f745e..2301c2b2f4f543e1f21ec0c4c4e251dec1da3dcf 100644 (file)
@@ -26,7 +26,7 @@ import org.slf4j.LoggerFactory;
  * An abstract request body backed by an {@link InputStream}.
  */
 public abstract sealed class AbstractBody implements AutoCloseable
-        permits ChildBody, OperationInputBody, PatchBody, ResourceBody {
+        permits ChildBody, DataPostBody, OperationInputBody, PatchBody, ResourceBody {
     private static final Logger LOG = LoggerFactory.getLogger(AbstractBody.class);
 
     private static final VarHandle INPUT_STREAM;
diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/databind/DataPostBody.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/databind/DataPostBody.java
new file mode 100644 (file)
index 0000000..551d664
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ * 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.databind;
+
+import java.io.InputStream;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Body of a {@code POST} request as defined in
+ * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.4">RFC8040 section 4.4</a>.
+ */
+@NonNullByDefault
+public abstract sealed class DataPostBody extends AbstractBody permits JsonDataPostBody, XmlDataPostBody {
+    DataPostBody(final InputStream inputStream) {
+        super(inputStream);
+    }
+
+    public abstract OperationInputBody toOperationInput();
+
+    public abstract ChildBody toResource();
+}
diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/databind/JsonDataPostBody.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/databind/JsonDataPostBody.java
new file mode 100644 (file)
index 0000000..bb75173
--- /dev/null
@@ -0,0 +1,28 @@
+/*
+ * 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.databind;
+
+import java.io.InputStream;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+@NonNullByDefault
+public final class JsonDataPostBody extends DataPostBody {
+    public JsonDataPostBody(final InputStream inputStream) {
+        super(inputStream);
+    }
+
+    @Override
+    public JsonOperationInputBody toOperationInput() {
+        return new JsonOperationInputBody(acquireStream());
+    }
+
+    @Override
+    public JsonChildBody toResource() {
+        return new JsonChildBody(acquireStream());
+    }
+}
diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/databind/XmlDataPostBody.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/databind/XmlDataPostBody.java
new file mode 100644 (file)
index 0000000..61d92d9
--- /dev/null
@@ -0,0 +1,28 @@
+/*
+ * 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.databind;
+
+import java.io.InputStream;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+@NonNullByDefault
+public final class XmlDataPostBody extends DataPostBody {
+    public XmlDataPostBody(final InputStream inputStream) {
+        super(inputStream);
+    }
+
+    @Override
+    public XmlOperationInputBody toOperationInput() {
+        return new XmlOperationInputBody(acquireStream());
+    }
+
+    @Override
+    public XmlChildBody toResource() {
+        return new XmlChildBody(acquireStream());
+    }
+}
index 9196840e2d837b797f70db7a71d523bf4a123a7f..eb840e58783a515b1ab1e5b3e9dd92bf47eaa046 100644 (file)
@@ -34,16 +34,13 @@ import org.opendaylight.restconf.api.query.StopTimeParam;
 import org.opendaylight.restconf.api.query.WithDefaultsParam;
 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
 import org.opendaylight.restconf.common.errors.RestconfError;
-import org.opendaylight.restconf.nb.rfc8040.Insert;
 import org.opendaylight.restconf.nb.rfc8040.ReadDataParams;
 import org.opendaylight.restconf.nb.rfc8040.legacy.InstanceIdentifierContext;
 import org.opendaylight.restconf.nb.rfc8040.legacy.QueryParameters;
 import org.opendaylight.restconf.nb.rfc8040.utils.parser.NetconfFieldsTranslator;
 import org.opendaylight.restconf.nb.rfc8040.utils.parser.WriterFieldsTranslator;
-import org.opendaylight.restconf.nb.rfc8040.utils.parser.YangInstanceIdentifierDeserializer;
 import org.opendaylight.yangtools.yang.common.ErrorTag;
 import org.opendaylight.yangtools.yang.common.ErrorType;
-import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
 
 @Beta
 public final class QueryParams {
@@ -155,43 +152,6 @@ public final class QueryParams {
         return new ReadDataParams(content, depth, fields, withDefaults, prettyPrint);
     }
 
-    public static @Nullable Insert parseInsert(final EffectiveModelContext modelContext, final UriInfo uriInfo) {
-        InsertParam insert = null;
-        PointParam point = null;
-
-        for (var entry : uriInfo.getQueryParameters().entrySet()) {
-            final var paramName = entry.getKey();
-            final var paramValues = entry.getValue();
-
-            try {
-                switch (paramName) {
-                    case InsertParam.uriName:
-                        insert = optionalParam(InsertParam::forUriValue, paramName, paramValues);
-                        break;
-                    case PointParam.uriName:
-                        point = optionalParam(PointParam::forUriValue, paramName, paramValues);
-                        break;
-                    default:
-                        throw unhandledParam("write", paramName);
-                }
-            } catch (IllegalArgumentException e) {
-                throw new RestconfDocumentedException("Invalid " + paramName + " value: " + e.getMessage(),
-                    ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, e);
-            }
-        }
-
-        try {
-            return Insert.forParams(insert, point,
-                // TODO: instead of a EffectiveModelContext, we should have received
-                //       YangInstanceIdentifierDeserializer.Result, from which we can use to seed the parser. This
-                //       call-site should not support 'yang-ext:mount' and should just reuse DataSchemaContextTree,
-                //       saving a lookup
-                value -> YangInstanceIdentifierDeserializer.create(modelContext, value).path.getLastPathArgument());
-        } catch (IllegalArgumentException e) {
-            throw new RestconfDocumentedException("Invalid query parameters: " + e.getMessage(), e);
-        }
-    }
-
     private static RestconfDocumentedException unhandledParam(final String operation, final String name) {
         return KNOWN_PARAMS.contains(name)
             ? new RestconfDocumentedException("Invalid parameter in " + operation + ": " + name,
index 51bc0b1fb191405a31a78d09171a12c78b96b985..818897a36026e46ca41a35a80ae8aba47e1f2d47 100644 (file)
@@ -27,6 +27,7 @@ import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.Optional;
 import java.util.concurrent.CancellationException;
 import javax.inject.Inject;
 import javax.inject.Singleton;
@@ -49,6 +50,8 @@ import org.opendaylight.restconf.common.patch.PatchContext;
 import org.opendaylight.restconf.common.patch.PatchStatusContext;
 import org.opendaylight.restconf.nb.rfc8040.Insert;
 import org.opendaylight.restconf.nb.rfc8040.ReadDataParams;
+import org.opendaylight.restconf.nb.rfc8040.databind.ChildBody;
+import org.opendaylight.restconf.nb.rfc8040.databind.DataPostBody;
 import org.opendaylight.restconf.nb.rfc8040.databind.DatabindContext;
 import org.opendaylight.restconf.nb.rfc8040.databind.DatabindProvider;
 import org.opendaylight.restconf.nb.rfc8040.databind.OperationInputBody;
@@ -60,6 +63,9 @@ import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload;
 import org.opendaylight.restconf.nb.rfc8040.rests.transactions.MdsalRestconfStrategy;
 import org.opendaylight.restconf.nb.rfc8040.rests.transactions.RestconfStrategy;
 import org.opendaylight.restconf.nb.rfc8040.utils.parser.ParserIdentifier;
+import org.opendaylight.restconf.server.api.DataPostResult;
+import org.opendaylight.restconf.server.api.DataPostResult.CreateResource;
+import org.opendaylight.restconf.server.api.DataPostResult.InvokeOperation;
 import org.opendaylight.restconf.server.api.DataPutResult;
 import org.opendaylight.restconf.server.api.OperationsGetResult;
 import org.opendaylight.restconf.server.api.RestconfServer;
@@ -78,9 +84,11 @@ import org.opendaylight.yangtools.yang.common.Revision;
 import org.opendaylight.yangtools.yang.common.RpcResultBuilder;
 import org.opendaylight.yangtools.yang.common.XMLNamespace;
 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.ContainerNode;
 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
+import org.opendaylight.yangtools.yang.model.api.ActionDefinition;
 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
 import org.opendaylight.yangtools.yang.model.api.stmt.RpcEffectiveStatement;
 import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier.Absolute;
@@ -96,7 +104,7 @@ import org.slf4j.LoggerFactory;
  */
 // FIXME: this should live in 'org.opendaylight.restconf.server.mdsal' package
 @Singleton
-@Component(service = { MdsalRestconfServer.class, RestconfServer.class })
+@Component
 public final class MdsalRestconfServer implements RestconfServer {
     private static final Logger LOG = LoggerFactory.getLogger(MdsalRestconfServer.class);
     private static final QName YANG_LIBRARY_VERSION = QName.create(Restconf.QNAME, "yang-library-version").intern();
@@ -221,8 +229,54 @@ public final class MdsalRestconfServer implements RestconfServer {
         return getRestconfStrategy(modelContext, reqPath.getMountPoint()).patchData(patch);
     }
 
-    // FIXME: should follow the same pattern as operationsPOST() does
-    RestconfFuture<DOMActionResult> dataInvokePOST(final InstanceIdentifierContext reqPath,
+    @Override
+    public RestconfFuture<CreateResource> dataPOST(final ChildBody body, final Map<String, String> queryParameters) {
+        return dataCreatePOST(bindRequestRoot(), body, queryParameters);
+    }
+
+    @Override
+    public RestconfFuture<? extends DataPostResult> dataPOST(final String identifier, final DataPostBody body,
+            final Map<String, String> queryParameters) {
+        final var reqPath = bindRequestPath(identifier);
+        if (reqPath.getSchemaNode() instanceof ActionDefinition) {
+            try (var inputBody = body.toOperationInput()) {
+                return dataInvokePOST(reqPath, inputBody);
+            }
+        }
+
+        try (var childBody = body.toResource()) {
+            return dataCreatePOST(reqPath, childBody, queryParameters);
+        }
+    }
+
+    private @NonNull RestconfFuture<CreateResource> dataCreatePOST(final InstanceIdentifierContext reqPath,
+            final ChildBody body, final Map<String, String> queryParameters) {
+        final var inference = reqPath.inference();
+        final var modelContext = inference.getEffectiveModelContext();
+
+        final Insert insert;
+        try {
+            insert = Insert.ofQueryParameters(modelContext, queryParameters);
+        } catch (IllegalArgumentException e) {
+            return RestconfFuture.failed(new RestconfDocumentedException(e.getMessage(),
+                ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, e));
+        }
+
+        final var parentPath = reqPath.getInstanceIdentifier();
+        final var payload = body.toPayload(parentPath, inference);
+        return getRestconfStrategy(modelContext, reqPath.getMountPoint())
+            .postData(concat(parentPath, payload.prefix()), payload.body(), insert);
+    }
+
+    private static YangInstanceIdentifier concat(final YangInstanceIdentifier parent, final List<PathArgument> args) {
+        var ret = parent;
+        for (var arg : args) {
+            ret = ret.node(arg);
+        }
+        return ret;
+    }
+
+    RestconfFuture<InvokeOperation> dataInvokePOST(final InstanceIdentifierContext reqPath,
             final OperationInputBody body) {
         final var yangIIdContext = reqPath.getInstanceIdentifier();
         final var inference = reqPath.inference();
@@ -237,8 +291,13 @@ public final class MdsalRestconfServer implements RestconfServer {
 
         final var mountPoint = reqPath.getMountPoint();
         final var schemaPath = inference.toSchemaInferenceStack().toSchemaNodeIdentifier();
-        return mountPoint != null ? dataInvokePOST(input, schemaPath, yangIIdContext, mountPoint)
+        final var future = mountPoint != null ? dataInvokePOST(input, schemaPath, yangIIdContext, mountPoint)
             : dataInvokePOST(input, schemaPath, yangIIdContext, actionService);
+
+        return future.transform(result -> result.getOutput()
+            .flatMap(output -> output.isEmpty() ? Optional.empty()
+                : Optional.of(new InvokeOperation(new NormalizedNodePayload(reqPath.inference(), output))))
+            .orElse(InvokeOperation.EMPTY));
     }
 
     /**
diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/RestconfDataServiceImpl.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/RestconfDataServiceImpl.java
deleted file mode 100644 (file)
index 078e717..0000000
+++ /dev/null
@@ -1,207 +0,0 @@
-/*
- * Copyright (c) 2016 Cisco Systems, Inc. and others.  All rights reserved.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License v1.0 which accompanies this distribution,
- * and is available at http://www.eclipse.org/legal/epl-v10.html
- */
-package org.opendaylight.restconf.nb.rfc8040.rests.services.impl;
-
-import static java.util.Objects.requireNonNull;
-
-import java.io.InputStream;
-import java.util.List;
-import javax.ws.rs.Consumes;
-import javax.ws.rs.Encoded;
-import javax.ws.rs.POST;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.container.AsyncResponse;
-import javax.ws.rs.container.Suspended;
-import javax.ws.rs.core.Context;
-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.Nullable;
-import org.opendaylight.mdsal.dom.api.DOMActionResult;
-import org.opendaylight.mdsal.dom.api.DOMMountPoint;
-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.OperationInputBody;
-import org.opendaylight.restconf.nb.rfc8040.databind.XmlChildBody;
-import org.opendaylight.restconf.nb.rfc8040.databind.XmlOperationInputBody;
-import org.opendaylight.restconf.nb.rfc8040.databind.jaxrs.QueryParams;
-import org.opendaylight.restconf.nb.rfc8040.legacy.InstanceIdentifierContext;
-import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload;
-import org.opendaylight.restconf.server.api.DataPostResult.CreateResource;
-import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
-import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
-import org.opendaylight.yangtools.yang.model.api.ActionDefinition;
-import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.Inference;
-
-/**
- * The "{+restconf}/data" subtree represents the datastore resource type, which is a collection of configuration data
- * and state data nodes.
- */
-@Path("/")
-public final class RestconfDataServiceImpl {
-    private final DatabindProvider databindProvider;
-    private final MdsalRestconfServer server;
-
-    public RestconfDataServiceImpl(final DatabindProvider databindProvider, final MdsalRestconfServer server) {
-        this.databindProvider = requireNonNull(databindProvider);
-        this.server = requireNonNull(server);
-    }
-
-    /**
-     * Create a top-level data resource.
-     *
-     * @param body data node for put to config DS
-     * @param uriInfo URI info
-     * @param ar {@link AsyncResponse} which needs to be completed
-     */
-    @POST
-    @Path("/data")
-    @Consumes({
-        MediaTypes.APPLICATION_YANG_DATA_JSON,
-        MediaType.APPLICATION_JSON,
-    })
-    public void postDataJSON(final InputStream body, @Context final UriInfo uriInfo,
-            @Suspended final AsyncResponse ar) {
-        try (var jsonBody = new JsonChildBody(body)) {
-            postData(jsonBody, uriInfo, ar);
-        }
-    }
-
-    /**
-     * Create a data resource in target.
-     *
-     * @param identifier path to target
-     * @param body data node for put to config DS
-     * @param uriInfo URI info
-     * @param ar {@link AsyncResponse} which needs to be completed
-     */
-    @POST
-    @Path("/data/{identifier:.+}")
-    @Consumes({
-        MediaTypes.APPLICATION_YANG_DATA_JSON,
-        MediaType.APPLICATION_JSON,
-    })
-    public void postDataJSON(@Encoded @PathParam("identifier") final String identifier, final InputStream body,
-            @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
-        final var reqPath = server.bindRequestPath(identifier);
-        if (reqPath.getSchemaNode() instanceof ActionDefinition) {
-            try (var jsonBody = new JsonOperationInputBody(body)) {
-                invokeAction(reqPath, jsonBody, ar);
-            }
-        } else {
-            try (var jsonBody = new JsonChildBody(body)) {
-                postData(reqPath.inference(), reqPath.getInstanceIdentifier(), jsonBody, uriInfo,
-                    reqPath.getMountPoint(), ar);
-            }
-        }
-    }
-
-    /**
-     * Create a top-level data resource.
-     *
-     * @param body data node for put to config DS
-     * @param uriInfo URI info
-     * @param ar {@link AsyncResponse} which needs to be completed
-     */
-    @POST
-    @Path("/data")
-    @Consumes({
-        MediaTypes.APPLICATION_YANG_DATA_XML,
-        MediaType.APPLICATION_XML,
-        MediaType.TEXT_XML
-    })
-    public void postDataXML(final InputStream body, @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
-        try (var xmlBody = new XmlChildBody(body)) {
-            postData(xmlBody, uriInfo, ar);
-        }
-    }
-
-    /**
-     * Create a data resource in target.
-     *
-     * @param identifier path to target
-     * @param body data node for put to config DS
-     * @param uriInfo URI info
-     * @param ar {@link AsyncResponse} which needs to be completed
-     */
-    @POST
-    @Path("/data/{identifier:.+}")
-    @Consumes({
-        MediaTypes.APPLICATION_YANG_DATA_XML,
-        MediaType.APPLICATION_XML,
-        MediaType.TEXT_XML
-    })
-    public void postDataXML(@Encoded @PathParam("identifier") final String identifier, final InputStream body,
-            @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
-        final var reqPath = server.bindRequestPath(identifier);
-        if (reqPath.getSchemaNode() instanceof ActionDefinition) {
-            try (var xmlBody = new XmlOperationInputBody(body)) {
-                invokeAction(reqPath, xmlBody, ar);
-            }
-        } else {
-            try (var xmlBody = new XmlChildBody(body)) {
-                postData(reqPath.inference(), reqPath.getInstanceIdentifier(), xmlBody, uriInfo,
-                    reqPath.getMountPoint(), ar);
-            }
-        }
-    }
-
-    private void postData(final ChildBody body, final UriInfo uriInfo, final AsyncResponse ar) {
-        postData(Inference.ofDataTreePath(databindProvider.currentContext().modelContext()),
-            YangInstanceIdentifier.of(), body, uriInfo, null, ar);
-    }
-
-    private void postData(final Inference inference, final YangInstanceIdentifier parentPath, final ChildBody body,
-            final UriInfo uriInfo, final @Nullable DOMMountPoint mountPoint, final AsyncResponse ar) {
-        final var modelContext = inference.getEffectiveModelContext();
-        final var insert = QueryParams.parseInsert(modelContext, uriInfo);
-        final var strategy = server.getRestconfStrategy(modelContext, mountPoint);
-        final var payload = body.toPayload(parentPath, inference);
-        final var data = payload.body();
-        final var path = concat(parentPath, payload.prefix());
-
-        strategy.postData(path, data, insert).addCallback(new JaxRsRestconfCallback<>(ar) {
-            @Override
-            Response transform(final CreateResource result) {
-                return Response.created(uriInfo.getBaseUriBuilder().path("data").path(result.createdPath()).build())
-                    .build();
-            }
-        });
-    }
-
-    private static YangInstanceIdentifier concat(final YangInstanceIdentifier parent, final List<PathArgument> args) {
-        var ret = parent;
-        for (var arg : args) {
-            ret = ret.node(arg);
-        }
-        return ret;
-    }
-
-    /**
-     * Invoke Action operation.
-     *
-     * @param payload {@link NormalizedNodePayload} - the body of the operation
-     * @param ar {@link AsyncResponse} which needs to be completed with a NormalizedNodePayload
-     */
-    private void invokeAction(final InstanceIdentifierContext reqPath, final OperationInputBody body,
-            final AsyncResponse ar) {
-        server.dataInvokePOST(reqPath, body).addCallback(new JaxRsRestconfCallback<>(ar) {
-            @Override
-            Response transform(final DOMActionResult result) {
-                final var output = result.getOutput().orElse(null);
-                return output == null || output.isEmpty() ? Response.status(Status.NO_CONTENT).build()
-                    : Response.status(Status.OK).entity(new NormalizedNodePayload(reqPath.inference(), output)).build();
-            }
-        });
-    }
-}
index 82ab6d1819e266d9c5ce6def66bda4928af548f7..9cb1815c9dc167503fc2db751802f2c7bbc38148 100644 (file)
@@ -38,28 +38,38 @@ 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.JsonChildBody;
+import org.opendaylight.restconf.nb.rfc8040.databind.JsonDataPostBody;
 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.XmlChildBody;
+import org.opendaylight.restconf.nb.rfc8040.databind.XmlDataPostBody;
 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.DataPostResult;
+import org.opendaylight.restconf.server.api.DataPostResult.CreateResource;
+import org.opendaylight.restconf.server.api.DataPostResult.InvokeOperation;
 import org.opendaylight.restconf.server.api.DataPutResult;
 import org.opendaylight.restconf.server.api.OperationsGetResult;
 import org.opendaylight.restconf.server.api.RestconfServer;
 import org.opendaylight.restconf.server.spi.OperationOutput;
 import org.opendaylight.yangtools.yang.common.Empty;
 import org.opendaylight.yangtools.yang.common.Revision;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * Baseline RESTCONF implementation with JAX-RS.
  */
 @Path("/")
 public final class RestconfImpl {
+    private static final Logger LOG = LoggerFactory.getLogger(RestconfImpl.class);
     private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MMM-dd HH:mm:ss");
 
     private final RestconfServer server;
@@ -358,6 +368,110 @@ public final class RestconfImpl {
         });
     }
 
+    /**
+     * Create a top-level data resource.
+     *
+     * @param body data node for put to config DS
+     * @param uriInfo URI info
+     * @param ar {@link AsyncResponse} which needs to be completed
+     */
+    @POST
+    @Path("/data")
+    @Consumes({
+        MediaTypes.APPLICATION_YANG_DATA_JSON,
+        MediaType.APPLICATION_JSON,
+    })
+    public void postDataJSON(final InputStream body, @Context final UriInfo uriInfo,
+            @Suspended final AsyncResponse ar) {
+        try (var jsonBody = new JsonChildBody(body)) {
+            completeDataPOST(server.dataPOST(jsonBody, QueryParams.normalize(uriInfo)), uriInfo, ar);
+        }
+    }
+
+    /**
+     * Create a data resource in target.
+     *
+     * @param identifier path to target
+     * @param body data node for put to config DS
+     * @param uriInfo URI info
+     * @param ar {@link AsyncResponse} which needs to be completed
+     */
+    @POST
+    @Path("/data/{identifier:.+}")
+    @Consumes({
+        MediaTypes.APPLICATION_YANG_DATA_JSON,
+        MediaType.APPLICATION_JSON,
+    })
+    public void postDataJSON(@Encoded @PathParam("identifier") final String identifier, final InputStream body,
+            @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
+        completeDataPOST(server.dataPOST(identifier, new JsonDataPostBody(body), QueryParams.normalize(uriInfo)),
+            uriInfo, ar);
+    }
+
+    /**
+     * Create a top-level data resource.
+     *
+     * @param body data node for put to config DS
+     * @param uriInfo URI info
+     * @param ar {@link AsyncResponse} which needs to be completed
+     */
+    @POST
+    @Path("/data")
+    @Consumes({
+        MediaTypes.APPLICATION_YANG_DATA_XML,
+        MediaType.APPLICATION_XML,
+        MediaType.TEXT_XML
+    })
+    public void postDataXML(final InputStream body, @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
+        try (var xmlBody = new XmlChildBody(body)) {
+            completeDataPOST(server.dataPOST(xmlBody, QueryParams.normalize(uriInfo)), uriInfo, ar);
+        }
+    }
+
+    /**
+     * Create a data resource in target.
+     *
+     * @param identifier path to target
+     * @param body data node for put to config DS
+     * @param uriInfo URI info
+     * @param ar {@link AsyncResponse} which needs to be completed
+     */
+    @POST
+    @Path("/data/{identifier:.+}")
+    @Consumes({
+        MediaTypes.APPLICATION_YANG_DATA_XML,
+        MediaType.APPLICATION_XML,
+        MediaType.TEXT_XML
+    })
+    public void postDataXML(@Encoded @PathParam("identifier") final String identifier, final InputStream body,
+            @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
+        completeDataPOST(server.dataPOST(identifier, new XmlDataPostBody(body), QueryParams.normalize(uriInfo)),
+            uriInfo, ar);
+    }
+
+    private static void completeDataPOST(final RestconfFuture<? extends DataPostResult> future, final UriInfo uriInfo,
+            final AsyncResponse ar) {
+        future.addCallback(new JaxRsRestconfCallback<DataPostResult>(ar) {
+            @Override
+            Response transform(final DataPostResult result) {
+                if (result instanceof CreateResource createResource) {
+                    return Response.created(uriInfo.getBaseUriBuilder()
+                            .path("data")
+                            .path(createResource.createdPath())
+                            .build())
+                        .build();
+                }
+                if (result instanceof InvokeOperation invokeOperation) {
+                    final var output = invokeOperation.output();
+                    return output == null ? Response.status(Status.NO_CONTENT).build()
+                        : Response.status(Status.OK).entity(output).build();
+                }
+                LOG.error("Unhandled result {}", result);
+                return Response.serverError().build();
+            }
+        });
+    }
+
     /**
      * Replace the data store.
      *
index dc00ea71746de94e8c63d4c41ac0f558efec4731..9f61964b27f406a7b1935d9b21aab00303711cbb 100644 (file)
@@ -366,7 +366,7 @@ public abstract class RestconfStrategy {
      * @param insert  {@link Insert}
      * @return A {@link RestconfFuture}
      */
-    public final RestconfFuture<CreateResource> postData(final YangInstanceIdentifier path,
+    public final @NonNull RestconfFuture<CreateResource> postData(final YangInstanceIdentifier path,
             final NormalizedNode data, final @Nullable Insert insert) {
         final ListenableFuture<? extends CommitInfo> future;
         if (insert != null) {
index 3e139a01495a9de3edcce7cafa4dd5c6731465be..05f5eccee48ff64da58185c7ce94d3c2f0c97eaa 100644 (file)
@@ -10,6 +10,8 @@ package org.opendaylight.restconf.server.api;
 import static java.util.Objects.requireNonNull;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload;
 
 /**
  * Result of a {@code POST} request as defined in
@@ -18,8 +20,8 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
 @NonNullByDefault
 public sealed interface DataPostResult {
     /**
-     * Result of a {@code POST} request as defined in
-     * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.4.1">RFC8040 Create ResourceMode</a>.
+     * Result of a {@code POST} request in as defined in
+     * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.4.1">RFC8040 Create Resource Mode</a>.
      *
      * @param createdPath API path of the newly-created resource
      */
@@ -29,4 +31,14 @@ public sealed interface DataPostResult {
             requireNonNull(createdPath);
         }
     }
+
+    /**
+     * Result of a {@code POST} request as defined in
+     * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.4.2">RFC8040 Invoke Operation Mode</a>.
+     *
+     * @param output Non-empty operation output, or {@code null}
+     */
+    record InvokeOperation(@Nullable NormalizedNodePayload output) implements DataPostResult {
+        public static final InvokeOperation EMPTY = new InvokeOperation(null);
+    }
 }
index bb2a8b0af4cb3473a102cfdad59b55a39cace51e..4b8212d9eb190d50cb25cb12f7a1138cb0ce3fe7 100644 (file)
@@ -15,10 +15,13 @@ 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.ChildBody;
+import org.opendaylight.restconf.nb.rfc8040.databind.DataPostBody;
 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.api.DataPostResult.CreateResource;
 import org.opendaylight.restconf.server.spi.OperationOutput;
 import org.opendaylight.yangtools.yang.common.Empty;
 
@@ -92,6 +95,11 @@ public interface RestconfServer {
      */
     RestconfFuture<PatchStatusContext> dataPATCH(String identifier, PatchBody body);
 
+    RestconfFuture<CreateResource> dataPOST(ChildBody body, Map<String, String> queryParameters);
+
+    RestconfFuture<? extends DataPostResult> dataPOST(String identifier, DataPostBody body,
+        Map<String, String> queryParameters);
+
     /**
      * Replace the data store.
      *
index 3d59d57028f33a4748414588c8e20038db9b65be..c53a127f80cd0acb716a6a7e99c39d35848bec6e 100644 (file)
@@ -32,6 +32,7 @@ import org.opendaylight.restconf.api.query.InsertParam;
 import org.opendaylight.restconf.api.query.RestconfQueryParam;
 import org.opendaylight.restconf.api.query.WithDefaultsParam;
 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
+import org.opendaylight.restconf.nb.rfc8040.Insert;
 import org.opendaylight.restconf.nb.rfc8040.ReceiveEventsParams;
 import org.opendaylight.restconf.nb.rfc8040.legacy.InstanceIdentifierContext;
 import org.opendaylight.restconf.nb.rfc8040.legacy.QueryParameters;
@@ -77,12 +78,11 @@ public class QueryParamsTest {
     public void checkParametersTypesNegativeTest() {
         assertInvalidIAE(ReceiveEventsParams::ofQueryParameters);
         assertUnknownParam(QueryParams::newReadDataParams);
-        assertUnknownParam(uriInfo -> QueryParams.parseInsert(mock(EffectiveModelContext.class), uriInfo));
+        assertInvalidIAE(queryParams -> Insert.ofQueryParameters(mock(EffectiveModelContext.class), queryParams));
 
         assertInvalidIAE(ReceiveEventsParams::ofQueryParameters, ContentParam.ALL);
         assertInvalidParam(QueryParams::newReadDataParams, InsertParam.LAST);
-        assertInvalidParam(
-            uriInfo -> QueryParams.parseInsert(mock(EffectiveModelContext.class), uriInfo),
+        assertInvalidIAE(queryParams -> Insert.ofQueryParameters(mock(EffectiveModelContext.class), queryParams),
             ContentParam.ALL);
     }
 
index 0296f77113e7d94ced675307fcb9c9ccd1d18d75..78fac1546b4d2e667c69ad28bc2c569159ae7c43 100644 (file)
@@ -15,7 +15,9 @@ import static org.mockito.Mockito.doReturn;
 
 import com.google.common.util.concurrent.Futures;
 import javax.ws.rs.container.AsyncResponse;
+import javax.ws.rs.core.MultivaluedHashMap;
 import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -29,7 +31,6 @@ import org.opendaylight.mdsal.dom.api.DOMRpcService;
 import org.opendaylight.mdsal.dom.spi.SimpleDOMActionResult;
 import org.opendaylight.restconf.nb.rfc8040.AbstractInstanceIdentifierTest;
 import org.opendaylight.restconf.nb.rfc8040.databind.DatabindContext;
-import org.opendaylight.restconf.nb.rfc8040.databind.DatabindProvider;
 import org.opendaylight.yangtools.yang.common.QName;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
 import org.opendaylight.yangtools.yang.data.impl.schema.Builders;
@@ -39,6 +40,8 @@ import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier.Absol
 public class Netconf799Test extends AbstractInstanceIdentifierTest {
     private static final QName OUTPUT_QNAME = QName.create(CONT_QNAME, "output");
 
+    @Mock
+    private UriInfo uriInfo;
     @Mock
     private DOMDataBroker dataBroker;
     @Mock
@@ -58,18 +61,17 @@ public class Netconf799Test extends AbstractInstanceIdentifierTest {
             Builders.containerBuilder().withNodeIdentifier(NodeIdentifier.create(OUTPUT_QNAME)).build())))
             .when(actionService).invokeAction(eq(Absolute.of(CONT_QNAME, CONT1_QNAME, RESET_QNAME)), any(), any());
 
-        final DatabindProvider databindProvider = () -> DatabindContext.ofModel(IID_SCHEMA);
-        final var dataService = new RestconfDataServiceImpl(databindProvider,
-            new MdsalRestconfServer(databindProvider, dataBroker, rpcService, actionService, mountPointService));
-
+        final var restconf = new RestconfImpl(new MdsalRestconfServer(
+            () -> DatabindContext.ofModel(IID_SCHEMA), dataBroker, rpcService, actionService, mountPointService));
+        doReturn(new MultivaluedHashMap<>()).when(uriInfo).getQueryParameters();
         doReturn(true).when(asyncResponse).resume(captor.capture());
-        dataService.postDataJSON("instance-identifier-module:cont/cont1/reset",
+        restconf.postDataJSON("instance-identifier-module:cont/cont1/reset",
             stringInputStream("""
             {
               "instance-identifier-module:input": {
                 "delay": 600
               }
-            }"""), null, asyncResponse);
+            }"""), uriInfo, asyncResponse);
         final var response = captor.getValue();
         assertEquals(204, response.getStatus());
         assertNull(response.getEntity());
index 7f08a96c450b94d0b85a40303ff47fb46d7436cc..a47b61c7071bfc4388e270eaa2e2c84b811befec 100644 (file)
@@ -10,84 +10,48 @@ package org.opendaylight.restconf.nb.rfc8040.rests.services.impl;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.mock;
 import static org.opendaylight.yangtools.util.concurrent.FluentFutures.immediateFalseFluentFuture;
 
 import java.net.URI;
-import javax.ws.rs.container.AsyncResponse;
 import javax.ws.rs.core.MultivaluedHashMap;
-import javax.ws.rs.core.Response;
 import javax.ws.rs.core.UriBuilder;
-import javax.ws.rs.core.UriInfo;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
+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.MockitoJUnitRunner;
+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.DOMActionService;
-import org.opendaylight.mdsal.dom.api.DOMDataBroker;
 import org.opendaylight.mdsal.dom.api.DOMDataTreeReadWriteTransaction;
-import org.opendaylight.mdsal.dom.api.DOMMountPointService;
-import org.opendaylight.mdsal.dom.api.DOMRpcService;
-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.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
 import org.opendaylight.yangtools.yang.data.impl.schema.Builders;
 
-@RunWith(MockitoJUnitRunner.StrictStubs.class)
-public class RestconfDataServiceImplTest extends AbstractJukeboxTest {
+@ExtendWith(MockitoExtension.class)
+class RestconfDataServiceImplTest extends AbstractRestconfTest {
     @Mock
-    private UriInfo uriInfo;
-    @Mock
-    private DOMDataTreeReadWriteTransaction readWrite;
-    @Mock
-    private DOMMountPointService mountPointService;
-    @Mock
-    private DOMDataBroker mountDataBroker;
-    @Mock
-    private DOMActionService actionService;
-    @Mock
-    private DOMRpcService rpcService;
-    @Mock
-    private AsyncResponse asyncResponse;
-    @Captor
-    private ArgumentCaptor<Response> responseCaptor;
-
-    private RestconfDataServiceImpl dataService;
-
-    @Before
-    public void setUp() throws Exception {
-        doReturn(CommitInfo.emptyFluentFuture()).when(readWrite).commit();
+    private DOMDataTreeReadWriteTransaction tx;
 
-        final var dataBroker = mock(DOMDataBroker.class);
-        doReturn(readWrite).when(dataBroker).newReadWriteTransaction();
-
-        final DatabindProvider databindProvider = () -> DatabindContext.ofModel(JUKEBOX_SCHEMA);
-        dataService = new RestconfDataServiceImpl(databindProvider,
-            new MdsalRestconfServer(databindProvider, dataBroker, rpcService, actionService, mountPointService));
+    @BeforeEach
+    void beforeEach() {
+        doReturn(CommitInfo.emptyFluentFuture()).when(tx).commit();
+        doReturn(tx).when(dataBroker).newReadWriteTransaction();
     }
 
     @Test
-    public void testPostData() {
+    void testPostData() {
         doReturn(new MultivaluedHashMap<>()).when(uriInfo).getQueryParameters();
-        doReturn(immediateFalseFluentFuture()).when(readWrite).exists(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID);
-        doNothing().when(readWrite).put(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID,
+        doReturn(immediateFalseFluentFuture()).when(tx).exists(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID);
+        doNothing().when(tx).put(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID,
             Builders.containerBuilder().withNodeIdentifier(new NodeIdentifier(JUKEBOX_QNAME)).build());
         doReturn(UriBuilder.fromUri("http://localhost:8181/rests/")).when(uriInfo).getBaseUriBuilder();
 
-        final var captor = ArgumentCaptor.forClass(Response.class);
-        doReturn(true).when(asyncResponse).resume(captor.capture());
-        dataService.postDataJSON(stringInputStream("""
+        doReturn(true).when(asyncResponse).resume(responseCaptor.capture());
+        restconf.postDataJSON(stringInputStream("""
             {
               "example-jukebox:jukebox" : {
               }
             }"""), uriInfo, asyncResponse);
-        final var response = captor.getValue();
+        final var response = responseCaptor.getValue();
         assertEquals(201, response.getStatus());
         assertEquals(URI.create("http://localhost:8181/rests/data/example-jukebox:jukebox"), response.getLocation());
     }
@@ -96,23 +60,21 @@ public class RestconfDataServiceImplTest extends AbstractJukeboxTest {
     public void testPostMapEntryData() {
         doReturn(new MultivaluedHashMap<>()).when(uriInfo).getQueryParameters();
         final var node = PLAYLIST_IID.node(BAND_ENTRY.name());
-        doReturn(immediateFalseFluentFuture()).when(readWrite).exists(LogicalDatastoreType.CONFIGURATION, node);
-        doNothing().when(readWrite).put(LogicalDatastoreType.CONFIGURATION, node, BAND_ENTRY);
+        doReturn(immediateFalseFluentFuture()).when(tx).exists(LogicalDatastoreType.CONFIGURATION, node);
+        doNothing().when(tx).put(LogicalDatastoreType.CONFIGURATION, node, BAND_ENTRY);
         doReturn(UriBuilder.fromUri("http://localhost:8181/rests/")).when(uriInfo).getBaseUriBuilder();
 
-        final var captor = ArgumentCaptor.forClass(Response.class);
-        doReturn(true).when(asyncResponse).resume(captor.capture());
-        dataService.postDataJSON("example-jukebox:jukebox", stringInputStream("""
+        doReturn(true).when(asyncResponse).resume(responseCaptor.capture());
+        restconf.postDataJSON("example-jukebox:jukebox", stringInputStream("""
             {
               "example-jukebox:playlist" : {
                 "name" : "name of band",
                 "description" : "band description"
               }
             }"""), uriInfo, asyncResponse);
-        final var response = captor.getValue();
+        final var response = responseCaptor.getValue();
         assertEquals(201, response.getStatus());
         assertEquals(URI.create("http://localhost:8181/rests/data/example-jukebox:jukebox/playlist=name%20of%20band"),
             response.getLocation());
     }
-
 }