play.mvc.Router Maven / Gradle / Ivy
package play.mvc;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import jregex.Matcher;
import jregex.Pattern;
import jregex.REFlags;
import org.apache.commons.lang.StringUtils;
import play.Logger;
import play.Play;
import play.Play.Mode;
import play.vfs.VirtualFile;
import play.exceptions.NoRouteFoundException;
import play.exceptions.UnexpectedException;
import play.mvc.results.NotFound;
import play.mvc.results.RenderStatic;
import play.templates.TemplateLoader;
import play.utils.Default;
/**
* The router matches HTTP requests to action invocations
*/
public class Router {
static Pattern routePattern = new Pattern("^({method}GET|POST|PUT|DELETE|OPTIONS|HEAD|WS|\\*)[(]?({headers}[^)]*)(\\))?\\s+({path}.*/[^\\s]*)\\s+({action}[^\\s(]+)({params}.+)?(\\s*)$");
/**
* Pattern used to locate a method override instruction in request.querystring
*/
static Pattern methodOverride = new Pattern("^.*x-http-method-override=({method}GET|PUT|POST|DELETE).*$");
/**
* Timestamp the routes file was last loaded at.
*/
public static long lastLoading = -1;
/**
* Parse the routes file. This is called at startup.
*
* @param prefix The prefix that the path of all routes in this route file start with. This prefix should not end with a '/' character.
*/
public static void load(String prefix) {
routes.clear();
parse(Play.routes, prefix);
lastLoading = System.currentTimeMillis();
// Plugins
Play.pluginCollection.onRoutesLoaded();
}
/**
* This one can be called to add new route. Last added is first in the route list.
*/
public static void prependRoute(String method, String path, String action, String headers) {
prependRoute(method, path, action, null, headers);
}
/**
* This one can be called to add new route. Last added is first in the route list.
*/
public static void prependRoute(String method, String path, String action) {
prependRoute(method, path, action, null, null);
}
/**
* Add a route at the given position
*/
public static void addRoute(int position, String method, String path, String action, String params, String headers) {
if (position > routes.size()) {
position = routes.size();
}
routes.add(position, getRoute(method, path, action, params, headers));
}
/**
* Add a route at the given position
*/
public static void addRoute(int position, String method, String path, String headers) {
addRoute(position, method, path, null, null, headers);
}
/**
* Add a route at the given position
*/
public static void addRoute(int position, String method, String path, String action, String headers) {
addRoute(position, method, path, action, null, headers);
}
/**
* Add a new route. Will be first in the route list
*/
public static void addRoute(String method, String path, String action) {
prependRoute(method, path, action);
}
/**
* Add a route at the given position
*/
public static void addRoute(String method, String path, String action, String headers) {
addRoute(method, path, action, null, headers);
}
/**
* Add a route
*/
public static void addRoute(String method, String path, String action, String params, String headers) {
appendRoute(method, path, action, params, headers, null, 0);
}
/**
* This is used internally when reading the route file. The order the routes are added matters and
* we want the method to append the routes to the list.
*/
public static void appendRoute(String method, String path, String action, String params, String headers, String sourceFile, int line) {
routes.add(getRoute(method, path, action, params, headers, sourceFile, line));
}
public static Route getRoute(String method, String path, String action, String params, String headers) {
return getRoute(method, path, action, params, headers, null, 0);
}
public static Route getRoute(String method, String path, String action, String params, String headers, String sourceFile, int line) {
Route route = new Route();
route.method = method;
route.path = path.replace("//", "/");
route.action = action;
route.routesFile = sourceFile;
route.routesFileLine = line;
route.addFormat(headers);
route.addParams(params);
route.compute();
Logger.trace("Adding [" + route.toString() + "] with params [" + params + "] and headers [" + headers + "]");
return route;
}
/**
* Add a new route at the beginning of the route list
*/
public static void prependRoute(String method, String path, String action, String params, String headers) {
routes.add(0, getRoute(method, path, action, params, headers));
}
/**
* Parse a route file.
* If an action starts with "plugin:name", replace that route by the ones declared
* in the plugin route file denoted by that name, if found.
*
* @param routeFile
* @param prefix The prefix that the path of all routes in this route file start with. This prefix should not
* end with a '/' character.
*/
static void parse(VirtualFile routeFile, String prefix) {
String fileAbsolutePath = routeFile.getRealFile().getAbsolutePath();
String content = routeFile.contentAsString();
if (content.indexOf("${") > -1 || content.indexOf("#{") > -1 || content.indexOf("%{") > -1) {
// Mutable map needs to be passed in.
content = TemplateLoader.load(routeFile).render(new HashMap(16));
}
parse(content, prefix, fileAbsolutePath);
}
static void parse(String content, String prefix, String fileAbsolutePath) {
int lineNumber = 0;
for (String line : content.split("\n")) {
lineNumber++;
line = line.trim().replaceAll("\\s+", " ");
if (line.length() == 0 || line.startsWith("#")) {
continue;
}
Matcher matcher = routePattern.matcher(line);
if (matcher.matches()) {
String action = matcher.group("action");
// module:
if (action.startsWith("module:")) {
String moduleName = action.substring("module:".length());
String newPrefix = prefix + matcher.group("path");
if (newPrefix.length() > 1 && newPrefix.endsWith("/")) {
newPrefix = newPrefix.substring(0, newPrefix.length() - 1);
}
if (moduleName.equals("*")) {
for (String p : Play.modulesRoutes.keySet()) {
parse(Play.modulesRoutes.get(p), newPrefix + p);
}
} else if (Play.modulesRoutes.containsKey(moduleName)) {
parse(Play.modulesRoutes.get(moduleName), newPrefix);
} else {
Logger.error("Cannot include routes for module %s (not found)", moduleName);
}
} else {
String method = matcher.group("method");
String path = prefix + matcher.group("path");
String params = matcher.group("params");
String headers = matcher.group("headers");
appendRoute(method, path, action, params, headers, fileAbsolutePath, lineNumber);
}
} else {
Logger.error("Invalid route definition : %s", line);
}
}
}
/**
* In PROD mode and if the routes are already loaded, this does nothing.
*
* In DEV mode, this checks each routes file's "last modified" time to see if the routes need updated.
*
* @param prefix The prefix that the path of all routes in this route file start with. This prefix should not end with a '/' character.
*/
public static void detectChanges(String prefix) {
if (Play.mode == Mode.PROD && lastLoading > 0) {
return;
}
if (Play.routes.lastModified() > lastLoading) {
load(prefix);
} else {
for (VirtualFile file : Play.modulesRoutes.values()) {
if (file.lastModified() > lastLoading) {
load(prefix);
return;
}
}
}
}
/**
* All the loaded routes.
*/
public static List routes = new ArrayList(500);
public static void routeOnlyStatic(Http.Request request) {
for (Route route : routes) {
try {
String format = request.format;
String host = request.host;
if (route.matches(request.method, request.path, format, host) != null) {
break;
}
} catch (Throwable t) {
if (t instanceof RenderStatic) {
throw (RenderStatic) t;
}
if (t instanceof NotFound) {
throw (NotFound) t;
}
}
}
}
public static Route route(Http.Request request) {
Logger.trace("Route: " + request.path + " - " + request.querystring);
// request method may be overriden if a x-http-method-override parameter is given
if (request.querystring != null && methodOverride.matches(request.querystring)) {
Matcher matcher = methodOverride.matcher(request.querystring);
if (matcher.matches()) {
Logger.trace("request method %s overriden to %s ", request.method, matcher.group("method"));
request.method = matcher.group("method");
}
}
for (Route route : routes) {
String format = request.format;
String host = request.host;
Map args = route.matches(request.method, request.path, format, host);
if (args != null) {
request.routeArgs = args;
request.action = route.action;
if (args.containsKey("format")) {
request.format = args.get("format");
}
if (request.action.indexOf("{") > -1) { // more optimization ?
for (String arg : request.routeArgs.keySet()) {
request.action = request.action.replace("{" + arg + "}", request.routeArgs.get(arg));
}
}
if (request.action.equals("404")) {
throw new NotFound(route.path);
}
return route;
}
}
// Not found - if the request was a HEAD, let's see if we can find a corresponding GET
if (request.method.equalsIgnoreCase("head")) {
request.method = "GET";
Route route = route(request);
request.method = "HEAD";
if (route != null) {
return route;
}
}
throw new NotFound(request.method, request.path);
}
public static Map route(String method, String path) {
return route(method, path, null, null);
}
public static Map route(String method, String path, String headers) {
return route(method, path, headers, null);
}
public static Map route(String method, String path, String headers, String host) {
for (Route route : routes) {
Map args = route.matches(method, path, headers, host);
if (args != null) {
args.put("action", route.action);
return args;
}
}
return new HashMap(16);
}
public static ActionDefinition reverse(String action) {
// Note the map is not Collections.EMPTY_MAP
because it will be copied and changed.
return reverse(action, new HashMap(16));
}
public static String getFullUrl(String action, Map args) {
ActionDefinition actionDefinition = reverse(action, args);
if (actionDefinition.method.equals("WS")) {
return Http.Request.current().getBase().replaceFirst("https?", "ws") + actionDefinition;
}
return Http.Request.current().getBase() + actionDefinition;
}
public static String getFullUrl(String action) {
// Note the map is not Collections.EMPTY_MAP
because it will be copied and changed.
return getFullUrl(action, new HashMap(16));
}
public static String reverse(VirtualFile file) {
return reverse(file, false);
}
public static String reverse(VirtualFile file, boolean absolute) {
if (file == null || !file.exists()) {
throw new NoRouteFoundException("File not found (" + file + ")");
}
String path = file.relativePath();
path = path.substring(path.indexOf("}") + 1);
for (Route route : routes) {
String staticDir = route.staticDir;
if (staticDir != null) {
if (!staticDir.startsWith("/")) {
staticDir = "/" + staticDir;
}
if (!staticDir.equals("/") && !staticDir.endsWith("/")) {
staticDir = staticDir + "/";
}
if (path.startsWith(staticDir)) {
String to = route.path + path.substring(staticDir.length());
if (to.endsWith("/index.html")) {
to = to.substring(0, to.length() - "/index.html".length() + 1);
}
if (absolute) {
if (!StringUtils.isEmpty(route.host)) {
// Compute the host
to = (Http.Request.current().secure ? "https://" : "http://") + route.host + to;
} else {
to = Http.Request.current().getBase() + to;
}
}
return to;
}
}
}
throw new NoRouteFoundException(file.relativePath());
}
public static String reverseWithCheck(String name, VirtualFile file, boolean absolute) {
if (file == null || !file.exists()) {
throw new NoRouteFoundException(name + " (file not found)");
}
return reverse(file, absolute);
}
public static ActionDefinition reverse(String action, Map args) {
if (action.startsWith("controllers.")) {
action = action.substring(12);
}
Map argsbackup = new HashMap(args);
// Add routeArgs
if (Scope.RouteArgs.current() != null) {
for (String key : Scope.RouteArgs.current().data.keySet()) {
if (!args.containsKey(key)) {
args.put(key, Scope.RouteArgs.current().data.get(key));
}
}
}
for (Route route : routes) {
if (route.actionPattern != null) {
Matcher matcher = route.actionPattern.matcher(action);
if (matcher.matches()) {
for (String group : route.actionArgs) {
String v = matcher.group(group);
if (v == null) {
continue;
}
args.put(group, v.toLowerCase());
}
List inPathArgs = new ArrayList(16);
boolean allRequiredArgsAreHere = true;
// les noms de parametres matchent ils ?
for (Route.Arg arg : route.args) {
inPathArgs.add(arg.name);
Object value = args.get(arg.name);
if (value == null) {
// This is a hack for reverting on hostname that are a regex expression.
// See [#344] for more into. This is not optimal and should retough. However,
// it allows us to do things like {(.*}}.domain.com
String host = route.host.replaceAll("\\{", "").replaceAll("\\}", "");
if (host.equals(arg.name) || host.matches(arg.name)) {
args.remove(arg.name);
route.host = Http.Request.current().domain;
break;
} else {
allRequiredArgsAreHere = false;
break;
}
} else {
if (value instanceof List>) {
@SuppressWarnings("unchecked")
List