Add RestconfCallback 67/108267/2
authorRobert Varga <robert.varga@pantheon.tech>
Thu, 5 Oct 2023 23:48:54 +0000 (01:48 +0200)
committerRobert Varga <robert.varga@pantheon.tech>
Fri, 6 Oct 2023 02:33:18 +0000 (04:33 +0200)
Improve RestconfFuture with a .addCallback(RestconfCallback), where
RestconfCallback is specialized to match RestconfFuture's reporting only
RestconfDocumentedException.

This opens up for a further specialization, JaxRsRestconfCallback, which
performs a transform-and-set of an AsyncResponse.

JIRA: NETCONF-718
Change-Id: I912847da952d7c23cc5d08a3da90758e3f065539
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/errors/RestconfCallback.java [new file with mode: 0644]
restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/errors/RestconfFuture.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/JaxRsRestconfCallback.java [new file with mode: 0644]
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/RestconfDataServiceImpl.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/RestconfInvokeOperationsServiceImpl.java
restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/RestconfDataServiceImplTest.java

diff --git a/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/errors/RestconfCallback.java b/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/errors/RestconfCallback.java
new file mode 100644 (file)
index 0000000..d7bd8c2
--- /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.common.errors;
+
+import com.google.common.util.concurrent.FutureCallback;
+import org.eclipse.jdt.annotation.NonNull;
+
+/**
+ * A {@link FutureCallback} tied to a {@link RestconfFuture}.
+ *
+ * @param <V> value type
+ */
+public abstract class RestconfCallback<V> implements FutureCallback<@NonNull V> {
+    @Override
+    public final void onFailure(final Throwable cause) {
+        onFailure(cause instanceof RestconfDocumentedException rde ? rde
+            : new RestconfDocumentedException("Unexpected failure", cause));
+    }
+
+    protected abstract void onFailure(@NonNull RestconfDocumentedException failure);
+}
index c49fde6e50adc718912aa240191461f2920ef18e..11f60a4b5a259d142737af12d52db0fa0b8213b9 100644 (file)
@@ -11,7 +11,9 @@ 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.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
 import java.util.concurrent.ExecutionException;
 import org.eclipse.jdt.annotation.NonNull;
 
@@ -43,6 +45,10 @@ public sealed class RestconfFuture<V> extends AbstractFuture<@NonNull V> permits
         return false;
     }
 
+    public final void addCallback(final RestconfCallback<? super V> callback) {
+        Futures.addCallback(this, callback, MoreExecutors.directExecutor());
+    }
+
     /**
      * Get the result.
      *
diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/JaxRsRestconfCallback.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/JaxRsRestconfCallback.java
new file mode 100644 (file)
index 0000000..fa1ad25
--- /dev/null
@@ -0,0 +1,41 @@
+/*
+ * 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 java.util.Objects.requireNonNull;
+
+import java.util.function.Function;
+import javax.ws.rs.container.AsyncResponse;
+import javax.ws.rs.core.Response;
+import org.opendaylight.restconf.common.errors.RestconfCallback;
+import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
+
+/**
+ * A {@link RestconfCallback} completing an {@link AsyncResponse}.
+ *
+ * @param <V> value type
+ */
+final class JaxRsRestconfCallback<V> extends RestconfCallback<V> {
+    private final Function<V, Response> transform;
+    private final AsyncResponse ar;
+
+    JaxRsRestconfCallback(final AsyncResponse ar, final Function<V, Response> transform) {
+        this.ar = requireNonNull(ar);
+        this.transform = requireNonNull(transform);
+    }
+
+    @Override
+    public void onSuccess(final V result) {
+        ar.resume(transform.apply(result));
+    }
+
+    @Override
+    protected void onFailure(final RestconfDocumentedException failure) {
+        ar.resume(failure);
+    }
+}
index 996962c8c9351fdeaa048a9f1a2eada1fe03de0b..b17f2c6a2526d8b96735ef2d9e349d17abe1cc62 100644 (file)
@@ -17,7 +17,6 @@ import static org.opendaylight.restconf.nb.rfc8040.rests.utils.RestconfStreamsCo
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Throwables;
-import com.google.common.util.concurrent.FutureCallback;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.MoreExecutors;
 import java.io.IOException;
