package io.norberg.rut;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import static io.norberg.rut.Encoding.decode;
import static io.norberg.rut.Router.Status.METHOD_NOT_ALLOWED;
import static io.norberg.rut.Router.Status.NOT_FOUND;
import static io.norberg.rut.Router.Status.SUCCESS;
/**
* A router for routing REST request paths to endpoints.
*
* @param <T> The target endpoint type.
*/
public final class Router<T> {
private final RadixTrie<RouteTarget<T>> trie;
private final boolean optionalTrailingSlash;
private Router(final RadixTrie<RouteTarget<T>> trie, final boolean optionalTrailingSlash) {
this.trie = trie;
this.optionalTrailingSlash = optionalTrailingSlash;
}
public static <T> Builder<T> builder() {
return new Builder<T>();
}
@SuppressWarnings("UnusedParameters")
public static <T> Builder<T> builder(Class<T> clazz) {
return new Builder<T>();
}
/**
* Route a request.
*
* @param method The request method. E.g. {@code GET, PUT, POST, DELETE}, etc.
* @param path The request path. E.g. {@code /foo/baz/bar}.
* @param result A {@link Result} for storing the routing result, target and captured parameters.
* The {@link Result} should have enough capacity to store all captured parameters.
* See {@link #result()}.
* @return Routing status. {@link Status#SUCCESS} if an endpoint and matching method was found.
* {@link Status#NOT_FOUND} if the endpoint could not be found, {@link Status#METHOD_NOT_ALLOWED}
* if the endpoint was found but the method did not match.
*/
public Status route(final CharSequence method, final CharSequence path, final Result<T> result) {
result.captor.optionalTrailingSlash(optionalTrailingSlash);
final RouteTarget<T> route = trie.lookup(path, result.captor);
if (route == null) {
return result.notFound().status();
}
final Target<T> target = route.lookup(method);
if (target == null) {
return result.notAllowed(route).status();
}
return result.success(path, route, target).status();
}
/**
* Create a {@link Result} with enough capacity to hold all captured parameters for any endpoint
* of this router. The {@link Result} is intended to be instantiated reused, to avoid garbage.
* Note that the {@link Result} is not thread safe, so a dedicated {@link Result} should be
* created per thread.
*/
public Result<T> result() {
return Result.capturing(trie.captures());
}
/**
* Routing result.
*/
public enum Status {
/**
* A matching endpoint and method was found.
*/
SUCCESS,
/**
* A matching endpoint was not found.
*/
NOT_FOUND,
/**
* A matching endpoint was found but no method matched.
*/
METHOD_NOT_ALLOWED
}
/**
* Routing target holder.
*/
private static class Target<T> {
private final T target;
private final String[] paramNames;
private final ParameterType[] paramTypes;
private Target(final T target, final String[] paramNames, ParameterType[] paramTypes) {
this.target = target;
this.paramNames = paramNames;
this.paramTypes = paramTypes;
}
}
/**
* Router builder.
*/
public static class Builder<T> {
private boolean optionalTrailingSlash;
private Builder() {
}
private final RadixTrie.Builder<RouteTarget<T>> trie = RadixTrie.builder();
/**
* Create a new {@link Router} that will route requests to all endpoints registered with {@link
* #route}.
*/
public Router<T> build() {
return new Router<T>(trie.build(), optionalTrailingSlash);
}
/**
* Register a routing path and method.
*
* @param method A method that should be accepted for the route.
* @param path The path of the route.
* @param target A routing target that will be returned when requests are successfully routed to
* this route.
*/
public Builder<T> route(final String method, final String path, final T target) {
return route(Route.of(method, path), target);
}
/**
* Register a route.
*
* @param route The route to register.
* @param target A routing target that will be returned when requests are successfully routed
* to this route.
*/
public Builder<T> route(final Route route, final T target) {
trie.insert(route.path(), new RouteVisitor(route, target));
return this;
}
/**
* Set trailing slash matching to be optional or not. When configured to be optional, trailing
* slash in both routed uris/paths and routes are disregarded. E.g., {@code /foo} may be routed
* to {@code /foo/} and conversely {@code /bar/} may be routed to {@code /bar}.
*
* @param optional {@code true} if trailing slashes should be disregarded, {@code false} if
* trailing slashes should be strictly routed.
*/
public Builder<T> optionalTrailingSlash(final boolean optional) {
this.optionalTrailingSlash = optional;
return this;
}
/**
* A {@link Trie.Visitor} that adds a {@link RouteTarget} to the terminal {@link Trie.Node}.
*/
private class RouteVisitor implements Trie.Visitor<RouteTarget<T>> {
private final Route route;
private final T target;
public RouteVisitor(final Route route, final T target) {
this.route = route;
this.target = target;
}
@Override
public RouteTarget<T> finish(final RouteTarget<T> currentValue) {
final List<String> captureNames = route.captureNames();
final String[] paramNames = captureNames.toArray(new String[captureNames.size()]);
final List<ParameterType> parameterTypes = route.captureParameterTypes();
final ParameterType[] paramTypes =
parameterTypes.toArray(new ParameterType[parameterTypes.size()]);
final Target<T> target = new Target<T>(this.target, paramNames, paramTypes);
if (currentValue == null) {
return RouteTarget.of(route.method(), target);
}
return currentValue.with(route.method(), target);
}
}
}
/**
* Routing result holder.
*
* @param <T> The router endpoint type.
*/
public static class Result<T> {
private final RadixTrie.Captor captor;
private Status status;
private RouteTarget<T> route;
private Target<T> target;
private CharSequence path;
private Result(final int captures) {
captor = new RadixTrie.Captor(captures);
}
/**
* Create a new {@link Result} with enough capacity to hold {@code captures} captured
* parameters.
*/
public static <T> Result<T> capturing(final int captures) {
return new Result<T>(captures);
}
/**
* Get routing status of the {@link Router#route} invocation.
*/
public Status status() {
return status;
}
/**
* Convenience method for checking if the routing status is {@link Status#SUCCESS}.
*/
public boolean isSuccess() {
return status() == SUCCESS;
}
/**
* Get routing target, if successful.
*/
public T target() {
if (target == null) {
throw new IllegalStateException("not matched");
}
return target.target;
}
/**
* Get the number of captured parameter values.
*/
public int params() {
return captor.values();
}
/**
* Get the name of the captured path parameter at index {code i}.
*/
public String paramName(final int i) {
if (target == null) {
throw new IllegalStateException("not matched");
}
return target.paramNames[i];
}
/**
* Get the value of the captured path parameter at index {code i}.
*/
public CharSequence paramValue(final int i) {
return captor.value(path, i);
}
/**
* Get the URL decoded value of the captured path parameter at index {code i}.
*
* @return The decoded value or null if the encoding is invalid.
*/
public CharSequence paramValueDecoded(final int i) {
return decode(paramValue(i));
}
/**
* Get the parameter type of the captured path parameter at index {@code i}.
*/
public ParameterType paramType(final int i) {
if (target == null) {
throw new IllegalStateException("not matched");
}
return target.paramTypes[i];
}
/**
* Get start offset into the routed path of the captured parameter at index {code i}.
*
* @see #paramValue
* @see #paramValueEnd
*/
public int paramValueStart(final int i) {
return captor.valueStart(i);
}
/**
* Get end offset into the routed path of the captured parameter at index {code i}.
*
* @see #paramValue
* @see #paramValueStart
*/
public int paramValueEnd(final int i) {
return captor.valueEnd(i);
}
/**
* Signal a route found but method not allowed.
*/
private Result<T> notAllowed(final RouteTarget<T> route) {
this.status = METHOD_NOT_ALLOWED;
this.route = route;
this.target = null;
this.path = null;
return this;
}
/**
* Signal no route found.
*/
private Result<T> notFound() {
this.status = NOT_FOUND;
this.route = null;
this.target = null;
this.path = null;
return this;
}
/**
* Signal a routing success.
*/
private Result<T> success(final CharSequence path, final RouteTarget<T> route,
final Target<T> target) {
this.status = SUCCESS;
this.route = route;
this.target = target;
this.path = path;
return this;
}
/**
* Get query string start index. -1 if there is no query string part.
*/
public int queryStart() {
return captor.queryStart();
}
/**
* Get query string end index. -1 if there is no query string part.
*/
public int queryEnd() {
return captor.queryEnd();
}
/**
* Get query string. null if there is no query string part.
*/
public CharSequence query() {
return captor.query(path);
}
/**
* Get all allowed methods for the route if {@link #status()} is {@link Status#SUCCESS} or
* {@link Status#METHOD_NOT_ALLOWED}. Returns an empty collection if {@link #status()} is {@link
* Status#NOT_FOUND}.
*/
public Collection<String> allowedMethods() {
if (route == null) {
throw new IllegalStateException("not matched");
}
return route.methods();
}
}
/**
* Holder for route methods and target endpoints.
*/
private static class RouteTarget<T> {
private final String method;
private final Target<T> target;
private final RouteTarget<T> next;
private final Collection<String> methods;
private RouteTarget(final String method, final Target<T> target, final RouteTarget<T> next) {
this.method = method;
this.target = target;
this.next = next;
this.methods = methods0();
}
/**
* Create a new route.
*/
private static <T> RouteTarget<T> of(final String method, final Target<T> target) {
return new RouteTarget<T>(method, target, null);
}
/**
* Add a new method and target to this route.
*/
private RouteTarget<T> with(final String method, final Target<T> target) {
return new RouteTarget<T>(method, target, this);
}
/**
* Look up a method in this route.
*
* @return The endpoint if the method matched. {@code null} otherwise.
*/
private Target<T> lookup(final CharSequence method) {
RouteTarget<T> route = this;
while (route != null) {
if (equals(route.method, method)) {
return route.target;
}
route = route.next;
}
return null;
}
/**
* Compare two {@link CharSequence}s.
*/
private boolean equals(final CharSequence a, final CharSequence b) {
if (a == b) {
return true;
}
final int length = a.length();
if (length != b.length()) {
return false;
}
for (int i = 0; i < length; i++) {
if (a.charAt(i) != b.charAt(i)) {
return false;
}
}
return true;
}
/**
* Get a {@link Collection} of {@link String} with all methods allowed by this endpoint.
*/
public Collection<String> methods() {
return methods;
}
/**
* Create a list of all methods allowed by this endpoint.
*/
private Collection<String> methods0() {
final List<String> methods = new ArrayList<String>();
RouteTarget<T> route = this;
while (route != null) {
methods.add(route.method);
route = route.next;
}
return Collections.unmodifiableList(methods);
}
}
}