Factor out netconf.databind.Request 96/115696/1
authorRobert Varga <robert.varga@pantheon.tech>
Sun, 2 Mar 2025 20:50:29 +0000 (21:50 +0100)
committerRobert Varga <robert.varga@pantheon.tech>
Sun, 2 Mar 2025 20:50:29 +0000 (21:50 +0100)
This is the counterpart to yang.common.RpcRequest, except having a nice
tie in with RequestException as the error reporting tool.

Change-Id: Ic6c3df7351fe4c7fd3e33509d1fc5af208cc6c59
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
protocol/databind/src/main/java/org/opendaylight/netconf/databind/AbstractRequest.java [new file with mode: 0644]
protocol/databind/src/main/java/org/opendaylight/netconf/databind/Request.java [new file with mode: 0644]
protocol/restconf-server-api/pom.xml
protocol/restconf-server-api/src/main/java/module-info.java
protocol/restconf-server-api/src/main/java/org/opendaylight/restconf/server/api/AbstractServerRequest.java
protocol/restconf-server-api/src/main/java/org/opendaylight/restconf/server/api/ServerRequest.java
protocol/restconf-server-api/src/main/java/org/opendaylight/restconf/server/api/TransformedServerRequest.java

diff --git a/protocol/databind/src/main/java/org/opendaylight/netconf/databind/AbstractRequest.java b/protocol/databind/src/main/java/org/opendaylight/netconf/databind/AbstractRequest.java
new file mode 100644 (file)
index 0000000..04144d6
--- /dev/null
@@ -0,0 +1,104 @@
+/*
+ * Copyright (c) 2025 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.netconf.databind;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.base.MoreObjects.ToStringHelper;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.VarHandle;
+import java.util.UUID;
+import org.eclipse.jdt.annotation.NonNull;
+
+/**
+ * Abstract base class for {@link Request} implementations. Each instance is automatically assigned a
+ * <a href="https://www.rfc-editor.org/rfc/rfc4122#section-4.4">type 4 UUID</a>.
+ *
+ * @param <R> type of reported result
+ */
+public abstract class AbstractRequest<R> implements Request<R> {
+    private static final VarHandle UUID_VH;
+
+    static {
+        try {
+            UUID_VH = MethodHandles.lookup().findVarHandle(AbstractRequest.class, "uuid", UUID.class);
+        } catch (NoSuchFieldException | IllegalAccessException e) {
+            throw new ExceptionInInitializerError(e);
+        }
+    }
+
+    @SuppressFBWarnings(value = "UUF_UNUSED_FIELD", justification = "https://github.com/spotbugs/spotbugs/issues/2749")
+    private volatile UUID uuid;
+
+    /**
+     * Default constructor.
+     */
+    protected AbstractRequest() {
+        // nothing here
+    }
+
+    @Override
+    public final UUID uuid() {
+        final var existing = (UUID) UUID_VH.getAcquire(this);
+        return existing != null ? existing : loadUuid();
+    }
+
+    private @NonNull UUID loadUuid() {
+        final var created = UUID.randomUUID();
+        final var witness = (UUID) UUID_VH.compareAndExchangeRelease(this, null, created);
+        return witness != null ? witness : created;
+    }
+
+    @Override
+    public final void completeWith(final R result) {
+        onSuccess(requireNonNull(result));
+    }
+
+    @Override
+    public final void completeWith(final RequestException failure) {
+        onFailure(requireNonNull(failure));
+    }
+
+    protected abstract void onSuccess(@NonNull R result);
+
+    protected abstract void onFailure(@NonNull RequestException failure);
+
+    @Override
+    public final int hashCode() {
+        return super.hashCode();
+    }
+
+    @Override
+    public final boolean equals(final Object obj) {
+        return super.equals(this);
+    }
+
+    @Override
+    public final String toString() {
+        return addToStringAttributes(MoreObjects.toStringHelper(this)).toString();
+    }
+
+    /**
+     * Add attributes to a {@link ToStringHelper}.
+     *
+     * @param helper the helper
+     * @return the helper
+     */
+    protected @NonNull ToStringHelper addToStringAttributes(final @NonNull ToStringHelper helper) {
+        helper.add("uuid", uuid());
+
+        final var principal = principal();
+        if (principal != null) {
+            helper.add("principal", principal.getName());
+        }
+
+        return helper;
+    }
+}
diff --git a/protocol/databind/src/main/java/org/opendaylight/netconf/databind/Request.java b/protocol/databind/src/main/java/org/opendaylight/netconf/databind/Request.java
new file mode 100644 (file)
index 0000000..55b93b5
--- /dev/null
@@ -0,0 +1,70 @@
+/*
+ * Copyright (c) 2025 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.netconf.databind;
+
+import com.google.common.annotations.Beta;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import java.security.Principal;
+import java.util.UUID;
+import java.util.concurrent.Executor;
+import java.util.function.Function;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * A request which can be completed either {@link #completeWith(Object) successfully} or
+ * {@link #completeWith(RequestException) unsuccessfully}. Each request has two invariants:
+ * <ol>
+ *   <li>it has a unique identifier, {@link #uuid()}</li>
+ *   <li>it has a {@link #principal()}, potentially unknown, which on behalf of whom the request is being made</li>
+ * </ol>
+ * A request can be thought of as a {@link FutureCallback} with attached metadata, where it is guaranteed the failure
+ * cause reported to {@link FutureCallback#onFailure(Throwable)} is always a {@link RequestException}. It can be adapted
+ * via {@link #transform(Function)} to a request of a different result type, similar to what a
+ * {@link Futures#transform(ListenableFuture, com.google.common.base.Function, Executor)} would do.
+ *
+ * <p>Completion is always signalled in the calling thread. Callers of {@link #completeWith(Object)} and
+ * {@link #completeWith(RequestException)} need to ensure that all side effects of the request have been completed. It
+ * is recommended that callers do not perform any further operations and just unwind the stack.
+ *
+ * @param <R> type of reported result
+ */
+@NonNullByDefault
+public interface Request<R> {
+    /**
+     * Return the identifier of this request.
+     *
+     * @return a {@link UUID}
+     */
+    UUID uuid();
+
+    /**
+     * Returns the {@link Principal} making this request.
+     *
+     * @return the Principal making this request, {@code null} if unauthenticated
+     */
+    @Nullable Principal principal();
+
+    /**
+     * Returns a request requesting a result of {@code U}. Completion of returned request will result in this request
+     * completing. If the request completes successfully, supplied function will be used to transform the result.
+     *
+     * @param <I> new result type
+     * @param function result mapping function
+     * @return a new request
+     */
+    // FIXME: this needs a better name
+    @Beta
+    <I> Request<I> transform(Function<I, R> function);
+
+    void completeWith(R result);
+
+    void completeWith(RequestException failure);
+}
index e6bcc79ce5eeb20258f79588a5beb7a8324bc788..de285045da80fb2545523babd9eeaaba4ed8e59c 100644 (file)
     <description>RESTCONF server API</description>
 
     <dependencies>