@@ -88,7 +87,6 @@ import org.opendaylight.restconf.nb.rfc8040.streams.listeners.ListenersBroker;
 import org.opendaylight.restconf.nb.rfc8040.streams.listeners.NotificationListenerAdapter;
 import org.opendaylight.restconf.nb.rfc8040.utils.parser.IdentifierCodec;
 import org.opendaylight.yang.gen.v1.urn.sal.restconf.event.subscription.rev140708.NotificationOutputTypeGrouping.NotificationOutputType;
-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.common.QName;
@@ -540,17 +538,8 @@ public final class RestconfDataServiceImpl {
         final var reqPath = server.bindRequestPath(databindProvider.currentContext(), identifier);
         final var strategy = server.getRestconfStrategy(reqPath.getSchemaContext(), reqPath.getMountPoint());
 
-        Futures.addCallback(strategy.delete(reqPath.getInstanceIdentifier()), new FutureCallback<>() {
-            @Override
-            public void onSuccess(final Empty result) {
-                ar.resume(Response.noContent().build());
-            }
-
-            @Override
-            public void onFailure(final Throwable failure) {
-                ar.resume(failure);
-            }
-        }, MoreExecutors.directExecutor());
+        strategy.delete(reqPath.getInstanceIdentifier()).addCallback(
+            new JaxRsRestconfCallback<>(ar, ignored -> Response.noContent().build()));
     }
 
     /**
@@ -669,19 +658,8 @@ public final class RestconfDataServiceImpl {
     private void plainPatchData(final InstanceIdentifierContext reqPath, final ResourceBody body,
             final AsyncResponse ar) {
         final var req = bindResourceRequest(reqPath, body);
-        final var future = req.strategy().merge(req.path(), req.data());
-
-        Futures.addCallback(future, new FutureCallback<>() {
-            @Override
-            public void onSuccess(final Empty result) {
-                ar.resume(Response.ok().build());
-            }
-
-            @Override
-            public void onFailure(final Throwable failure) {
-                ar.resume(failure);
-            }
-        }, MoreExecutors.directExecutor());
+        req.strategy().merge(req.path(), req.data()).addCallback(
+            new JaxRsRestconfCallback<>(ar, ignored -> Response.ok().build()));
     }
 
     private @NonNull ResourceRequest bindResourceRequest(final InstanceIdentifierContext reqPath,
@@ -796,18 +774,8 @@ public final class RestconfDataServiceImpl {
     @VisibleForTesting
     void yangPatchData(final @NonNull EffectiveModelContext modelContext,
             final @NonNull PatchContext patch, final @Nullable DOMMountPoint mountPoint, final AsyncResponse ar) {
-        Futures.addCallback(server.getRestconfStrategy(modelContext, mountPoint).patchData(patch),
-            new FutureCallback<>() {
-                @Override
-                public void onSuccess(final PatchStatusContext result) {
-                    ar.resume(result);
-                }
-
-                @Override
-                public void onFailure(final Throwable cause) {
-                    ar.resume(cause);
-                }
-            }, MoreExecutors.directExecutor());
+        server.getRestconfStrategy(modelContext, mountPoint).patchData(patch)
+            .addCallback(new JaxRsRestconfCallback<>(ar, status -> Response.ok().entity(status).build()));
     }
 
     private static @NonNull PatchContext parsePatchBody(final @NonNull EffectiveModelContext context,
index 03fbe8e93bd202817378848ab5c66b2f99491b10..53d19a8911f00ca51521e2af429a67688471dc03 100644 (file)
@@ -9,9 +9,6 @@ package org.opendaylight.restconf.nb.rfc8040.rests.services.impl;
 
 import static java.util.Objects.requireNonNull;
 
-import com.google.common.util.concurrent.FutureCallback;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.MoreExecutors;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.Optional;
@@ -147,24 +144,11 @@ public final class RestconfInvokeOperationsServiceImpl {
                     ErrorTag.MALFORMED_MESSAGE, e);
         }
 
-        Futures.addCallback(hackInvokeRpc(databind, reqPath, uriInfo, input), new FutureCallback<>() {
-            @Override
-            public void onSuccess(final Optional<ContainerNode> result) {
-                if (result.isPresent()) {
-                    final var output = result.orElseThrow();
-                    if (!output.isEmpty()) {
-                        ar.resume(Response.ok().entity(new NormalizedNodePayload(reqPath.inference(), output)).build());
-                        return;
-                    }
-                }
-                ar.resume(Response.noContent().build());
-            }
-
-            @Override
-            public void onFailure(final Throwable failure) {
-                ar.resume(failure);
-            }
-        }, MoreExecutors.directExecutor());
+        hackInvokeRpc(databind, reqPath, uriInfo, input).addCallback(new JaxRsRestconfCallback<>(ar,
+            result -> result
+                .filter(output -> !output.isEmpty())
+                .map(output -> Response.ok().entity(new NormalizedNodePayload(reqPath.inference(), output)).build())
+                .orElseGet(() -> Response.noContent().build())));
     }
 
     private RestconfFuture<Optional<ContainerNode>> hackInvokeRpc(final DatabindContext localDatabind,
index 0e8ad60ae3568a51ac47b57a54638d3c0f400752..0967368b365ca422f4523479b928395f898ff09b 100644 (file)
@@ -7,12 +7,13 @@
  */
 package org.opendaylight.restconf.nb.rfc8040.rests.services.impl;
 
