/**
* 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 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 org.jooby.internal.AssetProxy;
import org.jooby.internal.RouteImpl;
import org.jooby.internal.RouteMatcher;
import org.jooby.internal.RoutePattern;
import org.jooby.util.Collectors;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
/**
* Routes are a key concept in Jooby. Routes are executed in the same order they are defined
* (even for Mvc Routes).
*
*
Handlers
*
* There are two types of handlers: {@link Route.Handler} 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:
*
{@code com/t?st.html} - matches {@code com/test.html} but also {@code com/tast.jsp} or
* {@code com/txst.html}
*
{@code com/*.html} - matches all {@code .html} files in the {@code com} directory
*
com/{@literal **}/test.html - matches all {@code test.html} files underneath the
* {@code com} path
*
{@code **}/{@code *} - matches any path at any level.
*
{@code *} - matches any path at any level, shorthand for {@code **}/{@code *}.
*
*
*
Variables
*
* Jooby supports path parameters too:
*
*
* Some examples:
*
*
*
/user/{id} - /user/* and give you access to the id var.
*
/user/:id - /user/* and give you access to the id var.
*
/user/{id:\\d+} - /user/[digits] and give you access to the numeric
* id var.
*
*
*
Routes
*
* Routes are executed in the order they are defined, for example:
*
*
*
* 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!
* });
*
*
* Please note first and second routes are converted to a filter, so previous example is the same
* as:
*
*
* 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!
* });
*
*
*
Inline route
*
* An inline route can be defined using Lambda expressions, like:
*
*
* @author edgar
* @since 0.1.0
*/
class Definition {
/**
* Route's name.
*/
private String name = "/anonymous";
/**
* A route pattern.
*/
private RoutePattern compiledPattern;
/**
* 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 consumes = MediaType.ALL;
/**
* Defines the media types that the methods of a resource class or can produces. Default is:
* {@code *}/{@code *}.
*/
private List produces = MediaType.ALL;
/**
* A HTTP verb or *.
*/
private String method;
/**
* A path pattern.
*/
private String pattern;
private List excludes = Collections.emptyList();
/**
* Creates a new route definition.
*
* @param verb A HTTP verb or *.
* @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 *.
* @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 *.
* @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 *.
* @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.compiledPattern = new RoutePattern(method, pattern);
// normalized pattern
this.pattern = compiledPattern.pattern();
this.filter = filter;
}
/**
*
Path Patterns
*
* Jooby supports Ant-style path patterns:
*
*
* Some examples:
*
*
*
{@code com/t?st.html} - matches {@code com/test.html} but also {@code com/tast.jsp} or
* {@code com/txst.html}
*
{@code com/*.html} - matches all {@code .html} files in the {@code com} directory
*
com/{@literal **}/test.html - matches all {@code test.html} files underneath
* the {@code com} path
*
{@code **}/{@code *} - matches any path at any level.
*
{@code *} - matches any path at any level, shorthand for {@code **}/{@code *}.
*
*
*
Variables
*
* Jooby supports path parameters too:
*
*
* Some examples:
*
*
*
/user/{id} - /user/* and give you access to the id var.
*
/user/:id - /user/* and give you access to the id var.
*
/user/{id:\\d+} - /user/[digits] and give you access to the numeric
* id var.
*
*
* @return A path pattern.
*/
public String pattern() {
return pattern;
}
/**
* @return List of path variables (if any).
*/
public List vars() {
return compiledPattern.vars();
}
/**
* Test if the route matches the given verb, path, content type and accept header.
*
* @param verb A HTTP verb.
* @param path Current HTTP path.
* @param contentType The Content-Type header.
* @param accept The Accept header.
* @return A route or an empty optional.
*/
public Optional matches(final String verb,
final String path, final MediaType contentType,
final List accept) {
String fpath = verb.toUpperCase() + path;
if (excludes(fpath)) {
return Optional.empty();
}
RouteMatcher matcher = compiledPattern.matcher(fpath);
if (matcher.matches()) {
List result = MediaType.matcher(accept).filter(this.produces);
if (result.size() > 0 && canConsume(contentType)) {
// keep accept when */*
List produces = result.size() == 1 && result.get(0).name().equals("*/*")
? accept : this.produces;
return Optional.of(asRoute(verb, matcher, produces));
}
}
return Optional.empty();
}
/**
* @return HTTP method or *.
*/
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: anonymous.
*/
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.
*/
public Definition name(final String name) {
checkArgument(!Strings.isNullOrEmpty(name), "A route's name is required.");
this.name = RoutePattern.normalize(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 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));
}
/**
* Set the media types the route can consume.
*
* @param consumes The media types to test for.
* @return This route definition.
*/
public Definition 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 route definition.
*/
public Definition 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 route definition.
*/
public Definition consumes(final List consumes) {
checkArgument(consumes != null && consumes.size() > 0, "Consumes types are required");
if (consumes.size() > 1) {
this.consumes = Lists.newLinkedList(consumes);
Collections.sort(this.consumes);
} else {
this.consumes = ImmutableList.of(consumes.get(0));
}
return this;
}
/**
* Set the media types the route can produces.
*
* @param produces The media types to test for.
* @return This route definition.
*/
public Definition produces(final MediaType... produces) {
return produces(Arrays.asList(produces));
}
public Definition 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 route definition.
*/
public Definition produces(final List produces) {
checkArgument(produces != null && produces.size() > 0, "Produces types are required");
if (produces.size() > 1) {
this.produces = Lists.newLinkedList(produces);
Collections.sort(this.produces);
} else {
this.produces = ImmutableList.of(produces.get(0));
}
return this;
}
/**
* Excludes one or more path pattern from this route, useful for filter:
*
*
*
* @param excludes A path pattern.
* @return This route definition.
*/
public Definition excludes(final String... excludes) {
return excludes(Arrays.asList(excludes));
}
/**
* Excludes one or more path pattern from this route, useful for filter:
*
*