-        <dependency>
-            <groupId>com.github.spotbugs</groupId>
-            <artifactId>spotbugs-annotations</artifactId>
-            <optional>true</optional>
-        </dependency>
         <dependency>
             <groupId>com.google.code.gson</groupId>
             <artifactId>gson</artifactId>
index 283165935f1014f9480315097b219921636206e6..b3e3b72caed92f407f0c369754974bf65e491682 100644 (file)
@@ -28,6 +28,5 @@ module org.opendaylight.restconf.server.api {
 
     // Annotation-only dependencies
     requires static transitive org.eclipse.jdt.annotation;
-    requires static com.github.spotbugs.annotations;
     requires static org.osgi.annotation.bundle;
 }
index f0bd8cd95357992e9b5a52d5b75917150ecb13e0..68e50eda16996891a353677d1e2c07d841f90b21 100644 (file)
@@ -9,14 +9,10 @@ package org.opendaylight.restconf.server.api;
 
 import static java.util.Objects.requireNonNull;
 
-import com.google.common.base.MoreObjects;
 import com.google.common.base.MoreObjects.ToStringHelper;
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-import java.lang.invoke.MethodHandles;
-import java.lang.invoke.VarHandle;
-import java.util.UUID;
 import org.eclipse.jdt.annotation.NonNull;
 import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.opendaylight.netconf.databind.AbstractRequest;
 import org.opendaylight.netconf.databind.RequestException;
 import org.opendaylight.restconf.api.QueryParameters;
 import org.opendaylight.restconf.api.query.PrettyPrintParam;