-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.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
@@ -118,7 +119,7 @@ public class RestconfDataServiceImplTest extends AbstractJukeboxTest {
     @Mock
     private AsyncResponse asyncResponse;
     @Captor
-    private ArgumentCaptor<PatchStatusContext> patchStatus;
+    private ArgumentCaptor<Response> responseCaptor;
 
     private RestconfDataServiceImpl dataService;
 
@@ -418,9 +419,12 @@ public class RestconfDataServiceImplTest extends AbstractJukeboxTest {
                 .when(readWrite).exists(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID);
         doReturn(immediateTrueFluentFuture())
                 .when(readWrite).exists(LogicalDatastoreType.CONFIGURATION, GAP_IID);
-        doReturn(true).when(asyncResponse).resume(patchStatus.capture());
+        doReturn(true).when(asyncResponse).resume(responseCaptor.capture());
         dataService.yangPatchData(JUKEBOX_SCHEMA, patch, null, asyncResponse);
-        final var status = patchStatus.getValue();
+        final var response = responseCaptor.getValue();
+        assertEquals(200, response.getStatus());
+        final var status = assertInstanceOf(PatchStatusContext.class, response.getEntity());
+
         assertTrue(status.ok());
         assertEquals(3, status.editCollection().size());
         assertEquals("replace data", status.editCollection().get(1).getEditId());
@@ -438,9 +442,12 @@ public class RestconfDataServiceImplTest extends AbstractJukeboxTest {
                 .when(readWrite).exists(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID);
         doReturn(immediateTrueFluentFuture()).when(readWrite).exists(LogicalDatastoreType.CONFIGURATION, GAP_IID);
 
-        doReturn(true).when(asyncResponse).resume(patchStatus.capture());
+        doReturn(true).when(asyncResponse).resume(responseCaptor.capture());
         dataService.yangPatchData(JUKEBOX_SCHEMA, patch, mountPoint, asyncResponse);
-        final var status = patchStatus.getValue();
+        final var response = responseCaptor.getValue();
+        assertEquals(200, response.getStatus());
+        final var status = assertInstanceOf(PatchStatusContext.class, response.getEntity());
+
         assertTrue(status.ok());
         assertEquals(3, status.editCollection().size());
         assertNull(status.globalErrors());
@@ -460,9 +467,11 @@ public class RestconfDataServiceImplTest extends AbstractJukeboxTest {
                 .when(readWrite).exists(LogicalDatastoreType.CONFIGURATION, GAP_IID);
         doReturn(true).when(readWrite).cancel();
 
-        doReturn(true).when(asyncResponse).resume(patchStatus.capture());
+        doReturn(true).when(asyncResponse).resume(responseCaptor.capture());
         dataService.yangPatchData(JUKEBOX_SCHEMA, patch, null, asyncResponse);
-        final var status = patchStatus.getValue();
+        final var response = responseCaptor.getValue();
+        assertEquals(200, response.getStatus());
+        final var status = assertInstanceOf(PatchStatusContext.class, response.getEntity());
 
         assertFalse(status.ok());
         assertEquals(3, status.editCollection().size());