// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.jdisc; import com.yahoo.jdisc.application.BindingMatch; import com.yahoo.jdisc.application.UriPattern; import com.yahoo.jdisc.handler.BindingNotFoundException; import com.yahoo.jdisc.handler.ContentChannel; import com.yahoo.jdisc.handler.RequestDeniedException; import com.yahoo.jdisc.handler.RequestHandler; import com.yahoo.jdisc.handler.ResponseHandler; import com.yahoo.jdisc.service.CurrentContainer; import com.yahoo.jdisc.service.ServerProvider; import java.net.URI; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.concurrent.TimeUnit; /** *

This class represents a single request (which may have any content model that a {@link ServerProvider} chooses to * implement). The {@link #uri URI} is used by the {@link Container} to route it to the appropriate {@link * RequestHandler}, which in turn will provide a {@link ContentChannel} to write content to.

* *

To ensure application consistency throughout the lifetime of a Request, the Request itself holds an active * reference to the Container for which it was created. This has the unfortunate side-effect of requiring the creator of * a Request to do explicit reference counting during the setup of a content stream.

* *

For every successfully dispatched Request (i.e. a non-null ContentChannel has been retrieved), there will be * exactly one {@link Response} returned to the provided {@link ResponseHandler}.

* * @author Simon Thoresen Hult * @see Container * @see Response */ public class Request extends AbstractResource { private final Object monitor = new Object(); private final Map context = Collections.synchronizedMap(new HashMap<>()); private final HeaderFields headers = new HeaderFields(); private final Container container; private final Request parent; private final ResourceReference parentReference; private final long creationTime; private final boolean serverRequest; private final URI uri; private boolean cancel = false; private volatile BindingMatch bindingMatch; private TimeoutManager timeoutManager; private Long timeout; public enum RequestType { READ, WRITE, MONITORING } /** *

Creates a new instance of this class. As a {@link ServerProvider} you need to inject a {@link * CurrentContainer} instance at construction time and use that as argument to this method. As a {@link * RequestHandler} that needs to spawn child Requests, use the {@link #Request(Request, URI) other * constructor}.

* *

Because a Request holds an active reference to the owning {@link Container}, it is necessary to call {@link * #release()} once a {@link ContentChannel} has been established. Suggested usage:

* *
     * Request request = null;
     * ContentChannel content = null;
     * try {
     *     request = new Request(currentContainer, uri);
     *     (...)
     *     content = request.connect(responseHandler);
     * } finally {
     *    if (request != null) request.release();
     * }
     * content.write(...);
     * 
* * @param current The CurrentContainer for which this Request is created. * @param uri The identifier of this request. */ public Request(CurrentContainer current, URI uri) { this(current, uri, true); } public Request(CurrentContainer current, URI uri, boolean isServerRequest) { this(current, uri, isServerRequest, -1); } public Request(CurrentContainer current, URI uri, boolean isServerRequest, long creationTime) { parent = null; parentReference = null; serverRequest = isServerRequest; this.uri = uri.normalize(); container = current.newReference(uri, this); this.creationTime = creationTime >= 0 ? creationTime : container.currentTimeMillis(); } /** *

Creates a new instance of this class. As a {@link RequestHandler} you should use this method to spawn child * Requests of another. As a {@link ServerProvider} that needs to spawn new Requests, us the {@link * #Request(CurrentContainer, URI) other constructor}.

* *

Because a Request holds an active reference to the owning {@link Container}, it is necessary to call {@link * #release()} once a {@link ContentChannel} has been established. Suggested usage:

* *
     * Request request = null;
     * ContentChannel content = null;
     * try {
     *     request = new Request(parentRequest, uri);
     *     (...)
     *     content = request.connect(responseHandler);
     * } finally {
     *    if (request != null) request.release();
     * }
     * content.write(...);
     * 
* * @param parent The parent Request of this. * @param uri The identifier of this request. */ public Request(Request parent, URI uri) { this.parent = parent; container = null; creationTime = parent.container().currentTimeMillis(); serverRequest = false; this.uri = uri.normalize(); parentReference = this.parent.refer(this); } /** Returns the {@link Container} for which this Request was created */ public Container container() { return parent != null ? parent.container() : container; } /** * Returns the Uniform Resource Identifier used by the {@link Container} to resolve the appropriate {@link * RequestHandler} for this Request. */ public URI getUri() { return uri; } /** * Returns whether or not this Request was created by a {@link ServerProvider}. The value of this is used by * {@link Container#resolveHandler(Request)} to decide whether to match against server- or client-bindings. * * @return true, if this is a server request */ public boolean isServerRequest() { return serverRequest; } /** * Returns the last resolved {@link BindingMatch}, or null if none has been resolved yet. This is set * automatically when calling the {@link Container#resolveHandler(Request)} method. The BindingMatch object holds * information about the match of this Request's {@link #getUri() URI} to the {@link UriPattern} of the resolved * {@link RequestHandler}. It allows you to reflect on the parts of the URI that were matched by wildcards in the * UriPattern. * * @return the last resolved BindingMatch, or null * @see #setBindingMatch(BindingMatch) * @see Container#resolveHandler(Request) */ public BindingMatch getBindingMatch() { return bindingMatch; } /** *

Sets the last resolved {@link BindingMatch} of this Request. This is called by the {@link * Container#resolveHandler(Request)} method.

* * @param bindingMatch The BindingMatch to set. * @return This, to allow chaining. * @see #getBindingMatch() */ public Request setBindingMatch(BindingMatch bindingMatch) { this.bindingMatch = bindingMatch; return this; } /** *

Returns the named application context objects. This data is not intended for network transport, rather they * are intended for passing shared data between components of an Application.

* * @return The context map. */ public Map context() { return context; } /** *

Returns the set of header fields of this Request. These are the meta-data of the Request, and are not applied * to any internal {@link Container} logic. As opposed to the {@link #context()}, the headers ARE intended for * network transport. Modifying headers is a thread-unsafe operation -- any changes made after calling {@link * #connect(ResponseHandler)} might never become visible to other threads, and might throw * ConcurrentModificationExceptions in other threads.

* * @return The header fields. */ public HeaderFields headers() { return headers; } /** *

Sets a {@link TimeoutManager} to be called when {@link #setTimeout(long, TimeUnit)} is invoked. If a timeout * has already been set for this Request, the TimeoutManager is called before returning. This method will throw an * IllegalStateException if it has already been called.

* *

NOTE: This is used by the default timeout management implementation, so unless you are replacing that * mechanism you should avoid calling this method. If you do want to replace that mechanism, you need to * call this method prior to calling the target {@link RequestHandler} (since that injects the default manager).

* * @param timeoutManager The manager to set. * @throws NullPointerException If the TimeoutManager is null. * @throws IllegalStateException If another TimeoutManager has already been set. * @see #getTimeoutManager() * @see #setTimeout(long, TimeUnit) */ public void setTimeoutManager(TimeoutManager timeoutManager) { Objects.requireNonNull(timeoutManager, "timeoutManager"); synchronized (monitor) { if (this.timeoutManager != null) { throw new IllegalStateException("Timeout manager already set."); } this.timeoutManager = timeoutManager; if (timeout != null) { timeoutManager.scheduleTimeout(this); } } } /** *

Returns the {@link TimeoutManager} of this request, or null if none has been assigned.

* * @return The TimeoutManager of this Request. * @see #setTimeoutManager(TimeoutManager) */ public TimeoutManager getTimeoutManager() { synchronized (monitor) { return timeoutManager; } } /** *

Sets the allocated time that this Request is allowed to exist before the corresponding call to {@link * ResponseHandler#handleResponse(Response)} must have been made. If no timeout value is assigned to a Request, * there will be no timeout.

* *

Once the allocated time has expired, unless the {@link ResponseHandler} has already been called, the {@link * RequestHandler#handleTimeout(Request, ResponseHandler)} method is invoked.

* *

Calls to {@link #isCancelled()} return true if timeout has been exceeded.

* * @param timeout The allocated amount of time. * @param unit The time unit of the timeout argument. * @see #getTimeout(TimeUnit) * @see #timeRemaining(TimeUnit) */ public void setTimeout(long timeout, TimeUnit unit) { synchronized (monitor) { this.timeout = unit.toMillis(timeout); if (timeoutManager != null) { timeoutManager.scheduleTimeout(this); } } } /** *

Returns the allocated number of time units that this Request is allowed to exist. If no timeout has been set * for this Request, this method returns null.

* * @param unit The unit to return the timeout in. * @return The timeout of this Request. * @see #setTimeout(long, TimeUnit) */ public Long getTimeout(TimeUnit unit) { synchronized (monitor) { if (timeout == null) { return null; } return unit.convert(timeout, TimeUnit.MILLISECONDS); } } /** *

Returns the time that this Request is allowed to exist. If no timeout has been set, this method will return * null.

* * @param unit The unit to return the time in. * @return The number of time units left until this Request times out, or null. */ public Long timeRemaining(TimeUnit unit) { synchronized (monitor) { if (timeout == null) { return null; } return unit.convert(timeout - (container().currentTimeMillis() - creationTime), TimeUnit.MILLISECONDS); } } /** *

Returns the time that this Request has existed so far. * * @param unit The unit to return the time in. * @return The number of time units elapsed since this Request was created. */ public long timeElapsed(TimeUnit unit) { return unit.convert(container().currentTimeMillis() - creationTime, TimeUnit.MILLISECONDS); } /** *

Returns the time at which this Request was created. This is whatever value was returned by {@link * Timer#currentTimeMillis()} when constructing this.

* * @param unit The unit to return the time in. * @return The creation time of this Request. */ public long creationTime(TimeUnit unit) { return unit.convert(creationTime, TimeUnit.MILLISECONDS); } /** *

Returns whether or not this Request has been cancelled. This can be thought of as the {@link * Thread#isInterrupted()} of Requests - it does not enforce anything in ways of blocking the Request, it is simply * a signal to allow the developer to break early if the Request has already been dropped.

* *

This method will also return true if the Request has a non-null timeout, and that timeout has * expired.

* *

Finally, this method will also return true if this Request has a parent Request that has been * cancelled.

* * @return True if this Request has timed out or been cancelled. * @see #cancel() * @see #setTimeout(long, TimeUnit) */ public boolean isCancelled() { synchronized (monitor) { if (cancel) { return true; } if (timeout != null && timeRemaining(TimeUnit.MILLISECONDS) <= 0) { return true; } } if (parent != null && parent.isCancelled()) { return true; } return false; } /** *

Mark this request as cancelled and frees any resources held by the request if possible. * All subsequent calls to {@link #isCancelled()} on this Request return true.

* * @see #isCancelled() */ public void cancel() { synchronized (monitor) { if (cancel) return; if (timeoutManager != null && timeout != null) timeoutManager.unscheduleTimeout(this); cancel = true; } } /** *

Attempts to resolve and connect to the {@link RequestHandler} appropriate for the {@link URI} of this Request. * An exception is thrown if this operation fails at any point. This method is exception-safe.

* * @param responseHandler The handler to pass the corresponding {@link Response} to. * @return The {@link ContentChannel} to write the Request content to. * @throws NullPointerException If the {@link ResponseHandler} is null. * @throws BindingNotFoundException If the corresponding call to {@link Container#resolveHandler(Request)} returns * null. */ public ContentChannel connect(ResponseHandler responseHandler) { try { Objects.requireNonNull(responseHandler, "responseHandler"); RequestHandler requestHandler = container().resolveHandler(this); if (requestHandler == null) { throw new BindingNotFoundException(uri); } requestHandler = new ProxyRequestHandler(requestHandler); ContentChannel content = requestHandler.handleRequest(this, responseHandler); if (content == null) { throw new RequestDeniedException(this); } return content; } catch (Throwable t) { cancel(); throw t; } } @Override protected void destroy() { if (parentReference != null) { parentReference.close(); } if (container != null) { container.release(); } } }