@@ -27,28 +23,14 @@ import org.slf4j.LoggerFactory;
  * Abstract base class for {@link ServerRequest} implementations. Each instance is automatically assigned a
  * <a href="https://www.rfc-editor.org/rfc/rfc4122#section-4.4">type 4 UUID</a>.
  *
- * @param <T> type of reported result
+ * @param <R> type of reported result
  */
-public abstract non-sealed class AbstractServerRequest<T> implements ServerRequest<T> {
+public abstract non-sealed class AbstractServerRequest<R> extends AbstractRequest<R> implements ServerRequest<R> {
     private static final Logger LOG = LoggerFactory.getLogger(AbstractServerRequest.class);
 
-    private static final VarHandle UUID_VH;
-
-    static {
-        try {
-            UUID_VH = MethodHandles.lookup().findVarHandle(AbstractServerRequest.class, "uuid", UUID.class);
-        } catch (NoSuchFieldException | IllegalAccessException e) {
-            throw new ExceptionInInitializerError(e);
-        }
-    }
-
     private final @NonNull QueryParameters queryParameters;
     private final @NonNull PrettyPrintParam prettyPrint;
 
-    @SuppressFBWarnings(value = "UUF_UNUSED_FIELD", justification = "https://github.com/spotbugs/spotbugs/issues/2749")
-    @SuppressWarnings("unused")
-    private volatile UUID uuid;
-
     // TODO: this is where a binding to security principal and access control should be:
     //       - we would like to be able to have java.security.Principal#name() for logging purposes
     //       - we need to have a NACM-capable interface, through which we can check permissions (such as data PUT) and
@@ -75,40 +57,11 @@ public abstract non-sealed class AbstractServerRequest<T> implements ServerReque
         }
     }
 
-    @Override
-    public final UUID uuid() {
-        final var existing = (UUID) UUID_VH.getAcquire(this);
-        return existing != null ? existing : loadUuid();
-    }
-
-    private @NonNull UUID loadUuid() {
-        final var created = UUID.randomUUID();
-        final var witness = (UUID) UUID_VH.compareAndExchangeRelease(this, null, created);
-        return witness != null ? witness : created;
-    }
-
     @Override
     public final QueryParameters queryParameters() {
         return queryParameters;
     }
 
-    @Override
-    public final void completeWith(final T result) {
-        onSuccess(requireNonNull(result));
-    }
-
-    @Override
-    public final void completeWith(final RequestException failure) {
-        LOG.debug("Request {} failed", this, failure);
-        final var errors = failure.errors();
-        onFailure(new YangErrorsBody(errors));
-    }
-
-    @Override
-    public final void completeWith(final YangErrorsBody errors) {
-        onFailure(requireNonNull(errors));
-    }
-
     /**
      * Return the effective {@link PrettyPrintParam}.
      *
@@ -118,23 +71,22 @@ public abstract non-sealed class AbstractServerRequest<T> implements ServerReque
         return prettyPrint;
     }
 
-    protected abstract void onSuccess(@NonNull T result);
-
-    protected abstract void onFailure(@NonNull YangErrorsBody errors);
+    @Override
+    public final void completeWith(final YangErrorsBody errors) {
+        onFailure(requireNonNull(errors));
+    }
 
     @Override
-    public final String toString() {
-        return addToStringAttributes(MoreObjects.toStringHelper(this)).toString();
+    protected final void onFailure(final RequestException failure) {
+        LOG.debug("Request {} failed", this, failure);
+        onFailure(new YangErrorsBody(failure.errors()));
     }
 
-    protected ToStringHelper addToStringAttributes(final ToStringHelper helper) {
-        helper.add("uuid", uuid());
+    protected abstract void onFailure(@NonNull YangErrorsBody errors);
 
-        final var principal = principal();
-        if (principal != null) {
-            helper.add("principal", principal.getName());
-        }
-        return helper
+    @Override
+    protected ToStringHelper addToStringAttributes(final ToStringHelper helper) {
+        return super.addToStringAttributes(helper)
             .add("parameters", queryParameters)
             .add("prettyPrint", prettyPrint);
     }
index c409751d0db742351546018f9646bb0fdd052e41..15e2419cff1ba32e51d2a7f65a883d77febece95 100644 (file)
@@ -7,48 +7,30 @@
  */
 package org.opendaylight.restconf.server.api;
 
