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.ActionContext; import act.app.App; import act.app.AppServiceBase; import act.cli.tree.TreeNode; import act.conf.AppConfig; import act.controller.ParamNames; import act.handler.*; import act.handler.builtin.*; import act.handler.builtin.controller.RequestHandlerProxy; import act.util.ActContext; import act.util.DestroyableBase; import org.osgl.$; import org.osgl.Osgl; import org.osgl.exception.NotAppliedException; import org.osgl.http.H; import org.osgl.http.util.Path; import org.osgl.logging.L; import org.osgl.logging.Logger; import org.osgl.util.*; import javax.enterprise.context.ApplicationScoped; import java.io.File; import java.io.PrintStream; import java.io.Serializable; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; public class Router extends AppServiceBase<Router> { 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 = L.get(Router.class); Node _GET = Node.newRoot("GET"); Node _PUT = Node.newRoot("PUT"); Node _POST = Node.newRoot("POST"); Node _DEL = Node.newRoot("DELETE"); Node _PATCH = Node.newRoot("PATCH"); private Map<String, RequestHandlerResolver> resolvers = C.newMap(); private RequestHandlerResolver handlerLookup; // map action context to url context // for example `act.` -> `/~` private Map<CharSequence, String> urlContexts = new HashMap<>(); private Set<String> actionNames = new HashSet<>(); private AppConfig appConfig; private String portId; private int port; private OptionsInfoBase optionHandlerFactory; private void initControllerLookup(RequestHandlerResolver lookup) { if (null == lookup) { lookup = new RequestHandlerResolverBase() { @Override public RequestHandler resolve(CharSequence payload, App app) { 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); } @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; } // --- routing --- public RequestHandler getInvoker(H.Method method, CharSequence path, ActionContext context) { context.router(this); RequestHandler blockIssueHandler = app().blockIssueHandler(); if (method == H.Method.OPTIONS) { return optionHandlerFactory.optionHandler(path, context); } if (Arrays.binarySearch(targetMethods, method) < 0) { return UnknownHttpMethodHandler.INSTANCE; } Node node = search(method, Path.tokenizer(Unsafe.bufOf(path)), context); RequestHandler handler = getInvokerFrom(node); if (null == blockIssueHandler) { return handler; } if (handler instanceof StaticFileGetter || handler instanceof StaticResourceGetter) { return handler; } return blockIssueHandler; } public RequestHandler findStaticGetHandler(String url) { Iterator<CharSequence> path = Path.tokenizer(Unsafe.bufOf(url)); Node node = root(H.Method.GET); while (null != node && path.hasNext()) { CharSequence 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 (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 CharSequence withUrlContext(CharSequence path, CharSequence action) { String sAction = action.toString(); String urlContext = null; for (CharSequence 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, CharSequence path, CharSequence action) { addMapping(method, withUrlContext(path, action), resolveActionHandler(action), RouteSource.ROUTE_TABLE); } public void addMapping(H.Method method, CharSequence path, CharSequence action, RouteSource source) { addMapping(method, withUrlContext(path, action), resolveActionHandler(action), source); } public void addMapping(H.Method method, CharSequence path, RequestHandler handler) { addMapping(method, path, handler, RouteSource.ROUTE_TABLE); } public void addMapping(H.Method method, CharSequence path, RequestHandler handler, RouteSource source) { Node node = _locate(method, path, handler.toString()); 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: logger.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()), new RouteInfo(method, path.toString(), handler) ); default: throw E.unsupport(); } } } private RequestHandler prepareReverseRoutes(RequestHandler handler, Node node) { if (handler instanceof RequestHandlerInfo) { RequestHandlerInfo info = (RequestHandlerInfo) handler; CharSequence 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, C.<String, Object>map(), fullUrl); } public String reverseRoute(String action) { return reverseRoute(action, C.<String, Object>map()); } public String reverseRoute(String action, Map<String, Object> 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<String> DEF_ACTION_PATH_PROVIDER = new $.Func0<String>() { @Override public String apply() throws NotAppliedException, Osgl.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<String> 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<String, Object> 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<String, Object> args) { Node root = root(method); Node node = root.reverseRoutes.get(action); if (null == node) { return null; } C.List<String> elements = C.newList(); args = new HashMap<>(args); while (root != node) { if (node.isDynamic()) { Node targetNode = node; for (Map.Entry<String, Node> entry : node.dynamicReverseAliases.entrySet()) { if (entry.getKey().equals(action)) { targetNode = entry.getValue(); break; } } S.Buffer buffer = S.buffer(); for ($.Transformer<Map<String, Object>, String> builder : targetNode.nodeValueBuilders) { String s = builder.transform(args); buffer.append(s); } String s = buffer.toString(); if (S.blank(s)) { s = S.string(args.remove(S.string(targetNode.varNames.get(0)))); } if (S.blank(s)) { s = S.string("-"); } elements.add(s); } else { elements.add(node.name.toString()); } node = node.parent; } S.Buffer sb = S.newBuffer(); Iterator<String> itr = elements.reverseIterator(); while (itr.hasNext()) { sb.append("/").append(itr.next()); } if (method == H.Method.GET && !args.isEmpty()) { boolean first = true; for (Map.Entry<String, Object> entry : args.entrySet()) { Object v = entry.getValue(); if (null == v) { continue; } String k = entry.getKey(); if (first) { sb.append("?"); first = false; } else { sb.append("&"); } sb.append(k).append("=").append(Codec.encodeUrl(v.toString())); } } return sb.toString(); } public String urlBase() { ActionContext context = ActionContext.current(); if (null != context) { return urlBase(context); } AppConfig<?> config = Act.appConfig(); /* * Note we support named port (restricted access) is running in the scope of * the internal network, thus assume we do not have secure http channel on top * of that */ boolean secure = null != portId && config.httpSecure(); String scheme = secure ? "https" : "http"; String domain = config.host(); String urlContext = config.urlContext(); if (80 == port || 443 == port) { return S.concat(scheme, "://", domain); } else { return S.concat(scheme, "://", domain, ":", S.string(port)); } } public String urlBase(ActionContext context) { H.Request req = context.req(); String scheme = req.secure() ? "https" : "http"; int port = req.port(); String domain = req.domain(); if (80 == port || 443 == port) { return S.fmt("%s://%s", scheme, domain); } else { return S.fmt("%s://%s:%s", scheme, domain, port); } } private String ensureUrlContext(String path) { String urlContext = appConfig.urlContext(); if (null == urlContext || path.startsWith(urlContext)) { if ("/".equals(path)) { path = ""; } return path; } if (!path.startsWith("/")) { path = S.concat("/", path); if (path.startsWith(urlContext)) { return path; } } if ("/".equals(path)) { path = ""; } return S.concat(urlContext, path); } public String fullUrl(String path, Object... args) { path = S.fmt(path, args); if (path.startsWith("//") || path.startsWith("http")) { return path; } if (path.contains(".") || path.contains("(")) { path = reverseRoute(path); } S.Buffer sb = S.newBuffer(urlBase()); path = ensureUrlContext(path); return sb.append(S.fmt(path, args)).toString(); } /** * Return full URL of reverse rout of specified action * @param action the action path * @param renderArgs the render arguments * @return the full URL as described above */ public String fullUrl(String action, Map<String, Object> renderArgs) { return fullUrl(reverseRoute(action, renderArgs)); } private static final Method M_FULL_URL = $.getMethod(Router.class, "fullUrl", String.class, Object[].class); public String _fullUrl(String path, Object[] args) { return $.invokeVirtual(this, M_FULL_URL, path, args); } boolean isMapped(H.Method method, CharSequence path) { return null != _search(method, path); } private static String routeInfo(H.Method method, CharSequence path, Object handler) { return S.fmt("[%s %s] - > [%s]", method, path, handler); } private Node _search(H.Method method, CharSequence path) { Node node = root(method); assert node != null; E.unsupportedIf(null == node, "Method %s is not supported", method); if (path.length() == 1 && path.charAt(0) == '/') { return node; } String sUrl = path.toString(); List<CharSequence> paths = Path.tokenize(Unsafe.bufOf(sUrl)); int len = paths.size(); for (int i = 0; i < len - 1; ++i) { node = node.findChild((StrBase) paths.get(i)); if (null == node) return null; } return node.findChild((StrBase) paths.get(len - 1)); } private Node _locate(H.Method method, CharSequence path, String action) { Node node = root(method); E.unsupportedIf(null == node, "Method %s is not supported", method); assert null != node; int pathLen = path.length(); if (0 == pathLen || (1 == pathLen && path.charAt(0) == '/')) { return node; } String sUrl = path.toString(); List<CharSequence> paths = Path.tokenize(Unsafe.bufOf(sUrl)); int len = paths.size(); for (int i = 0; i < len - 1; ++i) { CharSequence part = paths.get(i); if (checkIgnoreRestParts(node, part)) { return node; } node = node.addChild((StrBase) part, path, action); } CharSequence part = paths.get(len - 1); if (checkIgnoreRestParts(node, part)) { return node; } return node.addChild((StrBase) part, path, action); } private boolean checkIgnoreRestParts(Node node, CharSequence nextPart) { boolean shouldIgnoreRests = S.eq(IGNORE_NOTATION, S.string(nextPart)); E.invalidConfigurationIf(node.ignoreRestParts() && !shouldIgnoreRests, "Bad route configuration: parts appended to route that ends with \"...\""); E.invalidConfigurationIf(shouldIgnoreRests && !node.children().isEmpty(), "Bad route configuration: \"...\" appended to node that has children"); node.ignoreRestParts(shouldIgnoreRests); return shouldIgnoreRests; } // --- action handler resolving /** * Register 3rd party action handler resolver with specified directive * * @param directive * @param resolver */ public void registerRequestHandlerResolver(String directive, RequestHandlerResolver resolver) { resolvers.put(directive, resolver); } // -- action method sensor public boolean isActionMethod(String className, String methodName) { return actionNames.contains(S.concat(className, ".", methodName)); } // TODO: build controllerNames set to accelerate the process public boolean possibleController(String className) { return setContains(actionNames, className); } private static boolean setContains(Set<String> set, String name) { for (String s: set) { if (s.contains(name)) return true; } return false; } public void debug(PrintStream ps) { for (H.Method method : supportedHttpMethods()) { Node node = root(method); node.debug(method, ps); } } public List<RouteInfo> debug() { List<RouteInfo> info = C.newList(); debug(info); return C.list(info).sorted(); } public void debug(List<RouteInfo> routes) { for (H.Method method : supportedHttpMethods()) { Node node = root(method); node.debug(method, routes); } } public static H.Method[] supportedHttpMethods() { return targetMethods; } private Node search(H.Method method, Iterator<CharSequence> path, ActionContext context) { Node node = root(method); if (node.terminateRouteSearch()) { S.Buffer sb = S.newBuffer(); while (path.hasNext()) { sb.append('/').append(path.next()); } context.param(ParamNames.PATH, sb.toString()); return node; } while (null != node && path.hasNext()) { CharSequence nodeName = path.next(); node = node.child(nodeName, context); if (null != node) { if (node.terminateRouteSearch()) { if (!path.hasNext()) { context.param(ParamNames.PATH, ""); } else { S.Buffer sb = S.newBuffer(); while (path.hasNext()) { sb.append('/').append(path.next()); } context.param(ParamNames.PATH, sb.toString()); } break; } else if (node.ignoreRestParts()) { break; } } } return node; } private static class RequestHandlerInfo extends DelegateRequestHandler { private CharSequence action; protected RequestHandlerInfo(RequestHandler handler, CharSequence action) { super(handler); this.action = action; } RequestHandler theHandler() { return handler_; } @Override public String toString() { return action.toString(); } } private RequestHandlerInfo resolveActionHandler(CharSequence action) { $.T2<String, String> t2 = splitActionStr(action); String directive = t2._1, payload = t2._2; if (S.notEmpty(directive)) { RequestHandlerResolver resolver = resolvers.get(directive); RequestHandler handler = null == resolver ? BuiltInHandlerResolver.tryResolve(directive, payload, app()) : resolver.resolve(payload, app()); E.unsupportedIf(null == handler, "cannot find action handler by directive %s on payload %s", directive, payload); return new RequestHandlerInfo(handler, action); } else { RequestHandler handler = handlerLookup.resolve(payload, app()); E.unsupportedIf(null == handler, "cannot find action handler: %s", action); actionNames.add(payload); return new RequestHandlerInfo(handler, action); } } private $.T2<String, String> splitActionStr(CharSequence action) { FastStr fs = FastStr.of(action); FastStr fs1 = fs.beforeFirst(':'); FastStr fs2 = fs1.isEmpty() ? fs : fs.substr(fs1.length() + 1); return $.T2(fs1.trim().toString(), fs2.trim().toString()); } private Node root(H.Method method) { switch (method) { case GET: return _GET; case POST: return _POST; case PUT: return _PUT; case DELETE: return _DEL; case PATCH: return _PATCH; default: throw E.unexpected("HTTP Method not supported: %s", method); } } private static AlwaysNotFound notFound() { return AlwaysNotFound.INSTANCE; } private static AlwaysBadRequest badRequest() { return AlwaysBadRequest.INSTANCE; } public final class f { public $.Predicate<String> IS_CONTROLLER = new $.Predicate<String>() { @Override public boolean test(String s) { for (String action : actionNames) { if (action.startsWith(s)) { return true; } } return false; } }; } public final f f = new f(); /** * The data structure support decision tree for * fast URL routing */ private static class Node extends DestroyableBase implements Serializable, TreeNode, Comparable<Node> { // used to pass a baq request result when dynamic regex matching failed private static final Node BADREQUEST = new Node(Integer.MIN_VALUE) { @Override boolean terminateRouteSearch() { return true; } }; static { BADREQUEST.handler = AlwaysBadRequest.INSTANCE; } static Node newRoot(String name) { Node node = new Node(-1); node.name = S.str(name); return node; } private int id; private boolean isDynamic; // --- for static node private StrBase name; // ignore all the rest in URL when routing private boolean ignoreRestParts; // --- for dynamic node private Pattern pattern; private String patternTrait; private List<CharSequence> varNames = new ArrayList<>(); // used to build the node value for reverse routing private List<$.Transformer<Map<String, Object>, String>> nodeValueBuilders = new ArrayList<>(); // --- references private Node root; private Node parent; private List<Node> dynamicChilds = new ArrayList<>(); private Map<CharSequence, Node> staticChildren = new HashMap<>(); private Map<UrlPath, Node> dynamicAliases = new HashMap<>(); private Map<String, Node> dynamicReverseAliases = new HashMap<>(); private RequestHandler handler; private RouteSource routeSource; private Map<String, Node> reverseRoutes = new HashMap<>(); private Node(int id) { this.id = id; name = FastStr.EMPTY_STR; root = this; } Node(StrBase name, Node parent) { E.NPE(name); this.name = name; this.parent = parent; this.id = name.hashCode(); this.root = parent.root; parseDynaName(name); } @Override public int hashCode() { return id; } @Override public boolean equals(Object obj) { if (obj == this) return true; if (obj instanceof Node) { Node that = (Node) obj; return that.id == id && that.name.equals(name); } return false; } @Override public int compareTo(Node o) { if (!o.isDynamic && !isDynamic) { return name.compareTo(o.name); } int myVars = varNames.size(), hisVars = o.varNames.size(); if (myVars != hisVars) { return -(myVars - hisVars); } boolean fullVar = "(.*)".equals(patternTrait), hisIsFullVar = "(.*)".equals(o.patternTrait); if (fullVar == hisIsFullVar) { return name.compareTo(o.name); } return fullVar ? 1 : -1; } public boolean ignoreRestParts() { return ignoreRestParts; } public void ignoreRestParts(boolean ignore) { this.ignoreRestParts = ignore; } public boolean isDynamic() { return isDynamic; } boolean metaInfoMatches(StrBase string) { return this.isDynamic && $.eq(string, name); // $.Var<String> patternTraitsVar = $.var(); // // boolean isDynamic = parseDynaNameStyleA(string, null, null, patternTraitsVar); // // isDynamic = isDynamic || parseDynaNameStyleB( // string, null, null, // patternTraitsVar, null); // // return isDynamic && patternTrait.equals(patternTraitsVar.get()); } public boolean matches(CharSequence chars) { if (!isDynamic()) return name.contentEquals(chars); return (null == pattern) || pattern.matcher(chars).matches(); } @Override @SuppressWarnings("unchecked") public List<TreeNode> children() { C.List<TreeNode> list = (C.List) C.list(staticChildren.values()); return list.append(dynamicChilds); } public Node child(CharSequence name, ActionContext context) { Node node = staticChildren.get(name); if (null == node && !dynamicChilds.isEmpty()) { UrlPath path = new UrlPath(context.req().path()); for (Node targetNode : dynamicChilds) { for (Map.Entry<UrlPath, Node> entry : targetNode.dynamicAliases.entrySet()) { if (entry.getKey().equals(path)) { targetNode = entry.getValue(); break; } } Pattern pattern = targetNode.pattern; Matcher matcher = null == pattern ? null : pattern.matcher(name); if (null != matcher && matcher.matches()) { if (!targetNode.nodeValueBuilders.isEmpty()) { for (CharSequence varName : targetNode.varNames) { String varNameStr = varName.toString(); String varValue = matcher.group(varNameStr); if (S.notBlank(varValue)) { context.param(varNameStr, S.urlDecode(S.string(varValue))); } } } else { CharSequence varName = targetNode.varNames.get(0); context.param(varName.toString(), S.urlDecode(S.string(name))); } return targetNode; } } return Node.BADREQUEST; } return node; } @Override public String id() { return name.toString(); } @Override public String label() { StringBuilder sb = S.newBuilder(name); if (null != handler) { sb.append(" -> ").append(RouteInfo.compactHandler(handler.toString())); } return sb.toString(); } @Override protected void releaseResources() { if (null != handler) { handler.destroy(); } Destroyable.Util.destroyAll(dynamicChilds, ApplicationScoped.class); Destroyable.Util.destroyAll(staticChildren.values(), ApplicationScoped.class); staticChildren.clear(); } Node childByMetaInfo(StrBase s) { Node node = staticChildren.get(s); if (null == node && !dynamicChilds.isEmpty()) { for (Node targetNode : dynamicChilds) { if (targetNode.metaInfoMatches(s)) { return targetNode; } } } return node; } Node findChild(StrBase<?> name) { name = name.trim(); return childByMetaInfo(name); } Node addChild(StrBase<?> name, CharSequence path, String action) { name = name.trim(); Node node = childByMetaInfo(name); if (null != node) { return node; } Node child = new Node(name, this); if (child.isDynamic()) { boolean isAlias = false; for (Node targetNode : dynamicChilds) { if (S.eq(targetNode.patternTrait, child.patternTrait)) { targetNode.dynamicAliases.put(new UrlPath(path), child); targetNode.dynamicReverseAliases.put(action, child); isAlias = true; break; } } if (!isAlias) { child.dynamicAliases.put(new UrlPath(path), child); child.dynamicReverseAliases.put(action, child); dynamicChilds.add(child); } Collections.sort(dynamicChilds); return child; } else { staticChildren.put(name, child); } return child; } Node handler(RequestHandler handler, RouteSource source) { this.routeSource = $.notNull(source); this.handler = handler.requireResolveContext() ? new ContextualHandler((RequestHandlerBase) handler, this) : handler; return this; } RequestHandler handler() { return this.handler; } RouteSource routeSource() { return routeSource; } boolean terminateRouteSearch() { return null != handler && handler.supportPartialPath(); } String path() { if (null == parent) return "/"; String pPath = parent.path(); return S.pathConcat(pPath, '/', name.toString()); } void debug(H.Method method, PrintStream ps) { if (null != handler) { ps.printf("%s %s %s\n", method, path(), handler); } for (Node node : staticChildren.values()) { node.debug(method, ps); } for (Node node : dynamicChilds) { node.debug(method, ps); } } void debug(H.Method method, List<RouteInfo> routes) { if (null != handler) { routes.add(new RouteInfo(method, path(), handler)); } for (Node node : staticChildren.values()) { node.debug(method, routes); } for (Node node : dynamicChilds) { node.debug(method, routes); } } private void parseDynaName(StrBase name) { $.Var<Pattern> patternVar = $.var(); $.Var<String> patternTraitsVar = $.var(); boolean isDynamic = parseDynaNameStyleA(name, varNames, patternVar, patternTraitsVar); this.isDynamic = isDynamic || parseDynaNameStyleB( name, varNames, patternVar, patternTraitsVar, nodeValueBuilders); if (!this.isDynamic) { return; } this.pattern = patternVar.get(); this.patternTrait = patternTraitsVar.get(); } /* * case one `{var_name<regex>}`, e.g /{foo<[a-b]+>} * case two `{<regex>var_name}`, e.g /{<[a-b]+>foo>} * case three `foo-{var_name<regex>}-{var_name<regex}-bar...` */ static boolean parseDynaNameStyleB( StrBase name, List<CharSequence> varNames, $.Var<Pattern> pattern, $.Var<String> patternTrait, List<$.Transformer<Map<String, Object>, String>> nodeValueBuilders ) { int pos = name.indexOf('{'); if (pos < 0) { return false; } int len = name.length(); int lastPos = 0; int leftPos = pos; S.Buffer patternTraitBuilder = S.buffer(); S.Buffer patternStrBuilder = null == pattern ? null : S.buffer(); while (leftPos >= 0 & leftPos < len) { final StrBase literal = name.substr(lastPos, leftPos); if (!literal.isEmpty()) { patternTraitBuilder.append(literal); if (null != pattern) { patternStrBuilder.append(literal); } if (null != nodeValueBuilders) { nodeValueBuilders.add(new $.Transformer<Map<String, Object>, String>() { @Override public String transform(Map<String, Object> stringObjectMap) { return S.string(literal); } }); } } int rightAngle = name.indexOf('>', leftPos); if (rightAngle < 0) { if (name.indexOf('<', leftPos) < 0) { rightAngle = leftPos; } else { throw new RoutingException("Invalid route: " + name); } } pos = name.indexOf('}', rightAngle); if (pos < 0) { throw new RuntimeException("Invalid node: " + name); } $.T2<? extends CharSequence, Pattern> t2 = parseVarBlock(name, leftPos + 1, pos); final CharSequence varName = t2._1; if (null != varNames) { varNames.add(varName); } Pattern pattern1 = t2._2; String patternStr = ".*"; if (null != pattern1) { patternStr = pattern1.pattern(); } if (null != patternStrBuilder) { patternStrBuilder.append("(?<").append(varName).append(">").append(patternStr).append(")"); } patternTraitBuilder.append("(").append(patternStr).append(")"); if (null != nodeValueBuilders) { nodeValueBuilders.add(new $.Transformer<Map<String, Object>, String>() { @Override public String transform(Map<String, Object> stringObjectMap) { String s = S.string(varName); s = S.notBlank(s) ? s : "-"; return S.string(stringObjectMap.remove(s)); } }); } lastPos = pos + 1; leftPos = name.indexOf('{', lastPos); } StrBase literal = name.substr(lastPos, name.length()); if (!literal.isEmpty()) { if (literal.charAt(literal.length() - 1) == '}') { literal = literal.tail(-1); } if (!literal.isEmpty()) { final StrBase finalLiteral = literal; patternTraitBuilder.append(literal); if (null != patternStrBuilder) { patternStrBuilder.append(literal); } if (null != nodeValueBuilders) { nodeValueBuilders.add(new $.Transformer<Map<String, Object>, String>() { @Override public String transform(Map<String, Object> stringObjectMap) { return S.string(finalLiteral); } }); } } } if (null != pattern) { pattern.set(Pattern.compile(patternStrBuilder.toString())); } patternTrait.set(patternTraitBuilder.toString()); return true; } private static $.T2<? extends CharSequence, Pattern> parseVarBlock(StrBase name, int blockStart, int blockEnd) { int pos = name.indexOf('<', blockStart); if (pos < 0 || pos >= blockEnd) { return $.T2(name.substr(blockStart, blockEnd), null); } Pattern pattern; StrBase varName; if (pos == blockStart) { pos = name.indexOf('>', blockStart); if (pos >= blockEnd) { throw new RoutingException("Invalid route: " + name); } pattern = Pattern.compile(name.substring(blockStart + 1, pos)); varName = name.substr(pos + 1, blockEnd); } else { if (name.charAt(blockEnd - 1) != '>') { throw new RoutingException("Invalid route: " + name); } pattern = Pattern.compile(name.substring(pos + 1, blockEnd - 1)); varName = name.substr(blockStart, pos); } return $.T2(varName, pattern); } /* * case one: `var_name:regex`, e.g /foo:[a-b]+ * case two: `:var_name`, e.g /:foo * case three: `var_name:`, e.g /foo: */ static boolean parseDynaNameStyleA( StrBase name, List<CharSequence> varNames, $.Var<Pattern> pattern, $.Var<String> patternTrait ) { int pos = name.indexOf(':'); if (pos < 0) { return false; } if (0 == pos) { if (null != varNames) { varNames.add(name.substring(1)); } } else { int len = name.length(); if (pos == len - 1) { if (null != varNames) { varNames.add(name.substring(0, len - 2)); } } else { if (null != varNames) { varNames.add(name.substring(0, pos)); } String patternStr = name.substring(pos + 1, name.length()); if (null != pattern) { pattern.set(Pattern.compile(patternStr)); } patternTrait.set(patternStr); } } return true; } } private enum BuiltInHandlerResolver implements RequestHandlerResolver { echo() { @Override public RequestHandler resolve(CharSequence msg, App app) { return new Echo(msg.toString()); } }, redirect() { @Override public RequestHandler resolve(CharSequence payload, App app) { return new Redirect(payload.toString()); } }, redirectdir() { @Override public RequestHandler resolve(CharSequence payload, App app) { return new RedirectDir(payload.toString()); } }, file() { @Override public RequestHandler resolve(CharSequence base, App app) { return new StaticFileGetter(app.file(base.toString())); } }, resource() { @Override public RequestHandler resolve(CharSequence payload, App app) { return new StaticResourceGetter(payload.toString()); } }, externalfile() { @Override public RequestHandler resolve(CharSequence base, App app) { File file = new File(base.toString()); if (!file.canRead()) { logger.warn("External file not found: %s", file.getPath()); } return new StaticFileGetter(file); } }; private static RequestHandler tryResolve(CharSequence directive, CharSequence payload, App app) { String s = directive.toString().toLowerCase(); try { return valueOf(s).resolve(payload, app); } catch (IllegalArgumentException e) { return null; } } @Override public void destroy() { } @Override public boolean isDestroyed() { return true; } @Override public Class<? extends Annotation> scope() { return ApplicationScoped.class; } } private static class ContextualHandler extends DelegateRequestHandler { private Set<String> pathVariables; protected ContextualHandler(RequestHandlerBase next, Node node) { super(next); pathVariables = new HashSet<>(); while (node != null) { if (node.isDynamic()) { for (CharSequence varName : node.varNames) { pathVariables.add(varName.toString()); } } node = node.parent; } } @Override public void handle(ActionContext context) { context.attribute(ActionContext.ATTR_HANDLER, realHandler()); context.attribute(ActionContext.ATTR_PATH_VARS, pathVariables); context.resolve(); super.handle(context); } } }