From: Robert Varga Date: Sun, 2 Mar 2025 20:50:29 +0000 (+0100) Subject: Factor out netconf.databind.Request X-Git-Url: https://git.opendaylight.org/gerrit/gitweb?a=commitdiff_plain;h=77e64eec105cfaef6172f11c118ee6ed04869bd3;p=netconf.git Factor out netconf.databind.Request 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 --- 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 index 0000000000..04144d690c --- /dev/null +++ b/protocol/databind/src/main/java/org/opendaylight/netconf/databind/AbstractRequest.java @@ -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 + * type 4 UUID. + * + * @param type of reported result + */ +public abstract class AbstractRequest implements Request { + 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 index 0000000000..55b93b5b90 --- /dev/null +++ b/protocol/databind/src/main/java/org/opendaylight/netconf/databind/Request.java @@ -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: + *
    + *
  1. it has a unique identifier, {@link #uuid()}
  2. + *
  3. it has a {@link #principal()}, potentially unknown, which on behalf of whom the request is being made
  4. + *
+ * 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. + * + *

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 type of reported result + */ +@NonNullByDefault +public interface Request { + /** + * 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 new result type + * @param function result mapping function + * @return a new request + */ + // FIXME: this needs a better name + @Beta + Request transform(Function function); + + void completeWith(R result); + + void completeWith(RequestException failure); +} diff --git a/protocol/restconf-server-api/pom.xml b/protocol/restconf-server-api/pom.xml index e6bcc79ce5..de285045da 100644 --- a/protocol/restconf-server-api/pom.xml +++ b/protocol/restconf-server-api/pom.xml @@ -22,11 +22,6 @@ RESTCONF server API - - com.github.spotbugs - spotbugs-annotations - true - com.google.code.gson gson diff --git a/protocol/restconf-server-api/src/main/java/module-info.java b/protocol/restconf-server-api/src/main/java/module-info.java index 283165935f..b3e3b72cae 100644 --- a/protocol/restconf-server-api/src/main/java/module-info.java +++ b/protocol/restconf-server-api/src/main/java/module-info.java @@ -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; } diff --git a/protocol/restconf-server-api/src/main/java/org/opendaylight/restconf/server/api/AbstractServerRequest.java b/protocol/restconf-server-api/src/main/java/org/opendaylight/restconf/server/api/AbstractServerRequest.java index f0bd8cd953..68e50eda16 100644 --- a/protocol/restconf-server-api/src/main/java/org/opendaylight/restconf/server/api/AbstractServerRequest.java +++ b/protocol/restconf-server-api/src/main/java/org/opendaylight/restconf/server/api/AbstractServerRequest.java @@ -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 * type 4 UUID. * - * @param type of reported result + * @param type of reported result */ -public abstract non-sealed class AbstractServerRequest implements ServerRequest { +public abstract non-sealed class AbstractServerRequest extends AbstractRequest implements ServerRequest { 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 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 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); } diff --git a/protocol/restconf-server-api/src/main/java/org/opendaylight/restconf/server/api/ServerRequest.java b/protocol/restconf-server-api/src/main/java/org/opendaylight/restconf/server/api/ServerRequest.java index c409751d0d..15e2419cff 100644 --- a/protocol/restconf-server-api/src/main/java/org/opendaylight/restconf/server/api/ServerRequest.java +++ b/protocol/restconf-server-api/src/main/java/org/opendaylight/restconf/server/api/ServerRequest.java @@ -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: *

    - *
  • requesting {@link #principal()}
  • *
  • HTTP request {@link #queryParameters() query parameters},
  • + *
  • the transport {@link #session()} invoking this request
  • *
- * It notably does not 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. * - *

Every request needs to be completed via one of {@link #completeWith(Object)}, - * {@link #completeWith(RequestException)} or other {@code completeWith} methods. + *

It notably does not 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 type of reported result + * @param type of reported result */ @NonNullByDefault -public sealed interface ServerRequest 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 extends Request 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 permits AbstractServerRequest, Transfor */ QueryParameters queryParameters(); - void completeWith(T result); - - void completeWith(RequestException failure); - void completeWith(YangErrorsBody errors); void completeWith(ErrorTag errorTag, FormattableBody body); - default ServerRequest transform(final Function function) { + @Override + default ServerRequest transform(final Function function) { return new TransformedServerRequest<>(this, function); } } diff --git a/protocol/restconf-server-api/src/main/java/org/opendaylight/restconf/server/api/TransformedServerRequest.java b/protocol/restconf-server-api/src/main/java/org/opendaylight/restconf/server/api/TransformedServerRequest.java index 0f262a7857..ed49dd31ce 100644 --- a/protocol/restconf-server-api/src/main/java/org/opendaylight/restconf/server/api/TransformedServerRequest.java +++ b/protocol/restconf-server-api/src/main/java/org/opendaylight/restconf/server/api/TransformedServerRequest.java @@ -22,10 +22,11 @@ import org.opendaylight.yangtools.yang.common.ErrorTag; /** * A {@link ServerRequest} transformed through a {@link Function}. * - * @param type of reported result + * @param input result type + * @param output result type */ @NonNullByDefault -record TransformedServerRequest(ServerRequest delegate, Function function) implements ServerRequest { +record TransformedServerRequest(ServerRequest delegate, Function function) implements ServerRequest { TransformedServerRequest { requireNonNull(delegate); requireNonNull(function); @@ -52,8 +53,8 @@ record TransformedServerRequest(ServerRequest delegate, Function } @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