package act.route;
/*-
* #%L
* ACT Framework
* %%
* Copyright (C) 2014 - 2017 ActFramework
* %%
* Licensed 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.
* #L%
*/
import act.Act;
import act.Destroyable;
import act.app.*;
import act.cli.tree.TreeNode;
import act.conf.AppConfig;
import act.controller.ParamNames;
import act.controller.builtin.ThrottleFilter;
import act.handler.*;
import act.handler.builtin.*;
import act.handler.builtin.controller.RequestHandlerProxy;
import act.security.CORS;
import act.security.CSRF;
import act.util.ActContext;
import act.util.DestroyableBase;
import act.ws.WsEndpoint;
import org.osgl.$;
import org.osgl.exception.NotAppliedException;
import org.osgl.http.H;
import org.osgl.http.util.Path;
import org.osgl.logging.LogManager;
import org.osgl.logging.Logger;
import org.osgl.mvc.result.Result;
import org.osgl.util.*;
import java.io.File;
import java.io.PrintStream;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import javax.enterprise.context.ApplicationScoped;
import javax.validation.constraints.NotNull;
public class Router extends AppHolderBase {
/**
* A visitor can be passed to the router to traverse
* the routes
*/
public interface Visitor {
/**
* Visit a route mapping in the router
*
* @param method the HTTP method
* @param path the URL path
* @param handler the handler
*/
void visit(H.Method method, String path, RequestHandler handler);
}
public static final String IGNORE_NOTATION = "...";
private static final H.Method[] targetMethods = new H.Method[]{
H.Method.GET, H.Method.POST, H.Method.DELETE, H.Method.PUT, H.Method.PATCH};
private static final Logger LOGGER = LogManager.get(Router.class);
Node _GET;
Node _PUT;
Node _POST;
Node _DEL;
Node _PATCH;
private Map resolvers = new HashMap<>();
private RequestHandlerResolver handlerLookup;
// map action context to url context
// for example `act.` -> `/~`
private Map urlContexts = new HashMap<>();
private Set actionNames = new HashSet<>();
private AppConfig appConfig;
private String portId;
private int port;
private OptionsInfoBase optionHandlerFactory;
private Set requireBodyParsing = new HashSet<>();
private void initControllerLookup(RequestHandlerResolver lookup) {
if (null == lookup) {
lookup = new RequestHandlerResolverBase() {
@Override
public RequestHandler resolve(String payload, App app) {
if (S.eq(WsEndpoint.PSEUDO_METHOD, payload.toString())) {
return Act.network().createWebSocketConnectionHandler();
}
return new RequestHandlerProxy(payload.toString(), app);
}
};
}
handlerLookup = lookup;
}
public Router(App app) {
this(null, app, null);
}
public Router(App app, String portId) {
this(null, app, portId);
}
public Router(RequestHandlerResolver handlerLookup, App app) {
this(handlerLookup, app, null);
}
public Router(RequestHandlerResolver handlerLookup, App app, String portId) {
super(app);
initControllerLookup(handlerLookup);
this.appConfig = app.config();
this.portId = portId;
if (S.notBlank(portId)) {
this.port = appConfig.namedPort(portId).port();
} else {
this.port = appConfig.httpSecure() ? appConfig.httpExternalSecurePort() : appConfig.httpExternalPort();
}
this.optionHandlerFactory = new OptionsInfoBase(this);
_GET = Node.newRoot("GET", appConfig);
_PUT = Node.newRoot("PUT", appConfig);
_POST = Node.newRoot("POST", appConfig);
_DEL = Node.newRoot("DELETE", appConfig);
_PATCH = Node.newRoot("PATCH", appConfig);
}
@Override
protected void releaseResources() {
_GET.destroy();
_DEL.destroy();
_POST.destroy();
_PUT.destroy();
_PATCH.destroy();
handlerLookup.destroy();
actionNames.clear();
appConfig = null;
}
public String portId() {
return portId;
}
public int port() {
return port;
}
/**
* Accept a {@link Visitor} to traverse route mapping in this
* router
*
* @param visitor the visitor
*/
public void accept(Visitor visitor) {
visit(_GET, H.Method.GET, visitor);
visit(_POST, H.Method.POST, visitor);
visit(_PUT, H.Method.PUT, visitor);
visit(_DEL, H.Method.DELETE, visitor);
visit(_PATCH, H.Method.PATCH, visitor);
}
private void visit(Node node, H.Method method, Visitor visitor) {
RequestHandler handler = node.handler;
if (null != handler) {
if (handler instanceof ContextualHandler) {
handler = ((ContextualHandler) handler).realHandler();
}
visitor.visit(method, node.path(), handler);
}
for (Node child : node.dynamicChilds) {
visit(child, method, visitor);
}
for (Node child : node.staticChildren.values()) {
visit(child, method, visitor);
}
}
// Mark handler as require body parsing
public void markRequireBodyParsing(RequestHandler handler) {
requireBodyParsing.add(handler);
}
// --- routing ---
public RequestHandler getInvoker(H.Method method, String path, ActionContext context) {
context.router(this);
if (method == H.Method.OPTIONS) {
return optionHandlerFactory.optionHandler(path, context);
}
Node node = root(method, false);
if (null == node) {
return UnknownHttpMethodHandler.INSTANCE;
}
node = search(node, Path.tokenizer(Unsafe.bufOf(path)), context);
RequestHandler handler = getInvokerFrom(node);
RequestHandler blockIssueHandler = app().blockIssueHandler();
if (null == blockIssueHandler) {
return handler;
}
if (handler instanceof FileGetter || handler instanceof ResourceGetter) {
return handler;
}
return blockIssueHandler;
}
public RequestHandler findStaticGetHandler(String url) {
Iterator path = Path.tokenizer(Unsafe.bufOf(url));
Node node = root(H.Method.GET);
while (null != node && path.hasNext()) {
String nodeName = path.next();
node = node.staticChildren.get(nodeName);
if (null == node || node.terminateRouteSearch()) {
break;
}
}
return null == node ? null : node.handler;
}
private RequestHandler getInvokerFrom(Node node) {
if (null == node) {
return notFound();
}
RequestHandler handler = node.handler;
if (null == handler) {
for (Node targetNode : node.dynamicChilds) {
if (Node.MATCH_ALL == targetNode.patternTrait || targetNode.pattern.matcher("").matches()) {
return getInvokerFrom(targetNode);
}
}
return notFound();
}
return handler;
}
// --- route building ---
public void addContext(String actionContext, String urlContext) {
urlContexts.put(actionContext, urlContext);
}
enum ConflictResolver {
/**
* Overwrite existing route
*/
OVERWRITE,
/**
* Overwrite and log warn message
*/
OVERWRITE_WARN,
/**
* Skip the new route
*/
SKIP,
/**
* Report error and exit app
*/
EXIT
}
private String withUrlContext(String path, String action) {
String sAction = action.toString();
String urlContext = null;
for (String key : urlContexts.keySet()) {
String sKey = key.toString();
if (sAction.startsWith(sKey)) {
urlContext = urlContexts.get(key);
break;
}
}
return null == urlContext ? path : S.pathConcat(urlContext, '/', path.toString());
}
public void addMapping(H.Method method, String path, String action) {
addMapping(method, withUrlContext(path, action), resolveActionHandler(action), RouteSource.ROUTE_TABLE);
}
public void addMapping(H.Method method, String path, String action, RouteSource source) {
addMapping(method, withUrlContext(path, action), resolveActionHandler(action), source);
}
public void addMapping(H.Method method, String path, RequestHandler handler) {
addMapping(method, path, handler, RouteSource.ROUTE_TABLE);
}
@SuppressWarnings("FallThrough")
public void addMapping(final H.Method method, final String path, RequestHandler handler, final RouteSource source) {
if (isTraceEnabled()) {
trace("R+ %s %s | %s (%s)", method, path, handler, source);
}
if (!app().config().builtInReqHandlerEnabled()) {
String sPath = path.toString();
if (sPath.startsWith("/~/")) {
// disable built-in handlers except those might impact application behaviour
// apibook is allowed here as it only available on dev mode
if (!(sPath.contains("asset") || sPath.contains("i18n") || sPath.contains("job") || sPath.contains("api") || sPath.contains("ticket"))) {
return;
}
}
}
Node node = _locate(method, path, handler.toString());
if (null == node.handler) {
Set conflicts = node.conflicts();
if (!conflicts.isEmpty()) {
for (Node conflict : conflicts) {
if (null != conflict.handler) {
node = conflict;
break;
}
}
}
}
if (null == node.handler) {
handler = prepareReverseRoutes(handler, node);
node.handler(handler, source);
} else {
RouteSource existing = node.routeSource();
ConflictResolver resolving = source.onConflict(existing);
switch (resolving) {
case OVERWRITE_WARN:
warn("\n\tOverwrite existing route \n\t\t%s\n\twith new route\n\t\t%s",
routeInfo(method, path, node.handler()),
routeInfo(method, path, handler)
);
case OVERWRITE:
handler = prepareReverseRoutes(handler, node);
node.handler(handler, source);
case SKIP:
break;
case EXIT:
throw new DuplicateRouteMappingException(
new RouteInfo(method, path.toString(), node.handler(), existing),
new RouteInfo(method, path.toString(), handler, source)
);
default:
throw E.unsupport();
}
}
}
private RequestHandler prepareReverseRoutes(RequestHandler handler, Node node) {
if (handler instanceof RequestHandlerInfo) {
RequestHandlerInfo info = (RequestHandlerInfo) handler;
String action = info.action;
Node root = node.root;
root.reverseRoutes.put(action.toString(), node);
handler = info.theHandler();
}
return handler;
}
public String reverseRoute(String action, boolean fullUrl) {
return reverseRoute(action, new HashMap(), fullUrl);
}
public String reverseRoute(String action) {
return reverseRoute(action, new HashMap());
}
public String reverseRoute(String action, Map args) {
String fullAction = inferFullActionPath(action);
for (H.Method m : supportedHttpMethods()) {
String url = reverseRoute(fullAction, m, args);
if (null != url) {
return ensureUrlContext(url);
}
}
return null;
}
public static final $.Func0 DEF_ACTION_PATH_PROVIDER = new $.Func0() {
@Override
public String apply() throws NotAppliedException, $.Break {
ActContext context = ActContext.Base.currentContext();
E.illegalStateIf(null == context, "cannot use shortcut action path outside of a act context");
return context.methodPath();
}
};
// See https://github.com/actframework/actframework/issues/107
public static String inferFullActionPath(String actionPath) {
return inferFullActionPath(actionPath, DEF_ACTION_PATH_PROVIDER);
}
public static String inferFullActionPath(String actionPath, $.Func0 currentActionPathProvider) {
String handler, controller = null;
if (actionPath.contains("/")) {
return actionPath;
}
int pos = actionPath.indexOf(".");
if (pos < 0) {
handler = actionPath;
} else {
controller = actionPath.substring(0, pos);
handler = actionPath.substring(pos + 1, actionPath.length());
if (handler.indexOf(".") > 0) {
// it's a full path, not shortcut
return actionPath;
}
}
String currentPath = currentActionPathProvider.apply();
if (null == currentPath) {
return actionPath;
}
pos = currentPath.lastIndexOf(".");
String currentPathWithoutHandler = currentPath.substring(0, pos);
if (null == controller) {
return S.concat(currentPathWithoutHandler, ".", handler);
}
pos = currentPathWithoutHandler.lastIndexOf(".");
String currentPathWithoutController = currentPathWithoutHandler.substring(0, pos);
return S.concat(currentPathWithoutController, ".", controller, ".", handler);
}
public String reverseRoute(String action, Map args, boolean fullUrl) {
String path = reverseRoute(action, args);
if (null == path) {
return null;
}
return fullUrl ? fullUrl(path) : path;
}
public String reverseRoute(String action, H.Method method, Map args) {
Node root = root(method);
Node node = root.reverseRoutes.get(action);
if (null == node) {
return null;
}
C.List elements = C.newList();
args = new HashMap<>(args);
while (root != node) {
if (node.isDynamic()) {
Node targetNode = node;
for (Map.Entry entry : node.dynamicReverseAliases.entrySet()) {
if (entry.getKey().equals(action)) {
targetNode = entry.getValue();
break;
}
}
S.Buffer buffer = S.buffer();
for ($.Transformer