play.mvc.Router Maven / Gradle / Ivy
package play.mvc;
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.exceptions.NoRouteFoundException;
import play.mvc.results.NotFound;
import play.mvc.results.RenderStatic;
import play.templates.TemplateLoader;
import play.utils.Default;
import play.utils.Utils;
import play.vfs.VirtualFile;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* 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();
if (Logger.isTraceEnabled()) {
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 CopyOnWriteArrayList();
public static void routeOnlyStatic(Http.Request request) {
for (Route route : routes) {
try {
if (route.matches(request.method, request.path, request.format, request.domain) != 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) {
if (Logger.isTraceEnabled()) {
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()) {
if (Logger.isTraceEnabled()) {
Logger.trace("request method %s overriden to %s ", request.method, matcher.group("method"));
}
request.method = matcher.group("method");
}
}
for (Route route : routes) {
Map args = route.matches(request.method, request.path, request.format, request.domain);
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);
String base = getBaseUrl();
if (actionDefinition.method.equals("WS")) {
return base.replaceFirst("https?", "ws") + actionDefinition;
}
return base + actionDefinition;
}
// Gets baseUrl from current request or application.baseUrl in application.conf
protected static String getBaseUrl() {
if (Http.Request.current() == null) {
// No current request is present - must get baseUrl from config
String appBaseUrl = Play.configuration.getProperty("application.baseUrl", "application.baseUrl");
if (appBaseUrl.endsWith("/")) {
// remove the trailing slash
appBaseUrl = appBaseUrl.substring(0, appBaseUrl.length()-1);
}
return appBaseUrl;
} else {
return Http.Request.current().getBase();
}
}
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) {
boolean isSecure = Http.Request.current() == null ? false : Http.Request.current().secure;
String base = getBaseUrl();
if (!StringUtils.isEmpty(route.host)) {
// Compute the host
int port = Http.Request.current() == null ? 80 : Http.Request.current().get().port;
String host = (port != 80 && port != 443) ? route.host + ":" + port : route.host;
to = (isSecure ? "https://" : "http://") + host + to;
} else {
to = base + 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) {
String encoding = Http.Response.current() == null ? Play.defaultWebEncoding : Http.Response.current().encoding;
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() == null ? "" : Http.Request.current().domain;
break;
} else {
allRequiredArgsAreHere = false;
break;
}
} else {
if (value instanceof List>) {
@SuppressWarnings("unchecked")
List