-import java.security.Principal;
-import java.util.UUID;
 import java.util.function.Function;
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
-import org.opendaylight.netconf.databind.RequestException;
+import org.opendaylight.netconf.databind.Request;
 import org.opendaylight.restconf.api.FormattableBody;
 import org.opendaylight.restconf.api.QueryParameters;
 import org.opendaylight.yangtools.yang.common.ErrorTag;
 
 /**
- * A request to {@link RestconfServer}. It contains state and binding established by whoever is performing the request
- * on the transport (typically HTTP) layer. This includes:
+ * A {@link Request} to {@link RestconfServer}. It contains state and binding established by whoever is performing the
+ * request on the transport (typically HTTP) layer. This includes:
  * <ul>
- *   <li>requesting {@link #principal()}</li>
  *   <li>HTTP request {@link #queryParameters() query parameters},</li>
+ *   <li>the transport {@link #session()} invoking this request</li>
  * </ul>
- * It notably does <b>not</b> hold the HTTP request path, nor the request body. Those are passed as separate arguments
- * to server methods as implementations of those methods are expected to act on them on multiple layers, i.e. they are
- * not a request invariant at the various processing layers.
  *
- * <p>Every request needs to be completed via one of {@link #completeWith(Object)},
- * {@link #completeWith(RequestException)} or other {@code completeWith} methods.
+ * <p>It notably does <b>not</b> hold the HTTP request path, nor the request body. Those are passed as separate
+ * arguments to server methods as implementations of those methods are expected to act on them on multiple layers, i.e.
+ * they are not a request invariant at the various processing layers.
  *
- * @param <T> type of reported result
+ * @param <R> type of reported result
  */
 @NonNullByDefault
-public sealed interface ServerRequest<T> permits AbstractServerRequest, TransformedServerRequest {
-    /**
-     * Return the identifier of this request.
-     *
-     * @return an UUID
-     */
-    UUID uuid();
-
-    /**
-     * Returns the Principal making this request.
-     *
-     * @return the Principal making this request, {@code null} if unauthenticated
-     */
-    @Nullable Principal principal();
-
+public sealed interface ServerRequest<R> extends Request<R> permits AbstractServerRequest, TransformedServerRequest {
     /**
      * Returns the {@link TransportSession} on which this request is executing, or {@code null} if there is no control
      * over transport sessions.
@@ -64,15 +46,12 @@ public sealed interface ServerRequest<T> permits AbstractServerRequest, Transfor
      */
     QueryParameters queryParameters();
 
-    void completeWith(T result);
-
-    void completeWith(RequestException failure);
-
     void completeWith(YangErrorsBody errors);
 
     void completeWith(ErrorTag errorTag, FormattableBody body);
 
-    default <O> ServerRequest<O> transform(final Function<O, T> function) {
+    @Override
+    default <I> ServerRequest<I> transform(final Function<I, R> function) {
         return new TransformedServerRequest<>(this, function);
     }
 }
index 0f262a78570fe14edd5a045c71208da01b640732..ed49dd31cec2e9254676816796d633ae2ab1bca8 100644 (file)
@@ -22,10 +22,11 @@ import org.opendaylight.yangtools.yang.common.ErrorTag;
 /**
  * A {@link ServerRequest} transformed through a {@link Function}.
  *
- * @param <T> type of reported result
+ * @param <I> input result type
+ * @param <R> output result type
  */
 @NonNullByDefault
-record TransformedServerRequest<O, T>(ServerRequest<T> delegate, Function<O, T> function) implements ServerRequest<O> {
+record TransformedServerRequest<I, R>(ServerRequest<R> delegate, Function<I, R> function) implements ServerRequest<I> {
     TransformedServerRequest {
         requireNonNull(delegate);
         requireNonNull(function);
@@ -52,8 +53,8 @@ record TransformedServerRequest<O, T>(ServerRequest<T> delegate, Function<O, T>
     }
 
     @Override
-    public void completeWith(final O result) {
-        delegate.completeWith(function.apply(result));
+    public void completeWith(final I result) {
+        delegate.completeWith(function.apply(requireNonNull(result)));
     }
 
     @Override