--- /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.common.errors;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.base.Throwables;
+import com.google.common.util.concurrent.AbstractFuture;
+import com.google.common.util.concurrent.ListenableFuture;
+import java.util.concurrent.ExecutionException;
+import org.eclipse.jdt.annotation.NonNull;
+
+/**
+ * A {@link ListenableFuture} specialization, which fails only with {@link RestconfDocumentedException} and does not
+ * produce {@code null} values.
+ *
+ * @param <V> resulting value type
+ */
+public sealed class RestconfFuture<V> extends AbstractFuture<@NonNull V> permits SettableRestconfFuture {
+ RestconfFuture() {
+ // Hidden on purpose
+ }
+
+ public static <V> RestconfFuture<V> of(final V value) {
+ final var future = new RestconfFuture<V>();
+ future.set(requireNonNull(value));
+ return future;
+ }
+
+ public static <V> RestconfFuture<V> failed(final RestconfDocumentedException cause) {
+ final var future = new RestconfFuture<V>();
+ future.setException(requireNonNull(cause));
+ return future;
+ }
+
+ @Override
+ public final boolean cancel(final boolean mayInterruptIfRunning) {
+ return false;
+ }
+
+ /**
+ * Get the result.
+ *
+ * @return The result
+ * @throws RestconfDocumentedException if this future failed or this call is interrupted.
+ */
+ public final @NonNull V getOrThrow() {
+ try {
+ return get();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new RestconfDocumentedException("Interrupted while waiting", e);
+ } catch (ExecutionException e) {
+ Throwables.throwIfInstanceOf(e.getCause(), RestconfDocumentedException.class);
+ throw new RestconfDocumentedException("Operation failed", 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.common.errors;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * A {@link RestconfFuture} which allows the result to be set via {@link #set(Object)} and
+ * {@link #setFailure(RestconfDocumentedException)}.
+ *
+ * @param <V> resulting value type
+ */
+public final class SettableRestconfFuture<V> extends RestconfFuture<V> {
+ @Override
+ public boolean set(final V value) {
+ return super.set(requireNonNull(value));
+ }
+
+ public boolean setFailure(final RestconfDocumentedException cause) {
+ return setException(requireNonNull(cause));
+ }
+}
import org.opendaylight.restconf.nb.rfc8040.rests.services.api.RestconfStreamsSubscriptionService;
import org.opendaylight.restconf.nb.rfc8040.rests.transactions.MdsalRestconfStrategy;
import org.opendaylight.restconf.nb.rfc8040.rests.transactions.RestconfStrategy;
-import org.opendaylight.restconf.nb.rfc8040.rests.utils.DeleteDataTransactionUtil;
import org.opendaylight.restconf.nb.rfc8040.rests.utils.PatchDataTransactionUtil;
import org.opendaylight.restconf.nb.rfc8040.rests.utils.PlainPatchDataTransactionUtil;
import org.opendaylight.restconf.nb.rfc8040.rests.utils.PostDataTransactionUtil;
@Override
public Response deleteData(final String identifier) {
- final InstanceIdentifierContext instanceIdentifier = ParserIdentifier.toInstanceIdentifier(identifier,
+ final var instanceIdentifier = ParserIdentifier.toInstanceIdentifier(identifier,
databindProvider.currentContext().modelContext(), mountPointService);
- final DOMMountPoint mountPoint = instanceIdentifier.getMountPoint();
- final RestconfStrategy strategy = getRestconfStrategy(mountPoint);
- return DeleteDataTransactionUtil.deleteData(strategy, instanceIdentifier.getInstanceIdentifier());
+ getRestconfStrategy(instanceIdentifier.getMountPoint())
+ .delete(instanceIdentifier.getInstanceIdentifier())
+ .getOrThrow();
+ return Response.noContent().build();
}
@Override
package org.opendaylight.restconf.nb.rfc8040.rests.transactions;
import static java.util.Objects.requireNonNull;
+import static org.opendaylight.mdsal.common.api.LogicalDatastoreType.CONFIGURATION;
+import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
import java.util.List;
import java.util.Optional;
+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.DOMDataTreeReadWriteTransaction;
import org.opendaylight.mdsal.dom.api.DOMTransactionChain;
+import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
+import org.opendaylight.restconf.common.errors.SettableRestconfFuture;
+import org.opendaylight.yangtools.yang.common.Empty;
+import org.opendaylight.yangtools.yang.common.ErrorTag;
+import org.opendaylight.yangtools.yang.common.ErrorType;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
return new MdsalRestconfTransaction(dataBroker);
}
+ @Override
+ protected void delete(final SettableRestconfFuture<Empty> future, final YangInstanceIdentifier path) {
+ final var tx = dataBroker.newReadWriteTransaction();
+ tx.exists(CONFIGURATION, path).addCallback(new FutureCallback<>() {
+ @Override
+ public void onSuccess(final Boolean result) {
+ if (!result) {
+ cancelTx(new RestconfDocumentedException("Data does not exist", ErrorType.PROTOCOL,
+ ErrorTag.DATA_MISSING, path));
+ return;
+ }
+
+ tx.delete(CONFIGURATION, path);
+ tx.commit().addCallback(new FutureCallback<CommitInfo>() {
+ @Override
+ public void onSuccess(final CommitInfo result) {
+ future.set(Empty.value());
+ }
+
+ @Override
+ public void onFailure(final Throwable cause) {
+ future.setFailure(new RestconfDocumentedException("Transaction to delete " + path + " failed",
+ cause));
+ }
+ }, MoreExecutors.directExecutor());
+ }
+
+ @Override
+ public void onFailure(final Throwable cause) {
+ cancelTx(new RestconfDocumentedException("Failed to access " + path, cause));
+ }
+
+ private void cancelTx(final RestconfDocumentedException ex) {
+ tx.cancel();
+ future.setFailure(ex);
+ }
+ }, MoreExecutors.directExecutor());
+ }
+
@Override
public ListenableFuture<Optional<NormalizedNode>> read(final LogicalDatastoreType store,
final YangInstanceIdentifier path) {
import com.google.common.util.concurrent.SettableFuture;
import java.util.List;
import java.util.Optional;
+import org.opendaylight.mdsal.common.api.CommitInfo;
import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
import org.opendaylight.mdsal.common.api.ReadFailedException;
import org.opendaylight.netconf.dom.api.NetconfDataTreeService;
+import org.opendaylight.restconf.common.errors.SettableRestconfFuture;
+import org.opendaylight.restconf.nb.rfc8040.rests.utils.TransactionUtil;
+import org.opendaylight.yangtools.yang.common.Empty;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
this.netconfService = requireNonNull(netconfService);
}
+ @Override
+ protected void delete(final SettableRestconfFuture<Empty> future, final YangInstanceIdentifier path) {
+ final var tx = prepareWriteExecution();
+ tx.delete(path);
+ Futures.addCallback(tx.commit(), new FutureCallback<CommitInfo>() {
+ @Override
+ public void onSuccess(final CommitInfo result) {
+ future.set(Empty.value());
+ }
+
+ @Override
+ public void onFailure(final Throwable cause) {
+ future.setFailure(TransactionUtil.decodeException(cause, "DELETE", path));
+ }
+ }, MoreExecutors.directExecutor());
+ }
+
@Override
public RestconfTransaction prepareWriteExecution() {
return new NetconfRestconfTransaction(netconfService);
*/
package org.opendaylight.restconf.nb.rfc8040.rests.transactions;
+import static java.util.Objects.requireNonNull;
+
import com.google.common.util.concurrent.ListenableFuture;
import java.util.List;
import java.util.Optional;
+import org.eclipse.jdt.annotation.NonNull;
import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
import org.opendaylight.mdsal.dom.api.DOMDataBroker;
import org.opendaylight.mdsal.dom.api.DOMMountPoint;
import org.opendaylight.netconf.dom.api.NetconfDataTreeService;
+import org.opendaylight.restconf.common.errors.RestconfFuture;
+import org.opendaylight.restconf.common.errors.SettableRestconfFuture;
+import org.opendaylight.yangtools.yang.common.Empty;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
* @return a FluentFuture containing the result of the check
*/
public abstract ListenableFuture<Boolean> exists(LogicalDatastoreType store, YangInstanceIdentifier path);
+
+ /**
+ * Delete data from the configuration datastore. If the data does not exist, this operation will fail, as outlined
+ * in <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.7">RFC8040 section 4.7</a>
+ *
+ * @param path Path to delete
+ * @return A {@link RestconfFuture}
+ * @throws NullPointerException if {@code path} is {@code null}
+ */
+ public final @NonNull RestconfFuture<Empty> delete(final YangInstanceIdentifier path) {
+ final var ret = new SettableRestconfFuture<Empty>();
+ delete(ret, requireNonNull(path));
+ return ret;
+ }
+
+ protected abstract void delete(@NonNull SettableRestconfFuture<Empty> future, @NonNull YangInstanceIdentifier path);
}
+++ /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.utils;
-
-import javax.ws.rs.core.Response;
-import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
-import org.opendaylight.restconf.nb.rfc8040.rests.transactions.RestconfStrategy;
-import org.opendaylight.restconf.nb.rfc8040.rests.transactions.RestconfTransaction;
-import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
-
-/**
- * Util class for delete specific data in config DS.
- */
-public final class DeleteDataTransactionUtil {
- private DeleteDataTransactionUtil() {
- // Hidden on purpose
- }
-
- /**
- * Delete data from DS via transaction.
- *
- * @param strategy object that perform the actual DS operations
- * @return {@link Response}
- */
- public static Response deleteData(final RestconfStrategy strategy, final YangInstanceIdentifier path) {
- final RestconfTransaction transaction = strategy.prepareWriteExecution();
- try {
- transaction.delete(path);
- } catch (RestconfDocumentedException e) {
- // close transaction if any and pass exception further
- transaction.cancel();
- throw e;
- }
-
- TransactionUtil.syncCommit(transaction.commit(), "DELETE", path);
- return Response.noContent().build();
- }
-}
LOG.trace("Transaction({}) SUCCESSFUL", txType);
}
+ public static @NonNull RestconfDocumentedException decodeException(final Throwable throwable,
+ final String txType, final YangInstanceIdentifier path) {
+ return decodeException(throwable, throwable, txType, path);
+ }
+
private static @NonNull RestconfDocumentedException decodeException(final ExecutionException ex,
final String txType, final YangInstanceIdentifier path) {
- if (ex.getCause() instanceof TransactionCommitFailedException commitFailed) {
+ return decodeException(ex, ex.getCause(), txType, path);
+ }
+
+ private static @NonNull RestconfDocumentedException decodeException(final Throwable ex, final Throwable cause,
+ final String txType, final YangInstanceIdentifier path) {
+ if (cause instanceof TransactionCommitFailedException) {
// If device send some error message we want this message to get to client and not just to throw it away
// or override it with new generic message. We search for NetconfDocumentedException that was send from
// netconfSB and we create RestconfDocumentedException accordingly.
- for (var error : Throwables.getCausalChain(commitFailed)) {
+ for (var error : Throwables.getCausalChain(cause)) {
if (error instanceof DocumentedException documentedError) {
final ErrorTag errorTag = documentedError.getErrorTag();
if (errorTag.equals(ErrorTag.DATA_EXISTS)) {
package org.opendaylight.restconf.nb.rfc8040.rests.transactions;
import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.opendaylight.restconf.common.patch.PatchEditOperation.REPLACE;
import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.Futures;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
-import javax.ws.rs.core.Response.Status;
+import java.util.concurrent.ExecutionException;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import org.eclipse.jdt.annotation.NonNull;
import org.opendaylight.restconf.common.patch.PatchStatusContext;
import org.opendaylight.restconf.nb.rfc8040.AbstractJukeboxTest;
import org.opendaylight.restconf.nb.rfc8040.WriteDataParams;
-import org.opendaylight.restconf.nb.rfc8040.rests.utils.DeleteDataTransactionUtil;
import org.opendaylight.restconf.nb.rfc8040.rests.utils.PatchDataTransactionUtil;
import org.opendaylight.restconf.nb.rfc8040.rests.utils.PlainPatchDataTransactionUtil;
import org.opendaylight.restconf.nb.rfc8040.rests.utils.PostDataTransactionUtil;
* Test of successful DELETE operation.
*/
@Test
- public final void testDeleteData() {
- final var response = DeleteDataTransactionUtil.deleteData(testDeleteDataStrategy(),
- YangInstanceIdentifier.of());
- // assert success
- assertEquals(Status.NO_CONTENT.getStatusCode(), response.getStatus());
+ public final void testDeleteData() throws Exception {
+ final var future = testDeleteDataStrategy().delete(YangInstanceIdentifier.of());
+ assertNotNull(Futures.getDone(future));
}
abstract @NonNull RestconfStrategy testDeleteDataStrategy();
*/
@Test
public final void testNegativeDeleteData() {
- final var strategy = testNegativeDeleteDataStrategy();
- final var ex = assertThrows(RestconfDocumentedException.class,
- () -> DeleteDataTransactionUtil.deleteData(strategy, YangInstanceIdentifier.of()));
- final var errors = ex.getErrors();
+ final var future = testNegativeDeleteDataStrategy().delete(YangInstanceIdentifier.of());
+ final var ex = assertThrows(ExecutionException.class, () -> Futures.getDone(future)).getCause();
+ assertThat(ex, instanceOf(RestconfDocumentedException.class));
+ final var errors = ((RestconfDocumentedException) ex).getErrors();
assertEquals(1, errors.size());
final var error = errors.get(0);
assertEquals(ErrorType.PROTOCOL, error.getErrorType());