/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.jooby;
import static com.google.common.base.Preconditions.checkArgument;
import static java.util.Objects.requireNonNull;
import static javaslang.API.Case;
import static javaslang.API.Match;
import static javaslang.Predicates.instanceOf;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.jooby.internal.RouteImpl;
import org.jooby.internal.RouteMatcher;
import org.jooby.internal.RoutePattern;
import org.jooby.internal.RouteSourceImpl;
import com.google.common.base.CaseFormat;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.primitives.Primitives;
import com.google.inject.Key;
import com.google.inject.TypeLiteral;
import com.google.inject.internal.util.SourceProvider;
import javaslang.CheckedFunction1;
/**
* Routes are a key concept in Jooby. Routes are executed in the same order they are defined.
*
* <h1>handlers</h1>
* <p>
* There are few type of handlers: {@link Route.Handler}, {@link Route.OneArgHandler}
* {@link Route.ZeroArgHandler} and {@link Route.Filter}. They behave very similar, except that a
* {@link Route.Filter} can decide if the next route handler can be executed or not. For example:
* </p>
*
* <pre>
* get("/filter", (req, rsp, chain) {@literal ->} {
* if (someCondition) {
* chain.next(req, rsp);
* } else {
* // respond, throw err, etc...
* }
* });
* </pre>
*
* While a {@link Route.Handler} always execute the next handler:
*
* <pre>
* get("/path", (req, rsp) {@literal ->} {
* rsp.send("handler");
* });
*
* // filter version
* get("/path", (req, rsp, chain) {@literal ->} {
* rsp.send("handler");
* chain.next(req, rsp);
* });
* </pre>
*
* The {@link Route.OneArgHandler} and {@link Route.ZeroArgHandler} offers a functional version of
* generating a response:
*
* <pre>{@code
* {
* get("/path", req -> "handler");
*
* get("/path", () -> "handler");
* }
* }</pre>
*
* There is no need to call {@link Response#send(Object)}.
*
* <h1>path patterns</h1>
* <p>
* Jooby supports Ant-style path patterns:
* </p>
* <p>
* Some examples:
* </p>
* <ul>
* <li>{@code com/t?st.html} - matches {@code com/test.html} but also {@code com/tast.jsp} or
* {@code com/txst.html}</li>
* <li>{@code com/*.html} - matches all {@code .html} files in the {@code com} directory</li>
* <li><code>com/{@literal **}/test.html</code> - matches all {@code test.html} files underneath the
* {@code com} path</li>
* <li>{@code **}/{@code *} - matches any path at any level.</li>
* <li>{@code *} - matches any path at any level, shorthand for {@code **}/{@code *}.</li>
* </ul>
*
* <h2>variables</h2>
* <p>
* Jooby supports path parameters too:
* </p>
* <p>
* Some examples:
* </p>
* <ul>
* <li><code> /user/{id}</code> - /user/* and give you access to the <code>id</code> var.</li>
* <li><code> /user/:id</code> - /user/* and give you access to the <code>id</code> var.</li>
* <li><code> /user/{id:\\d+}</code> - /user/[digits] and give you access to the numeric
* <code>id</code> var.</li>
* </ul>
*
* <h1>routes semantic</h1>
* <p>
* Routes are executed in the order they are defined, for example:
* </p>
*
* <pre>
* get("/", (req, rsp) {@literal ->} {
* log.info("first"); // start here and go to second
* });
*
* get("/", (req, rsp) {@literal ->} {
* log.info("second"); // execute after first and go to final
* });
*
* get("/", (req, rsp) {@literal ->} {
* rsp.send("final"); // done!
* });
* </pre>
*
* Please note first and second routes are converted to a filter, so previous example is the same
* as:
*
* <pre>
* get("/", (req, rsp, chain) {@literal ->} {
* log.info("first"); // start here and go to second
* chain.next(req, rsp);
* });
*
* get("/", (req, rsp, chain) {@literal ->} {
* log.info("second"); // execute after first and go to final
* chain.next(req, rsp);
* });
*
* get("/", (req, rsp) {@literal ->} {
* rsp.send("final"); // done!
* });
* </pre>
*
* <h2>script route</h2>
* <p>
* A script route can be defined using Lambda expressions, like:
* </p>
*
* <pre>
* get("/", (request, response) {@literal ->} {
* response.send("Hello Jooby");
* });
* </pre>
*
* Due to the use of lambdas a route is a singleton and you should NOT use global variables.
* For example this is a bad practice:
*
* <pre>
* List{@literal <}String{@literal >} names = new ArrayList{@literal <>}(); // names produces side effects
* get("/", (req, rsp) {@literal ->} {
* names.add(req.param("name").value();
* // response will be different between calls.
* rsp.send(names);
* });
* </pre>
*
* <h2>mvc Route</h2>
* <p>
* A Mvc Route use annotations to define routes:
* </p>
*
* <pre>
* {
* use(MyRoute.class);
* }
* </pre>
*
* MyRoute.java:
* <pre>
* {@literal @}Path("/")
* public class MyRoute {
*
* {@literal @}GET
* public String hello() {
* return "Hello Jooby";
* }
* }
* </pre>
* <p>
* Programming model is quite similar to JAX-RS/Jersey with some minor differences and/or
* simplifications.
* </p>
*
* <p>
* To learn more about Mvc Routes, please check {@link org.jooby.mvc.Path},
* {@link org.jooby.mvc.Produces} {@link org.jooby.mvc.Consumes}.
* </p>
*
* @author edgar
* @since 0.1.0
*/
public interface Route {
/**
* Provides useful information about where the route was defined.
*
* See {@link Definition#source()} and {@link Route#source()}.
*
* @author edgar
* @since 1.0.0.CR4
*/
interface Source {
/**
* There is no source information.
*/
Source BUILTIN = new Source() {
@Override
public int line() {
return -1;
}
@Override
public Optional<String> declaringClass() {
return Optional.empty();
}
@Override
public String toString() {
return "~builtin";
}
};
/**
* @return Line number where the route was defined or <code>-1</code> when not available.
*/
int line();
/**
* @return Class where the route
*/
Optional<String> declaringClass();
}
/**
* Converts a route output to something else, see {@link Router#map(Mapper)}.
*
* <pre>{@code
* {
* // we got bar.. not foo
* get("/foo", () -> "foo")
* .map(value -> "bar");
*
* // we got foo.. not bar
* get("/bar", () -> "bar")
* .map(value -> "foo");
* }
* }</pre>
*
* If you want to apply a single map to several routes:
*
* <pre>{@code
* {
* with(() -> {
* get("/foo", () -> "foo");
*
* get("/bar", () -> "bar");
*
* }).map(v -> "foo or bar");
* }
* }</pre>
*
* You can apply a {@link Mapper} to specific return type:
*
* <pre>{@code
* {
* with(() -> {
* get("/str", () -> "str");
*
* get("/int", () -> 1);
*
* }).map(String v -> "{" + v + "}");
* }
* }</pre>
*
* A call to <code>/str</code> produces <code>{str}</code>, while <code>/int</code> just
* <code>1</code>.
*
* <strong>NOTE</strong>: You can apply the map operator to routes that produces an output.
*
* For example, the map operator will be silently ignored here:
*
* <pre>{@code
* {
* get("/", (req, rsp) -> {
* rsp.send(...);
* }).map(v -> ..);
* }
* }</pre>
*
* @author edgar
* @param <T> Type to map.
*/
interface Mapper<T> {
/**
* Produces a new mapper by combining the two mapper into one.
*
* @param it The first mapper to apply.
* @param next The second mapper to apply.
* @return A new mapper.
*/
@SuppressWarnings({"rawtypes", "unchecked" })
static Mapper<Object> chain(final Mapper it, final Mapper next) {
return create(it.name() + ">" + next.name(), v -> next.map(it.map(v)));
}
/**
* Creates a new named mapper (just syntax suggar for creating a new mapper).
*
* @param name Mapper's name.
* @param fn Map function.
* @param <T> Value type.
* @return A new mapper.
*/
static <T> Mapper<T> create(final String name, final CheckedFunction1<T, Object> fn) {
return new Route.Mapper<T>() {
@Override
public String name() {
return name;
}
@Override
public Object map(final T value) throws Throwable {
return fn.apply(value);
}
@Override
public String toString() {
return name();
}
};
}
/**
* @return Mapper's name.
*/
default String name() {
String name = Optional.ofNullable(Strings.emptyToNull(getClass().getSimpleName()))
.orElseGet(() -> {
String classname = getClass().getName();
return classname.substring(classname.lastIndexOf('.') + 1);
});
return CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_HYPHEN, name);
}
/**
* Map the type to something else.
*
* @param value Value to map.
* @return Mapped value.
* @throws Throwable If mapping fails.
*/
Object map(T value) throws Throwable;
}
/**
* Common route properties, like static and global metadata via attributes, path exclusion,
* produces and consumes types.
*
* @author edgar
* @since 1.0.0.CR
* @param <T> Attribute subtype.
*/
interface Props<T extends Props<T>> {
/**
* Set route attribute. Only primitives, string, class, enum or array of previous types are
* allowed as attributes values.
*
* @param name Attribute's name.
* @param value Attribute's value.
* @return This instance.
*/
T attr(String name, Object value);
/**
* Tell jooby what renderer should use to render the output.
*
* @param name A renderer's name.
* @return This instance.
*/
default T renderer(final String name) {
return attr(RENDERER, name);
}
/**
* Set the route name. Route's name, helpful for debugging but also to implement dynamic and
* advanced routing. See {@link Route.Chain#next(String, Request, Response)}
*
*
* @param name A route's name.
* @return This instance.
*/
T name(final String name);
/**
* Set the media types the route can consume.
*
* @param consumes The media types to test for.
* @return This instance.
*/
default T consumes(final MediaType... consumes) {
return consumes(Arrays.asList(consumes));
}
/**
* Set the media types the route can consume.
*
* @param consumes The media types to test for.
* @return This instance.
*/
default T consumes(final String... consumes) {
return consumes(MediaType.valueOf(consumes));
}
/**
* Set the media types the route can consume.
*
* @param consumes The media types to test for.
* @return This instance.
*/
T consumes(final List<MediaType> consumes);
/**
* Set the media types the route can produces.
*
* @param produces The media types to test for.
* @return This instance.
*/
default T produces(final MediaType... produces) {
return produces(Arrays.asList(produces));
}
/**
* Set the media types the route can produces.
*
* @param produces The media types to test for.
* @return This instance.
*/
default T produces(final String... produces) {
return produces(MediaType.valueOf(produces));
}
/**
* Set the media types the route can produces.
*
* @param produces The media types to test for.
* @return This instance.
*/
T produces(final List<MediaType> produces);
/**
* Excludes one or more path pattern from this route, useful for filter:
*
* <pre>
* {
* use("*", req {@literal ->} {
* ...
* }).excludes("/logout");
* }
* </pre>
*
* @param excludes A path pattern.
* @return This instance.
*/
default T excludes(final String... excludes) {
return excludes(Arrays.asList(excludes));
}
/**
* Excludes one or more path pattern from this route, useful for filter:
*
* <pre>
* {
* use("*", req {@literal ->} {
* ...
* }).excludes("/logout");
* }
* </pre>
*
* @param excludes A path pattern.
* @return This instance.
*/
T excludes(final List<String> excludes);
T map(Mapper<?> mapper);
}
/**
* Group one ore more routes under a base path, see {@link Router#use(String)}.
*
* @author edgar
*/
class Group implements Props<Group> {
/** List of definitions. */
private List<Route.Definition> routes = new ArrayList<>();
private String rootPattern;
private String prefix;
public Group(final String pattern, final String prefix) {
requireNonNull(pattern, "Pattern is required.");
this.rootPattern = pattern;
this.prefix = prefix;
}
public Group(final String pattern) {
this(pattern, null);
}
public List<Route.Definition> routes() {
return routes;
}
// ********************************************************************************************
// ALL
// ********************************************************************************************
public Group all(final String pattern, final Route.Filter filter) {
newRoute("*", pattern, filter);
return this;
}
public Group all(final String pattern, final Route.Handler handler) {
newRoute("*", pattern, handler);
return this;
}
public Group all(final String pattern, final Route.OneArgHandler handler) {
newRoute("*", pattern, handler);
return this;
}
public Group all(final String pattern, final Route.ZeroArgHandler handler) {
newRoute("*", pattern, handler);
return this;
}
public Group all(final Route.Filter filter) {
newRoute("*", "", filter);
return this;
}
public Group all(final Route.Handler handler) {
newRoute("*", "", handler);
return this;
}
public Group all(final Route.OneArgHandler handler) {
newRoute("*", "", handler);
return this;
}
public Group all(final Route.ZeroArgHandler handler) {
newRoute("*", "", handler);
return this;
}
// ********************************************************************************************
// GET
// ********************************************************************************************
public Group get(final String pattern, final Route.Filter filter) {
newRoute(GET, pattern, filter);
return this;
}
public Group get(final String pattern, final Route.Handler handler) {
newRoute(GET, pattern, handler);
return this;
}
public Group get(final String pattern, final Route.OneArgHandler handler) {
newRoute(GET, pattern, handler);
return this;
}
public Group get(final String pattern, final Route.ZeroArgHandler handler) {
newRoute(GET, pattern, handler);
return this;
}
public Group get(final Route.Filter filter) {
newRoute(GET, "", filter);
return this;
}
public Group get(final Route.Handler handler) {
newRoute(GET, "", handler);
return this;
}
public Group get(final Route.OneArgHandler handler) {
newRoute(GET, "", handler);
return this;
}
public Group get(final Route.ZeroArgHandler handler) {
newRoute(GET, "", handler);
return this;
}
// ********************************************************************************************
// POST
// ********************************************************************************************
public Group post(final String pattern, final Route.Filter filter) {
newRoute(POST, pattern, filter);
return this;
}
public Group post(final String pattern, final Route.Handler handler) {
newRoute(POST, pattern, handler);
return this;
}
public Group post(final String pattern, final Route.OneArgHandler handler) {
newRoute(POST, pattern, handler);
return this;
}
public Group post(final String pattern, final Route.ZeroArgHandler handler) {
newRoute(POST, pattern, handler);
return this;
}
public Group post(final Route.Filter filter) {
newRoute(POST, "", filter);
return this;
}
public Group post(final Route.Handler handler) {
newRoute(POST, "", handler);
return this;
}
public Group post(final Route.OneArgHandler handler) {
newRoute(POST, "", handler);
return this;
}
public Group post(final Route.ZeroArgHandler handler) {
newRoute(POST, "", handler);
return this;
}
// ********************************************************************************************
// PUT
// ********************************************************************************************
public Group put(final String pattern, final Route.Filter filter) {
newRoute(PUT, pattern, filter);
return this;
}
public Group put(final String pattern, final Route.Handler handler) {
newRoute(PUT, pattern, handler);
return this;
}
public Group put(final String pattern, final Route.OneArgHandler handler) {
newRoute(PUT, pattern, handler);
return this;
}
public Group put(final String pattern, final Route.ZeroArgHandler handler) {
newRoute(PUT, pattern, handler);
return this;
}
public Group put(final Route.Filter filter) {
newRoute(PUT, "", filter);
return this;
}
public Group put(final Route.Handler handler) {
newRoute(PUT, "", handler);
return this;
}
public Group put(final Route.OneArgHandler handler) {
newRoute(PUT, "", handler);
return this;
}
public Group put(final Route.ZeroArgHandler handler) {
newRoute(PUT, "", handler);
return this;
}
// ********************************************************************************************
// DELETE
// ********************************************************************************************
public Group delete(final String pattern, final Route.Filter filter) {
newRoute(DELETE, pattern, filter);
return this;
}
public Group delete(final String pattern, final Route.Handler handler) {
newRoute(DELETE, pattern, handler);
return this;
}
public Group delete(final String pattern, final Route.OneArgHandler handler) {
newRoute(DELETE, pattern, handler);
return this;
}
public Group delete(final String pattern, final Route.ZeroArgHandler handler) {
newRoute(DELETE, pattern, handler);
return this;
}
public Group delete(final Route.Filter filter) {
newRoute(DELETE, "", filter);
return this;
}
public Group delete(final Route.Handler handler) {
newRoute(DELETE, "", handler);
return this;
}
public Group delete(final Route.OneArgHandler handler) {
newRoute(DELETE, "", handler);
return this;
}
public Group delete(final Route.ZeroArgHandler handler) {
newRoute(DELETE, "", handler);
return this;
}
// ********************************************************************************************
// PATCH
// ********************************************************************************************
public Group patch(final String pattern, final Route.Filter filter) {
newRoute(PATCH, pattern, filter);
return this;
}
public Group patch(final String pattern, final Route.Handler handler) {
newRoute(PATCH, pattern, handler);
return this;
}
public Group patch(final String pattern, final Route.OneArgHandler handler) {
newRoute(PATCH, pattern, handler);
return this;
}
public Group patch(final String pattern, final Route.ZeroArgHandler handler) {
newRoute(PATCH, pattern, handler);
return this;
}
public Group patch(final Route.Filter filter) {
newRoute(PATCH, "", filter);
return this;
}
public Group patch(final Route.Handler handler) {
newRoute(PATCH, "", handler);
return this;
}
public Group patch(final Route.OneArgHandler handler) {
newRoute(PATCH, "", handler);
return this;
}
public Group patch(final Route.ZeroArgHandler handler) {
newRoute(PATCH, "", handler);
return this;
}
/**
* Set the route name to the whole collection.
*
* @param name Name to use/set.
* @return This instance.
*/
@Override
public Group name(final String name) {
for (Definition definition : routes) {
if (prefix != null) {
definition.name(prefix + "/" + name);
} else {
definition.name(name);
}
}
return this;
}
@Override
public Group consumes(final List<MediaType> types) {
for (Definition definition : routes) {
definition.consumes(types);
}
return this;
}
@Override
public Group produces(final List<MediaType> types) {
for (Definition definition : routes) {
definition.produces(types);
}
return this;
}
@Override
public Group attr(final String name, final Object value) {
for (Definition definition : routes) {
definition.attr(name, value);
}
return this;
}
@Override
public Group excludes(final List<String> excludes) {
for (Definition definition : routes) {
definition.excludes(excludes);
}
return this;
}
@Override
public Group map(final Mapper<?> mapper) {
for (Definition definition : routes) {
definition.map(mapper);
}
return this;
}
private void newRoute(final String method, final String pattern,
final Route.Filter filter) {
newRoute(new Route.Definition(method, this.rootPattern + pattern, filter));
}
private void newRoute(final String method, final String pattern,
final Route.Handler filter) {
newRoute(new Route.Definition(method, this.rootPattern + pattern, filter));
}
private void newRoute(final String method, final String pattern,
final Route.OneArgHandler filter) {
newRoute(new Route.Definition(method, this.rootPattern + pattern, filter));
}
private void newRoute(final String method, final String pattern,
final Route.ZeroArgHandler filter) {
newRoute(new Route.Definition(method, this.rootPattern + pattern, filter));
}
private void newRoute(final Route.Definition route) {
if (prefix != null) {
route.name(prefix);
}
routes.add(route);
}
};
/**
* Collection of {@link Route.Props} useful for registering/setting route options at once.
*
* See {@link Router#get(String, String, String, OneArgHandler)} and variants.
*
* @author edgar
* @since 0.5.0
*/
@SuppressWarnings({"unchecked", "rawtypes" })
class Collection implements Props<Collection> {
/** List of definitions. */
private final Route.Props[] routes;
/**
* Creates a new collection of route definitions.
*
* @param definitions Collection of route definitions.
*/
public Collection(final Route.Props... definitions) {
this.routes = requireNonNull(definitions, "Route definitions are required.");
}
@Override
public Collection name(final String name) {
for (Props definition : routes) {
definition.name(name);
}
return this;
}
@Override
public Collection consumes(final List<MediaType> types) {
for (Props definition : routes) {
definition.consumes(types);
}
return this;
}
@Override
public Collection produces(final List<MediaType> types) {
for (Props definition : routes) {
definition.produces(types);
}
return this;
}
@Override
public Collection attr(final String name, final Object value) {
for (Props definition : routes) {
definition.attr(name, value);
}
return this;
}
@Override
public Collection excludes(final List<String> excludes) {
for (Props definition : routes) {
definition.excludes(excludes);
}
return this;
}
@Override
public Collection map(final Mapper<?> mapper) {
for (Props route : routes) {
route.map(mapper);
}
return this;
}
}
/**
* DSL for customize routes.
*
* <p>
* Some examples:
* </p>
*
* <pre>
* public class MyApp extends Jooby {
* {
* get("/", () {@literal ->} "GET");
*
* post("/", req {@literal ->} "POST");
*
* put("/", (req, rsp) {@literal ->} rsp.send("PUT"));
* }
* }
* </pre>
*
* <h1>Setting what a route can consumes</h1>
*
* <pre>
* public class MyApp extends Jooby {
* {
* post("/", (req, resp) {@literal ->} resp.send("POST"))
* .consumes(MediaType.json);
* }
* }
* </pre>
*
* <h1>Setting what a route can produces</h1>
*
* <pre>
* public class MyApp extends Jooby {
* {
* post("/", (req, resp) {@literal ->} resp.send("POST"))
* .produces(MediaType.json);
* }
* }
* </pre>
*
* <h1>Adding a name</h1>
*
* <pre>
* public class MyApp extends Jooby {
* {
* post("/", (req, resp) {@literal ->} resp.send("POST"))
* .name("My Root");
* }
* }
* </pre>
*
* @author edgar
* @since 0.1.0
*/
class Definition implements Props<Definition> {
private static final SourceProvider SRC = SourceProvider.DEFAULT_INSTANCE
.plusSkippedClasses(Definition.class, Jooby.class, Collection.class, Group.class,
javaslang.collection.List.class, Router.class, Forwarding.class, Deferred.class);
/**
* Route's name.
*/
private String name = "/anonymous";
/**
* A route pattern.
*/
private RoutePattern cpattern;
/**
* The target route.
*/
private Filter filter;
/**
* Defines the media types that the methods of a resource class or can accept. Default is:
* {@code *}/{@code *}.
*/
private List<MediaType> consumes = MediaType.ALL;
/**
* Defines the media types that the methods of a resource class or can produces. Default is:
* {@code *}/{@code *}.
*/
private List<MediaType> produces = MediaType.ALL;
/**
* A HTTP verb or <code>*</code>.
*/
private String method;
/**
* A path pattern.
*/
private String pattern;
private List<RoutePattern> excludes = Collections.emptyList();
private Map<String, Object> attributes = ImmutableMap.of();
private Mapper<?> mapper;
private int line;
private String declaringClass;
String prefix;
/**
* Creates a new route definition.
*
* @param verb A HTTP verb or <code>*</code>.
* @param pattern A path pattern.
* @param handler A route handler.
*/
public Definition(final String verb, final String pattern,
final Route.Handler handler) {
this(verb, pattern, (Route.Filter) handler);
}
/**
* Creates a new route definition.
*
* @param verb A HTTP verb or <code>*</code>.
* @param pattern A path pattern.
* @param handler A route handler.
*/
public Definition(final String verb, final String pattern,
final Route.OneArgHandler handler) {
this(verb, pattern, (Route.Filter) handler);
}
/**
* Creates a new route definition.
*
* @param verb A HTTP verb or <code>*</code>.
* @param pattern A path pattern.
* @param handler A route handler.
*/
public Definition(final String verb, final String pattern,
final Route.ZeroArgHandler handler) {
this(verb, pattern, (Route.Filter) handler);
}
/**
* Creates a new route definition.
*
* @param method A HTTP verb or <code>*</code>.
* @param pattern A path pattern.
* @param filter A callback to execute.
*/
public Definition(final String method, final String pattern,
final Filter filter) {
requireNonNull(pattern, "A route path is required.");
requireNonNull(filter, "A filter is required.");
this.method = method.toUpperCase();
this.cpattern = new RoutePattern(method, pattern);
// normalized pattern
this.pattern = cpattern.pattern();
this.filter = filter;
StackTraceElement source = SRC.get(new Throwable().getStackTrace());
this.line = source.getLineNumber();
this.declaringClass = source.getClassName();
}
/**
* <h1>Path Patterns</h1>
* <p>
* Jooby supports Ant-style path patterns:
* </p>
* <p>
* Some examples:
* </p>
* <ul>
* <li>{@code com/t?st.html} - matches {@code com/test.html} but also {@code com/tast.jsp} or
* {@code com/txst.html}</li>
* <li>{@code com/*.html} - matches all {@code .html} files in the {@code com} directory</li>
* <li><code>com/{@literal **}/test.html</code> - matches all {@code test.html} files underneath
* the {@code com} path</li>
* <li>{@code **}/{@code *} - matches any path at any level.</li>
* <li>{@code *} - matches any path at any level, shorthand for {@code **}/{@code *}.</li>
* </ul>
*
* <h2>Variables</h2>
* <p>
* Jooby supports path parameters too:
* </p>
* <p>
* Some examples:
* </p>
* <ul>
* <li><code> /user/{id}</code> - /user/* and give you access to the <code>id</code> var.</li>
* <li><code> /user/:id</code> - /user/* and give you access to the <code>id</code> var.</li>
* <li><code> /user/{id:\\d+}</code> - /user/[digits] and give you access to the numeric
* <code>id</code> var.</li>
* </ul>
*
* @return A path pattern.
*/
public String pattern() {
return pattern;
}
/**
* @return List of path variables (if any).
*/
public List<String> vars() {
return cpattern.vars();
}
/**
* Indicates if the {@link #pattern()} contains a glob charecter, like <code>?</code>,
* <code>*</code> or <code>**</code>.
*
* @return Indicates if the {@link #pattern()} contains a glob charecter, like <code>?</code>,
* <code>*</code> or <code>**</code>.
*/
public boolean glob() {
return cpattern.glob();
}
/**
* Source information (where the route was defined).
*
* @return Source information (where the route was defined).
*/
public Route.Source source() {
return new RouteSourceImpl(declaringClass, line);
}
/**
* Recreate a route path and apply the given variables.
*
* @param vars Path variables.
* @return A route pattern.
*/
public String reverse(final Map<String, Object> vars) {
return cpattern.reverse(vars);
}
/**
* Recreate a route path and apply the given variables.
*
* @param values Path variable values.
* @return A route pattern.
*/
public String reverse(final Object... values) {
return cpattern.reverse(values);
}
@Override
public Definition attr(final String name, final Object value) {
requireNonNull(name, "Attribute name is required.");
requireNonNull(value, "Attribute value is required.");
if (valid(value)) {
attributes = ImmutableMap.<String, Object> builder()
.putAll(attributes)
.put(name, value)
.build();
}
return this;
}
private boolean valid(final Object value) {
return Match(value).option(
Case(v -> Primitives.isWrapperType(Primitives.wrap(v.getClass())), true),
Case(instanceOf(String.class), true),
Case(instanceOf(Enum.class), true),
Case(instanceOf(Class.class), true),
Case(c -> c.getClass().isArray(), v -> valid(Array.get(v, 0))))
.getOrElse(false);
}
/**
* Get an attribute by name.
*
* @param name Attribute's name.
* @param <T> Attribute's type.
* @return Attribute's value or <code>null</code>.
*/
@SuppressWarnings("unchecked")
public <T> T attr(final String name) {
return (T) attributes.get(name);
}
/**
* @return A read only view of attributes.
*/
public Map<String, Object> attributes() {
return attributes;
}
/**
* Test if the route matches the given verb, path, content type and accept header.
*
* @param method A HTTP verb.
* @param path Current HTTP path.
* @param contentType The <code>Content-Type</code> header.
* @param accept The <code>Accept</code> header.
* @return A route or an empty optional.
*/
public Optional<Route> matches(final String method,
final String path, final MediaType contentType,
final List<MediaType> accept) {
String fpath = method + path;
if (excludes.size() > 0 && excludes(fpath)) {
return Optional.empty();
}
RouteMatcher matcher = cpattern.matcher(fpath);
if (matcher.matches()) {
List<MediaType> result = MediaType.matcher(accept).filter(this.produces);
if (result.size() > 0 && canConsume(contentType)) {
// keep accept when */*
List<MediaType> produces = result.size() == 1 && result.get(0).name().equals("*/*")
? accept : this.produces;
return Optional
.of(asRoute(method, matcher, produces, new RouteSourceImpl(declaringClass, line)));
}
}
return Optional.empty();
}
/**
* @return HTTP method or <code>*</code>.
*/
public String method() {
return method;
}
/**
* @return Handler behind this route.
*/
public Route.Filter filter() {
return filter;
}
/**
* Route's name, helpful for debugging but also to implement dynamic and advanced routing. See
* {@link Route.Chain#next(String, Request, Response)}
*
* @return Route name. Default is: <code>anonymous</code>.
*/
public String name() {
return name;
}
/**
* Set the route name. Route's name, helpful for debugging but also to implement dynamic and
* advanced routing. See {@link Route.Chain#next(String, Request, Response)}
*
*
* @param name A route's name.
* @return This definition.
*/
@Override
public Definition name(final String name) {
checkArgument(!Strings.isNullOrEmpty(name), "A route's name is required.");
this.name = normalize(prefix != null ? prefix + "/" + name : name);
return this;
}
/**
* Test if the route definition can consume a media type.
*
* @param type A media type to test.
* @return True, if the route can consume the given media type.
*/
public boolean canConsume(final MediaType type) {
return MediaType.matcher(Arrays.asList(type)).matches(consumes);
}
/**
* Test if the route definition can consume a media type.
*
* @param type A media type to test.
* @return True, if the route can consume the given media type.
*/
public boolean canConsume(final String type) {
return MediaType.matcher(MediaType.valueOf(type)).matches(consumes);
}
/**
* Test if the route definition can consume a media type.
*
* @param types A media types to test.
* @return True, if the route can produces the given media type.
*/
public boolean canProduce(final List<MediaType> types) {
return MediaType.matcher(types).matches(produces);
}
/**
* Test if the route definition can consume a media type.
*
* @param types A media types to test.
* @return True, if the route can produces the given media type.
*/
public boolean canProduce(final MediaType... types) {
return canProduce(Arrays.asList(types));
}
/**
* Test if the route definition can consume a media type.
*
* @param types A media types to test.
* @return True, if the route can produces the given media type.
*/
public boolean canProduce(final String... types) {
return canProduce(MediaType.valueOf(types));
}
@Override
public Definition consumes(final List<MediaType> types) {
checkArgument(types != null && types.size() > 0, "Consumes types are required");
if (types.size() > 1) {
this.consumes = Lists.newLinkedList(types);
Collections.sort(this.consumes);
} else {
this.consumes = ImmutableList.of(types.get(0));
}
return this;
}
@Override
public Definition produces(final List<MediaType> types) {
checkArgument(types != null && types.size() > 0, "Produces types are required");
if (types.size() > 1) {
this.produces = Lists.newLinkedList(types);
Collections.sort(this.produces);
} else {
this.produces = ImmutableList.of(types.get(0));
}
return this;
}
@Override
public Definition excludes(final List<String> excludes) {
this.excludes = excludes.stream()
.map(it -> new RoutePattern(method, it))
.collect(Collectors.toList());
return this;
}
/**
* @return List of exclusion filters (if any).
*/
public List<String> excludes() {
return excludes.stream().map(r -> r.pattern()).collect(Collectors.toList());
}
private boolean excludes(final String path) {
for (RoutePattern pattern : excludes) {
if (pattern.matcher(path).matches()) {
return true;
}
}
return false;
}
/**
* @return All the types this route can consumes.
*/
public List<MediaType> consumes() {
return Collections.unmodifiableList(this.consumes);
}
/**
* @return All the types this route can produces.
*/
public List<MediaType> produces() {
return Collections.unmodifiableList(this.produces);
}
@Override
public Definition map(final Mapper<?> mapper) {
this.mapper = requireNonNull(mapper, "Mapper is required.");
return this;
}
/**
* Set the line where this route is defined.
*
* @param line Line number.
* @return This instance.
*/
public Definition line(final int line) {
this.line = line;
return this;
}
/**
* Set the class where this route is defined.
*
* @param declaringClass A source class.
* @return This instance.
*/
public Definition declaringClass(final String declaringClass) {
this.declaringClass = declaringClass;
return this;
}
@Override
public String toString() {
StringBuilder buffer = new StringBuilder();
buffer.append(method()).append(" ").append(pattern()).append("\n");
buffer.append(" name: ").append(name()).append("\n");
buffer.append(" excludes: ").append(excludes).append("\n");
buffer.append(" consumes: ").append(consumes()).append("\n");
buffer.append(" produces: ").append(produces()).append("\n");
return buffer.toString();
}
/**
* Creates a new route.
*
* @param method A HTTP verb.
* @param matcher A route matcher.
* @param produces List of produces types.
* @param source Route source.
* @return A new route.
*/
private Route asRoute(final String method, final RouteMatcher matcher,
final List<MediaType> produces, final Route.Source source) {
return new RouteImpl(filter, this, method, matcher.path(), produces,
matcher.vars(), mapper, source);
}
}
/**
* A forwarding route.
*
* @author edgar
* @since 0.1.0
*/
class Forwarding implements Route {
/**
* Target route.
*/
private final Route route;
/**
* Creates a new {@link Forwarding} route.
*
* @param route A target route.
*/
public Forwarding(final Route route) {
this.route = requireNonNull(route, "A route is required.");
}
@Override
public String path() {
return route.path();
}
@Override
public String method() {
return route.method();
}
@Override
public String pattern() {
return route.pattern();
}
@Override
public String name() {
return route.name();
}
@Override
public Map<Object, String> vars() {
return route.vars();
}
@Override
public List<MediaType> consumes() {
return route.consumes();
}
@Override
public List<MediaType> produces() {
return route.produces();
}
@Override
public Map<String, Object> attributes() {
return route.attributes();
}
@Override
public <T> T attr(final String name) {
return route.attr(name);
}
@Override
public boolean glob() {
return route.glob();
}
@Override
public String reverse(final Map<String, Object> vars) {
return route.reverse(vars);
}
@Override
public String reverse(final Object... values) {
return route.reverse(values);
}
@Override
public Source source() {
return route.source();
}
@Override
public String print() {
return route.print();
}
@Override
public String print(final int indent) {
return route.print(indent);
}
@Override
public String toString() {
return route.toString();
}
/**
* Find a target route.
*
* @param route A route to check.
* @return A target route.
*/
public static Route unwrap(final Route route) {
requireNonNull(route, "A route is required.");
Route root = route;
while (root instanceof Forwarding) {
root = ((Forwarding) root).route;
}
return root;
}
}
/**
* The most advanced route handler which let you decided if the next route handler in the chain
* can be executed or not. Example of filters are:
*
* <p>
* Auth handler example:
* </p>
*
* <pre>
* String token = req.header("token").value();
* if (token != null) {
* // validate token...
* if (valid(token)) {
* chain.next(req, rsp);
* }
* } else {
* rsp.status(403);
* }
* </pre>
*
* <p>
* Logging/Around handler example:
* </p>
*
* <pre>
* long start = System.currentTimeMillis();
* chain.next(req, rsp);
* long end = System.currentTimeMillis();
* log.info("Request: {} took {}ms", req.path(), end - start);
* </pre>
*
* NOTE: Don't forget to call {@link Route.Chain#next(Request, Response)} if next route handler
* need to be executed.
*
* @author edgar
* @since 0.1.0
*/
public interface Filter {
/**
* The <code>handle</code> method of the Filter is called by the server each time a
* request/response pair is passed through the chain due to a client request for a resource at
* the end of the chain.
* The {@link Route.Chain} passed in to this method allows the Filter to pass on the request and
* response to the next entity in the chain.
*
* <p>
* A typical implementation of this method would follow the following pattern:
* </p>
* <ul>
* <li>Examine the request</li>
* <li>Optionally wrap the request object with a custom implementation to filter content or
* headers for input filtering</li>
* <li>Optionally wrap the response object with a custom implementation to filter content or
* headers for output filtering</li>
* <li>
* <ul>
* <li><strong>Either</strong> invoke the next entity in the chain using the {@link Route.Chain}
* object (<code>chain.next(req, rsp)</code>),</li>
* <li><strong>or</strong> not pass on the request/response pair to the next entity in the
* filter chain to block the request processing</li>
* </ul>
* <li>Directly set headers on the response after invocation of the next entity in the filter
* chain.</li>
* </ul>
*
* @param req A HTTP request.
* @param rsp A HTTP response.
* @param chain A route chain.
* @throws Throwable If something goes wrong.
*/
void handle(Request req, Response rsp, Route.Chain chain) throws Throwable;
}
/**
* A route handler that always call {@link Chain#next(Request, Response)}.
*
* <pre>
* public class MyApp extends Jooby {
* {
* get("/", (req, rsp) {@literal ->} rsp.send("Hello"));
* }
* }
* </pre>
*
* @author edgar
* @since 0.1.0
*/
interface Handler extends Filter {
@Override
default void handle(final Request req, final Response rsp, final Route.Chain chain)
throws Throwable {
handle(req, rsp);
chain.next(req, rsp);
}
/**
* Callback method for a HTTP request.
*
* @param req A HTTP request.
* @param rsp A HTTP response.
* @throws Throwable If something goes wrong. The exception will processed by Jooby.
*/
void handle(Request req, Response rsp) throws Throwable;
}
/**
* A handler for a MVC route, it extends {@link Handler} by adding a reference to the method
* behind this route.
*
* @author edgar
* @since 0.6.2
*/
interface MethodHandler extends Handler {
Method method();
}
/**
* A functional route handler that use the return value as HTTP response.
*
* <pre>
* {
* get("/",(req {@literal ->} "Hello");
* }
* </pre>
*
* @author edgar
* @since 0.1.1
*/
interface OneArgHandler extends Filter {
@Override
default void handle(final Request req, final Response rsp, final Route.Chain chain)
throws Throwable {
Object result = handle(req);
rsp.send(result);
chain.next(req, rsp);
}
/**
* Callback method for a HTTP request.
*
* @param req A HTTP request.
* @return Message to send.
* @throws Throwable If something goes wrong. The exception will processed by Jooby.
*/
Object handle(Request req) throws Throwable;
}
/**
* A functional handler that use the return value as HTTP response.
*
* <pre>
* public class MyApp extends Jooby {
* {
* get("/", () {@literal ->} "Hello");
* }
* }
* </pre>
*
* @author edgar
* @since 0.1.1
*/
interface ZeroArgHandler extends Filter {
@Override
default void handle(final Request req, final Response rsp, final Route.Chain chain)
throws Throwable {
Object result = handle();
rsp.send(result);
chain.next(req, rsp);
}
/**
* Callback method for a HTTP request.
*
* @return Message to send.
* @throws Throwable If something goes wrong. The exception will processed by Jooby.
*/
Object handle() throws Throwable;
}
/**
* <h2>before</h2>
*
* Allows for customized handler execution chains. It will be invoked before the actual handler.
*
* <pre>{@code
* {
* before((req, rsp) -> {
* // your code goes here
* });
* }
* }</pre>
*
* You are allowed to modify the request and response objects.
*
* Please note that the <code>before</code> handler is just syntax sugar for {@link Route.Filter}.
* For example, the <code>before</code> handler was implemented as:
*
* <pre>{@code
* {
* use("*", "*", (req, rsp, chain) -> {
* before(req, rsp);
* // your code goes here
* chain.next(req, rsp);
* });
* }
* }</pre>
*
* A <code>before</code> handler must to be registered before the actual handler you want to
* intercept.
*
* <pre>{@code
* {
* before((req, rsp) -> {
* // your code goes here
* });
*
* get("/path", req -> {
* // your code goes here
* return ...;
* });
* }
* }</pre>
*
* If you reverse the order then it won't work.
*
* <p>
* <strong>Remember</strong>: routes are executed in the order they are defined and the pipeline
* is executed as long you don't generate a response.
* </p>
*
* @author edgar
* @since 1.0.0.CR
*/
interface Before extends Route.Filter {
@Override
default void handle(final Request req, final Response rsp, final Chain chain) throws Throwable {
handle(req, rsp);
chain.next(req, rsp);
}
/**
* Allows for customized handler execution chains. It will be invoked before the actual handler.
*
* @param req Request.
* @param rsp Response
* @throws Throwable If something goes wrong.
*/
void handle(Request req, Response rsp) throws Throwable;
}
/**
* <h2>after</h2>
*
* Allows for customized response before send it. It will be invoked at the time a response need
* to be send.
*
* <pre>{@code
* {
* after("GET", "*", (req, rsp, result) -> {
* // your code goes here
* return result;
* });
* }
* }</pre>
*
* You are allowed to modify the request, response and result objects. The handler returns a
* {@link Result} which can be the same or an entirely new {@link Result}.
*
* Please note that the <code>after</code> handler is just syntax sugar for
* {@link Route.Filter}.
* For example, the <code>after</code> handler was implemented as:
*
* <pre>{@code
* {
* use("GET", "*", (req, rsp, chain) -> {
* chain.next(req, new Response.Forwarding(rsp) {
* public void send(Result result) {
* rsp.send(after(req, rsp, result);
* }
* });
* });
* }
* }</pre>
*
* Due <code>after</code> is implemented by wrapping the {@link Response} object. A
* <code>after</code> handler must to be registered before the actual handler you want to
* intercept.
*
* <pre>{@code
* {
* after("GET", "/path", (req, rsp, result) -> {
* // your code goes here
* return result;
* });
*
* get("/path", req -> {
* return "hello";
* });
* }
* }</pre>
*
* If you reverse the order then it won't work.
*
* <p>
* <strong>Remember</strong>: routes are executed in the order they are defined and the pipeline
* is executed as long you don't generate a response.
* </p>
*
* @author edgar
* @since 1.0.0.CR
*/
interface After extends Filter {
@Override
default void handle(final Request req, final Response rsp, final Chain chain) throws Throwable {
rsp.after(this);
chain.next(req, rsp);
}
/**
* Allows for customized response before send it. It will be invoked at the time a response need
* to be send.
*
* @param req Request.
* @param rsp Response
* @param result Result.
* @return Same or new result.
* @throws Exception If something goes wrong.
*/
Result handle(Request req, Response rsp, Result result) throws Exception;
}
/**
* <h2>complete</h2>
*
* Allows for log and cleanup a request. It will be invoked after we send a response.
*
* <pre>{@code
* {
* complete((req, rsp, cause) -> {
* // your code goes here
* });
* }
* }</pre>
*
* You are NOT allowed to modify the request and response objects. The <code>cause</code> is an
* {@link Optional} with a {@link Throwable} useful to identify problems.
*
* The goal of the <code>complete</code> handler is to probably cleanup request object and log
* responses.
*
* Please note that the <code>complete</code> handler is just syntax sugar for
* {@link Route.Filter}.
* For example, the <code>complete</code> handler was implemented as:
*
* <pre>{@code
* {
* use("*", "*", (req, rsp, chain) -> {
* Optional<Throwable> err = Optional.empty();
* try {
* chain.next(req, rsp);
* } catch (Throwable cause) {
* err = Optional.of(cause);
* } finally {
* complete(req, rsp, err);
* }
* });
* }
* }</pre>
*
* An <code>complete</code> handler must to be registered before the actual handler you want to
* intercept.
*
* <pre>{@code
* {
* complete((req, rsp, cause) -> {
* // your code goes here
* });
*
* get(req -> {
* return "hello";
* });
* }
* }</pre>
*
* If you reverse the order then it won't work.
*
* <p>
* <strong>Remember</strong>: routes are executed in the order they are defined and the pipeline
* is executed as long you don't generate a response.
* </p>
*
* <h2>example</h2>
* <p>
* Suppose you have a transactional resource, like a database connection. The next example shows
* you how to implement a simple and effective <code>transaction-per-request</code> pattern:
* </p>
*
* <pre>{@code
* {
* // start transaction
* before((req, rsp) -> {
* DataSource ds = req.require(DataSource.class);
* Connection connection = ds.getConnection();
* Transaction trx = connection.getTransaction();
* trx.begin();
* req.set("connection", connection);
* return true;
* });
*
* // commit/rollback transaction
* complete((req, rsp, cause) -> {
* // unbind connection from request
* try(Connection connection = req.unset("connection").get()) {
* Transaction trx = connection.getTransaction();
* if (cause.ifPresent()) {
* trx.rollback();
* } else {
* trx.commit();
* }
* }
* });
*
* // your transactional routes goes here
* get("/api/something", req -> {
* Connection connection = req.get("connection");
* // work with connection
* });
* }
* }</pre>
*
* @author edgar
* @since 1.0.0.CR
*/
interface Complete extends Filter {
@Override
default void handle(final Request req, final Response rsp, final Chain chain) throws Throwable {
rsp.complete(this);
chain.next(req, rsp);
}
/**
* Allows for log and cleanup a request. It will be invoked after we send a response.
*
* @param req Request.
* @param rsp Response
* @param cause Empty optional on success. Otherwise, it contains the exception.
*/
void handle(Request req, Response rsp, Optional<Throwable> cause);
}
/**
* Chain of routes to be executed. It invokes the next route in the chain.
*
* @author edgar
* @since 0.1.0
*/
interface Chain {
/**
* Invokes the next route in the chain where {@link Route#name()} starts with the given prefix.
*
* @param prefix Iterates over the route chain and keep routes that start with the given prefix.
* @param req A HTTP request.
* @param rsp A HTTP response.
* @throws Throwable If invocation goes wrong.
*/
void next(String prefix, Request req, Response rsp) throws Throwable;
/**
* Invokes the next route in the chain.
*
* @param req A HTTP request.
* @param rsp A HTTP response.
* @throws Throwable If invocation goes wrong.
*/
default void next(final Request req, final Response rsp) throws Throwable {
next(null, req, rsp);
}
}
/** Route key. */
Key<Set<Route.Definition>> KEY = Key.get(new TypeLiteral<Set<Route.Definition>>() {
});
char OUT_OF_PATH = '\u200B';
String GET = "GET";
String POST = "POST";
String PUT = "PUT";
String DELETE = "DELETE";
String PATCH = "PATCH";
String HEAD = "HEAD";
String CONNECT = "CONNECT";
String OPTIONS = "OPTIONS";
String TRACE = "TRACE";
/**
* Well known HTTP methods.
*/
List<String> METHODS = ImmutableList.<String> builder()
.add(GET,
POST,
PUT,
DELETE,
PATCH,
HEAD,
CONNECT,
OPTIONS,
TRACE)
.build();
/**
* Renderer attribute.
*
* @see Route.Definition#renderer(String)
*/
String RENDERER = "renderer";
/**
* @return Current request path.
*/
String path();
/**
* @return Current HTTP method.
*/
String method();
/**
* @return The currently matched pattern.
*/
String pattern();
/**
* Route's name, helpful for debugging but also to implement dynamic and advanced routing. See
* {@link Route.Chain#next(String, Request, Response)}
*
* @return Route name, defaults to <code>"anonymous"</code>
*/
String name();
/**
* Path variables, either named or by index (capturing group).
*
* <pre>
* /path/:var
* </pre>
*
* Variable <code>var</code> is accessible by name: <code>var</code> or index: <code>0</code>.
*
* @return The currently matched path variables (if any).
*/
Map<Object, String> vars();
/**
* @return List all the types this route can consumes, defaults is: {@code * / *}.
*/
List<MediaType> consumes();
/**
* @return List all the types this route can produces, defaults is: {@code * / *}.
*/
List<MediaType> produces();
/**
* True, when route's name starts with the given prefix. Useful for dynamic routing. See
* {@link Route.Chain#next(String, Request, Response)}
*
* @param prefix Prefix to check for.
* @return True, when route's name starts with the given prefix.
*/
default boolean apply(final String prefix) {
return name().startsWith(prefix);
}
/**
* @return All the available attributes in the execution chain.
*/
Map<String, Object> attributes();
/**
* Attribute by name.
*
* @param name Attribute's name.
* @param <T> Attribute's type.
* @return Attribute value.
*/
@SuppressWarnings("unchecked")
default <T> T attr(final String name) {
return (T) attributes().get(name);
}
/**
* Indicates if the {@link #pattern()} contains a glob character, like <code>?</code>,
* <code>*</code> or <code>**</code>.
*
* @return Indicates if the {@link #pattern()} contains a glob charecter, like <code>?</code>,
* <code>*</code> or <code>**</code>.
*/
boolean glob();
/**
* Recreate a route path and apply the given variables.
*
* @param vars Path variables.
* @return A route pattern.
*/
String reverse(final Map<String, Object> vars);
/**
* Recreate a route path and apply the given variables.
*
* @param values Path variable values.
* @return A route pattern.
*/
String reverse(final Object... values);
/**
* Normalize a path by removing double or trailing slashes.
*
* @param path A path to normalize.
* @return A normalized path.
*/
static String normalize(final String path) {
return RoutePattern.normalize(path);
}
/**
* Remove invalid path mark when present.
*
* @param path Path.
* @return Original path.
*/
static String unerrpath(final String path) {
if (path.charAt(0) == OUT_OF_PATH) {
return path.substring(1);
}
return path;
}
/**
* Mark a path as invalid.
*
* @param path Path.
* @return Invalid path.
*/
static String errpath(final String path) {
return OUT_OF_PATH + path;
}
/**
* Source information (where the route was defined).
*
* @return Source information (where the route was defined).
*/
Route.Source source();
/**
* Print route information like: method, path, source, etc... Useful for debugging.
*
* @param indent Indent level
* @return Output.
*/
default String print(final int indent) {
StringBuilder buff = new StringBuilder();
String[] header = {"Method", "Path", "Source", "Name", "Pattern", "Consumes", "Produces" };
String[] values = {method(), path(), source().toString(), name(), pattern(),
consumes().toString(), produces().toString() };
BiConsumer<Function<Integer, String>, Character> format = (v, s) -> {
buff.append(Strings.padEnd("", indent, ' '))
.append("|").append(s);
for (int i = 0; i < header.length; i++) {
buff
.append(Strings.padEnd(v.apply(i), Math.max(header[i].length(), values[i].length()), s))
.append(s).append("|").append(s);
}
buff.setLength(buff.length() - 1);
};
format.accept(i -> header[i], ' ');
buff.append("\n");
format.accept(i -> "-", '-');
buff.append("\n");
format.accept(i -> values[i], ' ');
return buff.toString();
}
/**
* Print route information like: method, path, source, etc... Useful for debugging.
*
* @return Output.
*/
default String print() {
return print(0);
}
}