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