/** * Jooby https://jooby.io * Apache License Version 2.0 https://jooby.io/LICENSE.txt * Copyright 2014 Edgar Espina */ package io.jooby; import io.jooby.annotations.Transactional; import io.jooby.exception.MethodNotAllowedException; import io.jooby.exception.NotAcceptableException; import io.jooby.exception.NotFoundException; import io.jooby.exception.UnsupportedMediaType; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.io.Serializable; import java.lang.reflect.Type; import java.util.Collection; import java.util.Collections; import java.util.Optional; import java.util.Set; import java.util.HashSet; import java.util.Map; import java.util.List; import java.util.ArrayList; import java.util.Arrays; import java.util.TreeMap; import java.util.concurrent.Executor; /** * Route contains information about the HTTP method, path pattern, which content types consumes and * produces, etc.. * * Additionally, contains metadata about route return Java type, argument source (query, path, etc..) and * Java type. * * This class contains all the metadata associated to a route. It is like a {@link Class} object * for routes. * * @author edgar * @since 2.0.0 */ public class Route { /** * Decorates a route handler by running logic before and after route handler. This pattern is * also known as Filter. * *
{@code
* {
* decorator(next -> ctx -> {
* long start = System.currentTimeMillis();
* Object result = next.apply(ctx);
* long end = System.currentTimeMillis();
* System.out.println("Took: " + (end - start));
* return result;
* });
* }
* }
*
* @author edgar
* @since 2.0.0
*/
public interface Decorator extends Aware {
/**
* Chain the decorator within next handler.
*
* @param next Next handler.
* @return A new handler.
*/
@Nonnull Handler apply(@Nonnull Handler next);
/**
* Chain this decorator with another and produces a new decorator.
*
* @param next Next decorator.
* @return A new decorator.
*/
@Nonnull default Decorator then(@Nonnull Decorator next) {
return h -> apply(next.apply(h));
}
/**
* Chain this decorator with a handler and produces a new handler.
*
* @param next Next handler.
* @return A new handler.
*/
@Nonnull default Handler then(@Nonnull Handler next) {
return ctx -> apply(next).apply(ctx);
}
}
/**
* Decorates a handler and run logic before handler is executed.
*
* @author edgar
* @since 2.0.0
*/
public interface Before {
/**
* Execute application code before next handler.
*
* @param ctx Web context.
* @throws Exception If something goes wrong.
*/
void apply(@Nonnull Context ctx) throws Exception;
/**
* Chain this filter with next one and produces a new before filter.
*
* @param next Next decorator.
* @return A new decorator.
*/
@Nonnull default Before then(@Nonnull Before next) {
return ctx -> {
apply(ctx);
if (!ctx.isResponseStarted()) {
next.apply(ctx);
}
};
}
/**
* Chain this decorator with a handler and produces a new handler.
*
* @param next Next handler.
* @return A new handler.
*/
@Nonnull default Handler then(@Nonnull Handler next) {
return ctx -> {
apply(ctx);
if (!ctx.isResponseStarted()) {
return next.apply(ctx);
}
return ctx;
};
}
}
/**
* Execute application logic after a response has been generated by a route handler.
*
* For functional handler the value is accessible and you are able to modify the response:
*
* {@code
* {
* after((ctx, result) -> {
* // Modify response
* ctx.setResponseHeader("foo", "bar");
* // do something with value:
* log.info("{} produces {}", ctx, result);
* });
*
* get("/", ctx -> {
* return "Functional value";
* });
* }
* }
*
* For side-effect handler (direct use of send methods, outputstream, writer, etc.) you are not
* allowed to modify the response or access to the value (value is always null):
*
* {@code
* {
* after((ctx, result) -> {
* // Always null:
* assertNull(result);
*
* // Response started is set to: true
* assertTrue(ctx.isResponseStarted());
* });
*
* get("/", ctx -> {
* return ctx.send("Side effect");
* });
* }
* }
*
* @author edgar
* @since 2.0.0
*/
public interface After {
/**
* Chain this filter with next one and produces a new after filter.
*
* @param next Next filter.
* @return A new filter.
*/
@Nonnull default After then(@Nonnull After next) {
return (ctx, result, failure) -> {
next.apply(ctx, result, failure);
apply(ctx, result, failure);
};
}
/**
* Execute application logic on a route response.
*
* @param ctx Web context.
* @param result Response generated by route handler.
* @param failure Uncaught exception generated by route handler.
* @throws Exception If something goes wrong.
*/
void apply(@Nonnull Context ctx, @Nullable Object result, @Nullable Throwable failure)
throws Exception;
}
/**
* Listener interface for events that are run at the completion of a request/response
* cycle (i.e. when the request has been completely read, and the response has been fully
* written).
*
* At this point it is too late to modify the exchange further.
*
* Completion listeners are invoked in reverse order.
*
* @author edgar
*/
public interface Complete {
/**
* Callback method.
*
* @param ctx Read-Only web context.
* @throws Exception If something goes wrong.
*/
void apply(@Nonnull Context ctx) throws Exception;
}
public interface Aware {
/**
* Allows a handler to listen for route metadata.
*
* @param route Route metadata.
*/
default void setRoute(Route route) {
}
}
/**
* Route handler here is where the application logic lives.
*
* @author edgar
* @since 2.0.0
*/
public interface Handler extends Serializable, Aware {
/**
* Execute application code.
*
* @param ctx Web context.
* @return Route response.
* @throws Exception If something goes wrong.
*/
@Nonnull Object apply(@Nonnull Context ctx) throws Exception;
/**
* Chain this after decorator with next and produces a new decorator.
*
* @param next Next decorator.
* @return A new handler.
*/
@Nonnull default Handler then(@Nonnull After next) {
return ctx -> {
Throwable cause = null;
Object value = null;
try {
value = apply(ctx);
} catch (Throwable x) {
cause = x;
// Early mark context as errored response code:
ctx.setResponseCode(ctx.getRouter().errorCode(cause));
}
Object result;
try {
if (ctx.isResponseStarted()) {
result = Context.readOnly(ctx);
next.apply((Context) result, value, cause);
} else {
result = value;
next.apply(ctx, value, cause);
}
} catch (Throwable x) {
result = null;
if (cause == null) {
cause = x;
} else {
cause.addSuppressed(x);
}
}
if (cause == null) {
return result;
} else {
if (ctx.isResponseStarted()) {
return ctx;
} else {
throw SneakyThrows.propagate(cause);
}
}
};
}
}
/**
* Handler for {@link StatusCode#NOT_FOUND} responses.
*/
public static final Handler NOT_FOUND = ctx -> ctx
.sendError(new NotFoundException(ctx.getRequestPath()));
/**
* Handler for {@link StatusCode#METHOD_NOT_ALLOWED} responses.
*/
public static final Handler METHOD_NOT_ALLOWED = ctx -> {
ctx.setResetHeadersOnError(false);
// Allow header was generated by routing algorithm
if (ctx.getMethod().equals(Router.OPTIONS)) {
return ctx.send(StatusCode.OK);
} else {
Listreserve(/{k1}/{k2}, {"k1": ""foo", "k2": "bar"}) => /foo/bar
*
* @param keys Path keys.
* @return Path.
*/
public @Nonnull String reverse(@Nonnull Mapreserve(/{k1}/{k2}, "foo", "bar") => /foo/bar
*
* @param values Values.
* @return Path.
*/
public @Nonnull String reverse(Object... values) {
return Router.reverse(getPattern(), values);
}
/**
* Handler instance which might or might not be the same as {@link #getHandler()}.
*
* The handle is required to extract correct metadata.
*
* @return Handle.
*/
public @Nonnull Object getHandle() {
return handle;
}
/**
* Before pipeline or null.
*
* @return Before pipeline or null.
*/
public @Nullable Before getBefore() {
return before;
}
/**
* Set before filter.
*
* @param before Before filter.
* @return This route.
*/
public @Nonnull Route setBefore(@Nullable Before before) {
this.before = before;
return this;
}
/**
* After filter or null.
*
* @return After filter or null.
*/
public @Nullable After getAfter() {
return after;
}
/**
* Set after filter.
*
* @param after After filter.
* @return This route.
*/
public @Nonnull Route setAfter(@Nonnull After after) {
this.after = after;
return this;
}
/**
* Decorator or null.
*
* @return Decorator or null.
*/
public @Nullable Decorator getDecorator() {
return decorator;
}
/**
* Set route decorator.
*
* @param decorator Decorator.
* @return This route.
*/
public @Nonnull Route setDecorator(@Nullable Decorator decorator) {
this.decorator = decorator;
return this;
}
/**
* Set route handle instance, required when handle is different from {@link #getHandler()}.
*
* @param handle Handle instance.
* @return This route.
*/
public @Nonnull Route setHandle(@Nonnull Object handle) {
this.handle = handle;
return this;
}
/**
* Set route pipeline. This method is part of public API but isn't intended to be used by public.
*
* @param pipeline Pipeline.
* @return This routes.
*/
public @Nonnull Route setPipeline(Route.Handler pipeline) {
this.pipeline = pipeline;
return this;
}
/**
* Route encoder.
*
* @return Route encoder.
*/
public @Nonnull MessageEncoder getEncoder() {
return encoder;
}
/**
* Set encoder.
*
* @param encoder MessageEncoder.
* @return This route.
*/
public @Nonnull Route setEncoder(@Nonnull MessageEncoder encoder) {
this.encoder = encoder;
return this;
}
/**
* Return return type.
*
* @return Return type.
*/
public @Nullable Type getReturnType() {
return returnType;
}
/**
* Set route return type.
*
* @param returnType Return type.
* @return This route.
*/
public @Nonnull Route setReturnType(@Nullable Type returnType) {
this.returnType = returnType;
return this;
}
/**
* Response types (format) produces by this route. If set, we expect to find a match in the
* Accept header. If none matches, we send a {@link StatusCode#NOT_ACCEPTABLE}
* response.
*
* @return Immutable list of produce types.
*/
public @Nonnull ListContent-Type header
* is checked against these values. If none matches we send a
* {@link StatusCode#UNSUPPORTED_MEDIA_TYPE} exception.
*
* @return Immutable list of consumed types.
*/
public @Nonnull Listnull.
*
* @return Executor key.
*/
public @Nullable String getExecutorKey() {
return executorKey;
}
/**
* Set executor key. The route is going to use the given key to fetch an executor. Possible values
* are:
*
* - null: no specific executor, uses the default Jooby logic to choose one, based
* on the value of {@link ExecutionMode};
* - worker: use the executor provided by the server.
* - arbitrary name: use an named executor which as registered using
* {@link Router#executor(String, Executor)}.
*
* @param executorKey Executor key.
* @return This route.
*/
public @Nonnull Route setExecutorKey(@Nullable String executorKey) {
this.executorKey = executorKey;
return this;
}
/**
* Route tags.
*
* @return Route tags.
*/
public @Nonnull List