Migrate PUT operations on /data to MdsalRestconfServer/RestconfImpl.
This temporarily duplicates Insert parsing logic, which will be
eliminated in a follow-up patch.
JIRA: NETCONF-773
Change-Id: I8692fede3238cdcca283dfc7d9bc8e123b18aea7
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
package org.opendaylight.restconf.nb.rfc8040;
import static java.util.Objects.requireNonNull;
+import static org.opendaylight.restconf.nb.rfc8040.ReceiveEventsParams.optionalParam;
import com.google.common.annotations.Beta;
import com.google.common.base.MoreObjects;
+import java.util.Map;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.opendaylight.restconf.api.query.InsertParam;
import org.opendaylight.restconf.api.query.PointParam;
+import org.opendaylight.restconf.nb.rfc8040.utils.parser.YangInstanceIdentifierDeserializer;
import org.opendaylight.yangtools.concepts.Immutable;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
+import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
/**
* Parser and holder of query parameters from uriInfo for data and datastore modification operations.
this.pointArg = pointArg;
}
+ /**
+ * Return an {@link Insert} parameter for specified query parameters.
+ *
+ * @param queryParameters Parameters and their values
+ * @return An {@link Insert}, or {@code null} if no insert information is present
+ * @throws NullPointerException if any argument is {@code null}
+ * @throws IllegalArgumentException if the parameters are invalid
+ */
+ public static @Nullable Insert ofQueryParameters(final EffectiveModelContext modelContext,
+ final Map<String, String> queryParameters) {
+ InsertParam insert = null;
+ PointParam point = null;
+
+ for (var entry : queryParameters.entrySet()) {
+ final var paramName = entry.getKey();
+ final var paramValue = entry.getValue();
+
+ switch (paramName) {
+ case InsertParam.uriName:
+ insert = optionalParam(InsertParam::forUriValue, paramName, paramValue);
+ break;
+ case PointParam.uriName:
+ point = optionalParam(PointParam::forUriValue, paramName, paramValue);
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid parameter: " + paramName);
+ }
+ }
+
+ 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());
+ }
+
public static @Nullable Insert forParams(final @Nullable InsertParam insert, final @Nullable PointParam point,
final PointParser pointParser) {
if (insert == null) {
/**
* Return {@link ReceiveEventsParams} for specified query parameters.
+ *
* @param queryParameters Parameters and their values
* @return A {@link ReceiveEventsParams}
+ * @throws NullPointerException if {@code queryParameters} is {@code null}
+ * @throws IllegalArgumentException if the parameters are invalid
*/
public static @NonNull ReceiveEventsParams ofQueryParameters(final Map<String, String> queryParameters) {
StartTimeParam startTime = null;
return helper.toString();
}
- private static <T> @Nullable T optionalParam(final Function<String, @NonNull T> factory, final String name,
+ static <T> @Nullable T optionalParam(final Function<String, @NonNull T> factory, final String name,
final String value) {
try {
return factory.apply(requireNonNull(value));
import com.google.common.annotations.Beta;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableMap;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
// Utility class
}
+ /**
+ * Normalize query parameters from an {@link UriInfo}.
+ *
+ * @param uriInfo An {@link UriInfo}
+ * @return Normalized query parameters
+ * @throws NullPointerException if {@code uriInfo} is {@code null}
+ * @throws IllegalArgumentException if there are multiple values for a parameter
+ */
+ public static @NonNull ImmutableMap<String, String> normalize(final UriInfo uriInfo) {
+ final var builder = ImmutableMap.<String, String>builder();
+ for (var entry : uriInfo.getQueryParameters().entrySet()) {
+ final var values = entry.getValue();
+ switch (values.size()) {
+ case 0:
+ // No-op
+ break;
+ case 1:
+ builder.put(entry.getKey(), values.get(0));
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "Parameter " + entry.getKey() + " can appear at most once in request URI");
+ }
+ }
+ return builder.build();
+ }
+
public static QueryParameters newQueryParameters(final ReadDataParams params,
final InstanceIdentifierContext identifier) {
final var fields = params.fields();
import java.net.URI;
import java.util.Comparator;
import java.util.List;
+import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.CancellationException;
import javax.inject.Inject;
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.Insert;
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.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.DataPutResult;
import org.opendaylight.restconf.server.api.OperationsContent;
import org.opendaylight.restconf.server.api.RestconfServer;
import org.opendaylight.restconf.server.spi.OperationInput;
* @param schemaPath schema path of data
* @return {@link DOMActionResult}
*/
- private static RestconfFuture<DOMActionResult> dataInvokePOST(final ContainerNode data,
- final Absolute schemaPath, final YangInstanceIdentifier yangIId, final DOMMountPoint mountPoint) {
+ private static RestconfFuture<DOMActionResult> dataInvokePOST(final ContainerNode data, final Absolute schemaPath,
+ final YangInstanceIdentifier yangIId, final DOMMountPoint mountPoint) {
final var actionService = mountPoint.getService(DOMActionService.class);
return actionService.isPresent() ? dataInvokePOST(data, schemaPath, yangIId, actionService.orElseThrow())
: RestconfFuture.failed(new RestconfDocumentedException("DOMActionService is missing."));
}
+ @Override
+ public RestconfFuture<DataPutResult> dataPUT(final ResourceBody body, final Map<String, String> query) {
+ return dataPUT(bindRequestRoot(), body, query);
+ }
+
+ @Override
+ public RestconfFuture<DataPutResult> dataPUT(final String identifier, final ResourceBody body,
+ final Map<String, String> queryParameters) {
+ return dataPUT(bindRequestPath(identifier), body, queryParameters);
+ }
+
+ private @NonNull RestconfFuture<DataPutResult> dataPUT(final InstanceIdentifierContext reqPath,
+ final ResourceBody body, final Map<String, String> queryParameters) {
+ final Insert insert;
+ try {
+ insert = Insert.ofQueryParameters(reqPath.getSchemaContext(), queryParameters);
+ } catch (IllegalArgumentException e) {
+ return RestconfFuture.failed(new RestconfDocumentedException(e.getMessage(),
+ ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, e));
+ }
+ final var req = bindResourceRequest(reqPath, body);
+ return req.strategy().putData(req.path(), req.data(), insert);
+ }
+
@Override
public OperationsContent operationsGET() {
return operationsGET(databindProvider.currentContext().modelContext());
import javax.ws.rs.Consumes;
import javax.ws.rs.Encoded;
import javax.ws.rs.POST;
-import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.container.AsyncResponse;
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.JsonResourceBody;
import org.opendaylight.restconf.nb.rfc8040.databind.OperationInputBody;
-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.XmlResourceBody;
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.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.data.api.YangInstanceIdentifier;
this.server = requireNonNull(server);
}
- /**
- * Replace the data store.
- *
- * @param uriInfo request URI information
- * @param body data node for put to config DS
- * @param ar {@link AsyncResponse} which needs to be completed
- */
- @PUT
- @Path("/data")
- @Consumes({
- MediaTypes.APPLICATION_YANG_DATA_JSON,
- MediaType.APPLICATION_JSON,
- })
- public void putDataJSON(@Context final UriInfo uriInfo, final InputStream body, @Suspended final AsyncResponse ar) {
- try (var jsonBody = new JsonResourceBody(body)) {
- putData(null, uriInfo, jsonBody, ar);
- }
- }
-
- /**
- * Create or replace the target data resource.
- *
- * @param identifier path to target
- * @param uriInfo request URI information
- * @param body data node for put to config DS
- * @param ar {@link AsyncResponse} which needs to be completed
- */
- @PUT
- @Path("/data/{identifier:.+}")
- @Consumes({
- MediaTypes.APPLICATION_YANG_DATA_JSON,
- MediaType.APPLICATION_JSON,
- })
- public void putDataJSON(@Encoded @PathParam("identifier") final String identifier,
- @Context final UriInfo uriInfo, final InputStream body, @Suspended final AsyncResponse ar) {
- try (var jsonBody = new JsonResourceBody(body)) {
- putData(identifier, uriInfo, jsonBody, ar);
- }
- }
-
- /**
- * Replace the data store.
- *
- * @param uriInfo request URI information
- * @param body data node for put to config DS
- * @param ar {@link AsyncResponse} which needs to be completed
- */
- @PUT
- @Path("/data")
- @Consumes({
- MediaTypes.APPLICATION_YANG_DATA_XML,
- MediaType.APPLICATION_XML,
- MediaType.TEXT_XML
- })
- public void putDataXML(@Context final UriInfo uriInfo, final InputStream body, @Suspended final AsyncResponse ar) {
- try (var xmlBody = new XmlResourceBody(body)) {
- putData(null, uriInfo, xmlBody, ar);
- }
- }
-
- /**
- * Create or replace the target data resource.
- *
- * @param identifier path to target
- * @param uriInfo request URI information
- * @param body data node for put to config DS
- * @param ar {@link AsyncResponse} which needs to be completed
- */
- @PUT
- @Path("/data/{identifier:.+}")
- @Consumes({
- MediaTypes.APPLICATION_YANG_DATA_XML,
- MediaType.APPLICATION_XML,
- MediaType.TEXT_XML
- })
- public void putDataXML(@Encoded @PathParam("identifier") final String identifier,
- @Context final UriInfo uriInfo, final InputStream body, @Suspended final AsyncResponse ar) {
- try (var xmlBody = new XmlResourceBody(body)) {
- putData(identifier, uriInfo, xmlBody, ar);
- }
- }
-
- private void putData(final @Nullable String identifier, final UriInfo uriInfo, final ResourceBody body,
- final AsyncResponse ar) {
- final var reqPath = server.bindRequestPath(identifier);
- final var insert = QueryParams.parseInsert(reqPath.getSchemaContext(), uriInfo);
- final var req = server.bindResourceRequest(reqPath, body);
-
- req.strategy().putData(req.path(), req.data(), insert).addCallback(new JaxRsRestconfCallback<>(ar) {
- @Override
- Response transform(final CreateOrReplaceResult result) {
- return switch (result) {
- // Note: no Location header, as it matches the request path
- case CREATED -> Response.status(Status.CREATED).build();
- case REPLACED -> Response.noContent().build();
- };
- }
- });
- }
-
/**
* Create a top-level data resource.
*
import javax.ws.rs.NotFoundException;
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 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.DataPutResult;
import org.opendaylight.restconf.server.api.OperationsContent;
import org.opendaylight.restconf.server.api.RestconfServer;
import org.opendaylight.restconf.server.spi.OperationOutput;
});
}
+ /**
+ * Replace the data store.
+ *
+ * @param uriInfo request URI information
+ * @param body data node for put to config DS
+ * @param ar {@link AsyncResponse} which needs to be completed
+ */
+ @PUT
+ @Path("/data")
+ @Consumes({
+ MediaTypes.APPLICATION_YANG_DATA_JSON,
+ MediaType.APPLICATION_JSON,
+ })
+ public void dataJsonPUT(@Context final UriInfo uriInfo, final InputStream body, @Suspended final AsyncResponse ar) {
+ try (var jsonBody = new JsonResourceBody(body)) {
+ completeDataPUT(server.dataPUT(jsonBody, QueryParams.normalize(uriInfo)), ar);
+ }
+ }
+
+ /**
+ * Create or replace the target data resource.
+ *
+ * @param identifier path to target
+ * @param uriInfo request URI information
+ * @param body data node for put to config DS
+ * @param ar {@link AsyncResponse} which needs to be completed
+ */
+ @PUT
+ @Path("/data/{identifier:.+}")
+ @Consumes({
+ MediaTypes.APPLICATION_YANG_DATA_JSON,
+ MediaType.APPLICATION_JSON,
+ })
+ public void dataJsonPUT(@Encoded @PathParam("identifier") final String identifier,
+ @Context final UriInfo uriInfo, final InputStream body, @Suspended final AsyncResponse ar) {
+ try (var jsonBody = new JsonResourceBody(body)) {
+ completeDataPUT(server.dataPUT(identifier, jsonBody, QueryParams.normalize(uriInfo)), ar);
+ }
+ }
+
+ /**
+ * Replace the data store.
+ *
+ * @param uriInfo request URI information
+ * @param body data node for put to config DS
+ * @param ar {@link AsyncResponse} which needs to be completed
+ */
+ @PUT
+ @Path("/data")
+ @Consumes({
+ MediaTypes.APPLICATION_YANG_DATA_XML,
+ MediaType.APPLICATION_XML,
+ MediaType.TEXT_XML
+ })
+ public void dataXmlPUT(@Context final UriInfo uriInfo, final InputStream body, @Suspended final AsyncResponse ar) {
+ try (var xmlBody = new XmlResourceBody(body)) {
+ completeDataPUT(server.dataPUT(xmlBody, QueryParams.normalize(uriInfo)), ar);
+ }
+ }
+
+ /**
+ * Create or replace the target data resource.
+ *
+ * @param identifier path to target
+ * @param uriInfo request URI information
+ * @param body data node for put to config DS
+ * @param ar {@link AsyncResponse} which needs to be completed
+ */
+ @PUT
+ @Path("/data/{identifier:.+}")
+ @Consumes({
+ MediaTypes.APPLICATION_YANG_DATA_XML,
+ MediaType.APPLICATION_XML,
+ MediaType.TEXT_XML
+ })
+ public void dataXmlPUT(@Encoded @PathParam("identifier") final String identifier,
+ @Context final UriInfo uriInfo, final InputStream body, @Suspended final AsyncResponse ar) {
+ try (var xmlBody = new XmlResourceBody(body)) {
+ completeDataPUT(server.dataPUT(identifier, xmlBody, QueryParams.normalize(uriInfo)), ar);
+ }
+ }
+
+ private static void completeDataPUT(final RestconfFuture<DataPutResult> future, final AsyncResponse ar) {
+ future.addCallback(new JaxRsRestconfCallback<>(ar) {
+ @Override
+ Response transform(final DataPutResult result) {
+ return switch (result) {
+ // Note: no Location header, as it matches the request path
+ case CREATED -> Response.status(Status.CREATED).build();
+ case REPLACED -> Response.noContent().build();
+ };
+ }
+ });
+ }
+
/**
* List RPC and action operations in RFC7951 format.
*
import org.opendaylight.restconf.common.patch.PatchStatusContext;
import org.opendaylight.restconf.common.patch.PatchStatusEntity;
import org.opendaylight.restconf.nb.rfc8040.Insert;
+import org.opendaylight.restconf.server.api.DataPutResult;
import org.opendaylight.restconf.server.spi.OperationInput;
import org.opendaylight.restconf.server.spi.OperationOutput;
import org.opendaylight.restconf.server.spi.RpcImplementation;
// FIXME: it seems the first three operations deal with lifecycle of a transaction, while others invoke various
// operations. This should be handled through proper allocation indirection.
public abstract class RestconfStrategy {
- /**
- * Result of a {@code PUT} request as defined in
- * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.5">RFC8040 section 4.5</a>. The definition makes it
- * clear that the logical operation is {@code create-or-replace}.
- */
- public enum CreateOrReplaceResult {
- /**
- * A new resource has been created.
- */
- CREATED,
- /*
- * An existing resources has been replaced.
- */
- REPLACED;
- }
-
private static final Logger LOG = LoggerFactory.getLogger(RestconfStrategy.class);
private final @NonNull EffectiveModelContext modelContext;
* @param path path of data
* @param data data
* @param insert {@link Insert}
- * @return A {@link CreateOrReplaceResult}
+ * @return A {@link DataPutResult}
*/
- public final RestconfFuture<CreateOrReplaceResult> putData(final YangInstanceIdentifier path,
+ public final RestconfFuture<DataPutResult> putData(final YangInstanceIdentifier path,
final NormalizedNode data, final @Nullable Insert insert) {
final var exists = TransactionUtil.syncAccess(exists(path), path);
commitFuture = replaceAndCommit(prepareWriteExecution(), path, data);
}
- final var ret = new SettableRestconfFuture<CreateOrReplaceResult>();
+ final var ret = new SettableRestconfFuture<DataPutResult>();
Futures.addCallback(commitFuture, new FutureCallback<CommitInfo>() {
@Override
public void onSuccess(final CommitInfo result) {
- ret.set(exists ? CreateOrReplaceResult.REPLACED : CreateOrReplaceResult.CREATED);
+ ret.set(exists ? DataPutResult.REPLACED : DataPutResult.CREATED);
}
@Override
import javax.ws.rs.sse.SseEventSink;
import javax.xml.xpath.XPathExpressionException;
import org.opendaylight.restconf.nb.rfc8040.ReceiveEventsParams;
+import org.opendaylight.restconf.nb.rfc8040.databind.jaxrs.QueryParams;
import org.opendaylight.restconf.server.spi.RestconfStream;
import org.opendaylight.restconf.server.spi.RestconfStream.EncodingName;
import org.slf4j.Logger;
throw new NotFoundException("No such stream: " + streamName);
}
- final var queryParameters = ImmutableMap.<String, String>builder();
- for (var entry : uriInfo.getQueryParameters().entrySet()) {
- final var values = entry.getValue();
- switch (values.size()) {
- case 0:
- // No-op
- break;
- case 1:
- queryParameters.put(entry.getKey(), values.get(0));
- break;
- default:
- throw new BadRequestException(
- "Parameter " + entry.getKey() + " can appear at most once in request URI");
- }
+ final ImmutableMap<String, String> queryParameters;
+ try {
+ queryParameters = QueryParams.normalize(uriInfo);
+ } catch (IllegalArgumentException e) {
+ throw new BadRequestException(e.getMessage(), e);
}
final ReceiveEventsParams params;
try {
- params = ReceiveEventsParams.ofQueryParameters(queryParameters.build());
+ params = ReceiveEventsParams.ofQueryParameters(queryParameters);
} catch (IllegalArgumentException e) {
throw new BadRequestException(e.getMessage(), e);
}
--- /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.server.api;
+
+/**
+ * Result of a {@code PUT} request as defined in
+ * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.5">RFC8040 section 4.5</a>. The definition makes it
+ * clear that the logical operation is {@code create-or-replace}.
+ */
+public enum DataPutResult {
+ /**
+ * A new resource has been created.
+ */
+ CREATED,
+ /*
+ * An existing resources has been replaced.
+ */
+ REPLACED;
+}
\ No newline at end of file
package org.opendaylight.restconf.server.api;
import java.net.URI;
+import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.opendaylight.restconf.api.ApiPath;
*/
RestconfFuture<PatchStatusContext> dataPATCH(String identifier, PatchBody body);
+ /**
+ * Replace the data store.
+ *
+ * @param body data node for put to config DS
+ * @param queryParameters Query parameters
+ * @return A {@link RestconfFuture} completing with {@link DataPutResult}
+ */
+ RestconfFuture<DataPutResult> dataPUT(ResourceBody body, Map<String, String> queryParameters);
+
+ /**
+ * Create or replace a data store resource.
+ *
+ * @param identifier resource identifier
+ * @param body data node for put to config DS
+ * @param queryParameters Query parameters
+ * @return A {@link RestconfFuture} completing with {@link DataPutResult}
+ */
+ RestconfFuture<DataPutResult> dataPUT(String identifier, ResourceBody body, Map<String, String> queryParameters);
+
/**
* Return the set of supported RPCs supported by {@link #operationsPOST(URI, String, OperationInputBody)}.
*
--- /dev/null
+/*
+ * Copyright (c) 2023 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.restconf.nb.rfc8040.rests.services.impl;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.opendaylight.yangtools.util.concurrent.FluentFutures.immediateTrueFluentFuture;
+
+import java.util.Optional;
+import javax.ws.rs.core.MultivaluedHashMap;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+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.DOMDataBroker;
+import org.opendaylight.mdsal.dom.api.DOMDataTreeReadTransaction;
+import org.opendaylight.mdsal.dom.api.DOMDataTreeReadWriteTransaction;
+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;
+
+@ExtendWith(MockitoExtension.class)
+class RestconfDataPutTest extends AbstractRestconfTest {
+ private final MultivaluedMap<String, String> queryParamenters = new MultivaluedHashMap<>();
+
+ @Mock
+ private DOMDataTreeReadTransaction readTx;
+ @Mock
+ private DOMDataTreeReadWriteTransaction rwTx;
+
+ @BeforeEach
+ void beforeEach() {
+ doReturn(queryParamenters).when(uriInfo).getQueryParameters();
+ doReturn(readTx).when(dataBroker).newReadOnlyTransaction();
+ doReturn(rwTx).when(dataBroker).newReadWriteTransaction();
+ doReturn(CommitInfo.emptyFluentFuture()).when(rwTx).commit();
+ }
+
+ @Test
+ void testPutData() {
+ doReturn(immediateTrueFluentFuture()).when(readTx).exists(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID);
+ doNothing().when(rwTx).put(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID, EMPTY_JUKEBOX);
+
+ doReturn(true).when(asyncResponse).resume(responseCaptor.capture());
+ restconf.dataJsonPUT("example-jukebox:jukebox", uriInfo, stringInputStream("""
+ {
+ "example-jukebox:jukebox" : {
+ "player": {
+ "gap": "0.2"
+ }
+ }
+ }"""), asyncResponse);
+ final var response = responseCaptor.getValue();
+ assertEquals(Response.Status.NO_CONTENT.getStatusCode(), response.getStatus());
+ }
+
+ @Test
+ void testPutDataWithMountPoint() {
+ doReturn(immediateTrueFluentFuture()).when(readTx).exists(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID);
+ doNothing().when(rwTx).put(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID, EMPTY_JUKEBOX);
+ doReturn(Optional.of(mountPoint)).when(mountPointService).getMountPoint(any());
+ doReturn(Optional.of(FixedDOMSchemaService.of(JUKEBOX_SCHEMA))).when(mountPoint)
+ .getService(DOMSchemaService.class);
+ doReturn(Optional.of(dataBroker)).when(mountPoint).getService(DOMDataBroker.class);
+ doReturn(Optional.of(rpcService)).when(mountPoint).getService(DOMRpcService.class);
+ doReturn(Optional.empty()).when(mountPoint).getService(NetconfDataTreeService.class);
+
+ doReturn(true).when(asyncResponse).resume(responseCaptor.capture());
+ restconf.dataXmlPUT("example-jukebox:jukebox/yang-ext:mount/example-jukebox:jukebox",
+ uriInfo, stringInputStream("""
+ <jukebox xmlns="http://example.com/ns/example-jukebox">
+ <player>
+ <gap>0.2</gap>
+ </player>
+ </jukebox>"""), asyncResponse);
+ final var response = responseCaptor.getValue();
+ assertEquals(Response.Status.NO_CONTENT.getStatusCode(), response.getStatus());
+ }
+}
package org.opendaylight.restconf.nb.rfc8040.rests.services.impl;
import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.mockito.ArgumentMatchers.any;
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 static org.opendaylight.yangtools.util.concurrent.FluentFutures.immediateTrueFluentFuture;
import java.net.URI;
-import java.util.Optional;
-import java.util.Set;
import javax.ws.rs.container.AsyncResponse;
import javax.ws.rs.core.MultivaluedHashMap;
-import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
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.DOMDataTreeReadTransaction;
import org.opendaylight.mdsal.dom.api.DOMDataTreeReadWriteTransaction;
-import org.opendaylight.mdsal.dom.api.DOMMountPoint;
import org.opendaylight.mdsal.dom.api.DOMMountPointService;
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.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;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
import org.opendaylight.yangtools.yang.data.impl.schema.Builders;
@Mock
private DOMDataTreeReadWriteTransaction readWrite;
@Mock
- private DOMDataTreeReadTransaction read;
- @Mock
private DOMMountPointService mountPointService;
@Mock
- private DOMMountPoint mountPoint;
- @Mock
private DOMDataBroker mountDataBroker;
@Mock
- private NetconfDataTreeService netconfService;
- @Mock
private DOMActionService actionService;
@Mock
private DOMRpcService rpcService;
@Mock
- private MultivaluedMap<String, String> queryParamenters;
- @Mock
private AsyncResponse asyncResponse;
@Captor
private ArgumentCaptor<Response> responseCaptor;
@Before
public void setUp() throws Exception {
- doReturn(Set.of()).when(queryParamenters).entrySet();
- doReturn(queryParamenters).when(uriInfo).getQueryParameters();
-
doReturn(CommitInfo.emptyFluentFuture()).when(readWrite).commit();
final var dataBroker = mock(DOMDataBroker.class);
- doReturn(read).when(dataBroker).newReadOnlyTransaction();
doReturn(readWrite).when(dataBroker).newReadWriteTransaction();
final DatabindProvider databindProvider = () -> DatabindContext.ofModel(JUKEBOX_SCHEMA);
dataService = new RestconfDataServiceImpl(databindProvider,
new MdsalRestconfServer(databindProvider, dataBroker, rpcService, actionService, mountPointService));
- doReturn(Optional.of(mountPoint)).when(mountPointService)
- .getMountPoint(any(YangInstanceIdentifier.class));
- doReturn(Optional.of(FixedDOMSchemaService.of(JUKEBOX_SCHEMA))).when(mountPoint)
- .getService(DOMSchemaService.class);
- doReturn(Optional.of(mountDataBroker)).when(mountPoint).getService(DOMDataBroker.class);
- doReturn(Optional.of(rpcService)).when(mountPoint).getService(DOMRpcService.class);
- doReturn(Optional.empty()).when(mountPoint).getService(NetconfDataTreeService.class);
- doReturn(read).when(mountDataBroker).newReadOnlyTransaction();
- doReturn(readWrite).when(mountDataBroker).newReadWriteTransaction();
- }
-
- @Test
- public void testPutData() {
- doReturn(immediateTrueFluentFuture()).when(read)
- .exists(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID);
- doNothing().when(readWrite).put(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID, EMPTY_JUKEBOX);
-
- doReturn(true).when(asyncResponse).resume(responseCaptor.capture());
- dataService.putDataJSON("example-jukebox:jukebox", uriInfo, stringInputStream("""
- {
- "example-jukebox:jukebox" : {
- "player": {
- "gap": "0.2"
- }
- }
- }"""), asyncResponse);
- final var response = responseCaptor.getValue();
- assertEquals(Response.Status.NO_CONTENT.getStatusCode(), response.getStatus());
- }
-
- @Test
- public void testPutDataWithMountPoint() {
- doReturn(immediateTrueFluentFuture()).when(read)
- .exists(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID);
- doNothing().when(readWrite).put(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID, EMPTY_JUKEBOX);
-
- doReturn(true).when(asyncResponse).resume(responseCaptor.capture());
- dataService.putDataXML("example-jukebox:jukebox/yang-ext:mount/example-jukebox:jukebox",
- uriInfo, stringInputStream("""
- <jukebox xmlns="http://example.com/ns/example-jukebox">
- <player>
- <gap>0.2</gap>
- </player>
- </jukebox>"""), asyncResponse);
- final var response = responseCaptor.getValue();
- assertEquals(Response.Status.NO_CONTENT.getStatusCode(), response.getStatus());
}
@Test