--- /dev/null
+/*
+ * 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;
+ }
+}
--- /dev/null
+/*
+ * 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);
+}
<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>
// Annotation-only dependencies
requires static transitive org.eclipse.jdt.annotation;
- requires static com.github.spotbugs.annotations;
requires static org.osgi.annotation.bundle;
}
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;
* 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
}
}
- @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}.
*
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);
}
*/
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.
*/
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);
}
}
/**
* 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);
}
@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