/* * Copyright (C) 2010 eXo Platform SAS. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.exoplatform.web.controller.router; //import javanet.staxutils.IndentingXMLStreamWriter; import java.io.IOException; import java.io.StringWriter; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; import javax.xml.stream.XMLOutputFactory; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamWriter; import org.exoplatform.web.controller.QualifiedName; import org.exoplatform.web.controller.metadata.PathParamDescriptor; import org.exoplatform.web.controller.metadata.RequestParamDescriptor; import org.exoplatform.web.controller.metadata.RouteDescriptor; import org.exoplatform.web.controller.metadata.RouteParamDescriptor; import org.gatein.common.util.Tools; /** * The implementation of the routing algorithm. * * @author <a href="mailto:julien.viet@exoplatform.com">Julien Viet</a> * @version $Revision$ */ class Route { void writeTo(XMLStreamWriter writer) throws XMLStreamException { if (this instanceof SegmentRoute) { writer.writeStartElement("segment"); writer.writeAttribute("path", "/" + ((SegmentRoute) this).name); writer.writeAttribute("terminal", "" + terminal); } else if (this instanceof PatternRoute) { PatternRoute pr = (PatternRoute) this; StringBuilder path = new StringBuilder("/"); for (int i = 0; i < pr.params.length; i++) { path.append(pr.chunks[i]).append("{").append(pr.params[i].name.getValue()).append("}"); } path.append(pr.chunks[pr.chunks.length - 1]); writer.writeStartElement("pattern"); writer.writeAttribute("path", path.toString()); writer.writeAttribute("terminal", Boolean.toString(terminal)); for (PathParam param : pr.params) { writer.writeStartElement("path-param"); writer.writeAttribute("qname", param.name.getValue()); writer.writeAttribute("encodingMode", param.encodingMode.toString()); writer.writeAttribute("pattern", param.matchingRegex.toString()); writer.writeEndElement(); } } else { writer.writeStartElement("route"); } // for (RouteParam routeParam : routeParamArray) { writer.writeStartElement("route-param"); writer.writeAttribute("qname", routeParam.name.getValue()); writer.writeAttribute("value", routeParam.value); writer.writeEndElement(); } // for (RequestParam requestParam : requestParamArray) { writer.writeStartElement("request-param"); writer.writeAttribute("qname", requestParam.name.getValue()); writer.writeAttribute("name", requestParam.matchName); if (requestParam.matchPattern != null) { writer.writeAttribute("value", requestParam.matchPattern.getPattern()); } writer.writeEndElement(); } // /* * for (Map.Entry<String, SegmentRoute[]> entry : segments.entrySet()) { writer.writeStartElement("segment"); * writer.writeAttribute("name", entry.getKey()); for (SegmentRoute segment : entry.getValue()) { * segment.writeTo(writer); } writer.writeEndElement(); } * * // for (PatternRoute pattern : patterns) { pattern.writeTo(writer); } */ // writer.writeEndElement(); } @Override public String toString() { try { XMLOutputFactory factory = XMLOutputFactory.newInstance(); StringWriter sw = new StringWriter(); XMLStreamWriter xmlWriter = factory.createXMLStreamWriter(sw); // xmlWriter = new IndentingXMLStreamWriter(xmlWriter); writeTo(xmlWriter); return sw.toString(); } catch (XMLStreamException e) { throw new AssertionError(e); } } /** . */ private static final Route[] EMPTY_ROUTE_ARRAY = new Route[0]; /** . */ private static final RouteParam[] EMPTY_ROUTE_PARAM_ARRAY = new RouteParam[0]; /** . */ private static final RequestParam[] EMPTY_REQUEST_PARAM_ARRAY = new RequestParam[0]; /** . */ private final Router router; /** . */ private Route parent; /** . */ private boolean terminal; /** . */ private Route[] children; /** . */ private Map<QualifiedName, RouteParam> routeParamMap; /** . */ private RouteParam[] routeParamArray; /** . */ private Map<String, RequestParam> requestParamMap; /** . */ private RequestParam[] requestParamArray; Route(Router router) { this.router = router; this.parent = null; this.terminal = true; this.children = EMPTY_ROUTE_ARRAY; this.routeParamMap = Collections.emptyMap(); this.routeParamArray = EMPTY_ROUTE_PARAM_ARRAY; this.requestParamMap = Collections.emptyMap(); this.requestParamArray = EMPTY_REQUEST_PARAM_ARRAY; } final boolean isTerminal() { return terminal; } /* * Ok, so this is not the fastest way to do it, but for now it's OK, it's what is needed, we'll find a way to optimize it * later with some precompilation. */ final void render(RenderContext context, URIWriter writer) throws IOException { RouteMatch r = find(context); // We found a route we need to render it now if (r != null) { r.render(writer); } } static class RouteMatch { /** The matched route. */ final Route route; /** The matched parameters. */ final Map<QualifiedName, String> matches; /** . */ final RenderContext context; RouteMatch(RenderContext context, Route route, Map<QualifiedName, String> matches) { this.context = context; this.route = route; this.matches = matches; } private void render(URIWriter writer) throws IOException { // Append path first renderPath(route, writer, false); // Append query parameters after renderQueryString(route, writer); } private boolean renderPath(Route route, URIWriter writer, boolean hasChildren) throws IOException { boolean endWithSlash; if (route.parent != null) { endWithSlash = renderPath(route.parent, writer, true); } else { endWithSlash = false; } // if (route instanceof SegmentRoute) { SegmentRoute sr = (SegmentRoute) route; if (!endWithSlash) { writer.append('/'); endWithSlash = true; } String name = sr.encodedName; writer.append(name); if (name.length() > 0) { endWithSlash = false; } } else if (route instanceof PatternRoute) { PatternRoute pr = (PatternRoute) route; if (!endWithSlash) { writer.append('/'); endWithSlash = true; } int i = 0; int count = 0; while (i < pr.params.length) { writer.append(pr.encodedChunks[i]); count += pr.chunks[i].length(); // PathParam def = pr.params[i]; String value = matches.get(def.name); count += value.length(); // Write value for (int len = value.length(), j = 0; j < len; j++) { char c = value.charAt(j); if (c == route.router.separatorEscape) { if (def.encodingMode == EncodingMode.PRESERVE_PATH) { writer.append('_'); } else { writer.append('%'); writer.append(route.router.separatorEscapeNible1); writer.append(route.router.separatorEscapeNible2); } } else if (c == '/') { writer.append(def.encodingMode == EncodingMode.PRESERVE_PATH ? '/' : route.router.separatorEscape); } else { writer.appendSegment(c); } } // i++; } writer.append(pr.encodedChunks[i]); count += pr.chunks[i].length(); if (count > 0) { endWithSlash = false; } } else { if (!hasChildren) { writer.append('/'); endWithSlash = true; } } // return endWithSlash; } private void renderQueryString(Route route, URIWriter writer) throws IOException { if (route.parent != null) { renderQueryString(route.parent, writer); } // for (RequestParam requestParamDef : route.requestParamArray) { String s = matches.get(requestParamDef.name); switch (requestParamDef.valueMapping) { case CANONICAL: break; case NEVER_EMPTY: if (s != null && s.length() == 0) { s = null; } break; case NEVER_NULL: if (s == null) { s = ""; } break; } if (s != null) { writer.appendQueryParameter(requestParamDef.matchName, s); } } } } final RouteMatch find(RenderContext context) { context.enter(); RouteMatch route = _find(context); context.leave(); return route; } private RouteMatch _find(RenderContext context) { // Match first the static parameteters for (RouteParam param : routeParamArray) { RenderContext.Parameter entry = context.getParameter(param.name); if (entry != null && !entry.isMatched() && param.value.equals(entry.getValue())) { entry.remove(entry.getValue()); } else { return null; } } // Match any request parameter for (RequestParam requestParamDef : requestParamArray) { RenderContext.Parameter entry = context.getParameter(requestParamDef.name); boolean matched = false; if (entry != null && !entry.isMatched()) { if (requestParamDef.matchPattern == null || context.matcher(requestParamDef.matchPattern).matches(entry.getValue())) { matched = true; } } if (matched) { entry.remove(entry.getValue()); } else { switch (requestParamDef.controlMode) { case OPTIONAL: // Do nothing break; case REQUIRED: return null; default: throw new AssertionError(); } } } // Match any pattern parameter if (this instanceof PatternRoute) { PatternRoute prt = (PatternRoute) this; for (int i = 0; i < prt.params.length; i++) { PathParam param = prt.params[i]; RenderContext.Parameter s = context.getParameter(param.name); String matched = null; if (s != null && !s.isMatched()) { switch (param.encodingMode) { case FORM: case PRESERVE_PATH: for (int j = 0; j < param.matchingRegex.length; j++) { Regex renderingRegex = param.matchingRegex[j]; if (context.matcher(renderingRegex).matches(s.getValue())) { matched = param.templatePrefixes[j] + s.getValue() + param.templateSuffixes[j]; break; } } break; default: throw new AssertionError(); } } if (matched != null) { s.remove(matched); } else { return null; } } } // if (context.isEmpty() && terminal) { Map<QualifiedName, String> matches = Collections.emptyMap(); for (QualifiedName name : context.getNames()) { RenderContext.Parameter parameter = context.getParameter(name); if (matches.isEmpty()) { matches = new HashMap<QualifiedName, String>(); } String match = parameter.getMatch(); matches.put(name, match); } return new RouteMatch(context, this, matches); } // for (Route route : children) { RouteMatch a = route.find(context); if (a != null) { return a; } } // return null; } /** * Create a route matcher for the a request. * * @param path the path * @param requestParams the query parameters * @return the route matcher */ final RouteMatcher route(String path, Map<String, String[]> requestParams) { return new RouteMatcher(this, Path.parse(path), requestParams); } static class RouteFrame { /** * Defines the status of a frame. */ static enum Status { BEGIN, MATCHED_PARAMS, PROCESS_CHILDREN, MATCHED, END } /** . */ private final RouteFrame parent; /** . */ private final Route route; /** . */ private final Path path; /** . */ private Status status; /** The matches. */ private Map<QualifiedName, String> matches; /** * The index when iterating child in * {@link org.exoplatform.web.controller.router.Route.RouteFrame.Status#PROCESS_CHILDREN} status. */ private int childIndex; private RouteFrame(RouteFrame parent, Route route, Path path) { this.parent = parent; this.route = route; this.path = path; this.status = Status.BEGIN; this.childIndex = 0; } private RouteFrame(Route route, Path path) { this(null, route, path); } Map<QualifiedName, String> getParameters() { Map<QualifiedName, String> parameters = null; for (RouteFrame frame = this; frame != null; frame = frame.parent) { if (frame.matches != null) { if (parameters == null) { parameters = new HashMap<QualifiedName, String>(); } parameters.putAll(frame.matches); } for (RouteParam param : frame.route.routeParamArray) { if (parameters == null) { parameters = new HashMap<QualifiedName, String>(); } parameters.put(param.name, param.value); } } return parameters != null ? parameters : Collections.<QualifiedName, String> emptyMap(); } } static class RouteMatcher implements Iterator<Map<QualifiedName, String>> { /** . */ private final Map<String, String[]> requestParams; /** . */ private RouteFrame frame; /** . */ private RouteFrame next; RouteMatcher(Route route, Path path, Map<String, String[]> requestParams) { this.frame = new RouteFrame(route, path); this.requestParams = requestParams; } public boolean hasNext() { if (next == null) { if (frame != null) { frame = route(frame, requestParams); } if (frame != null && frame.status == RouteFrame.Status.MATCHED) { next = frame; } } return next != null; } public Map<QualifiedName, String> next() { if (!hasNext()) { throw new NoSuchElementException(); } Map<QualifiedName, String> parameters = next.getParameters(); next = null; return parameters; } public void remove() { throw new UnsupportedOperationException(); } } private static RouteFrame route(RouteFrame root, Map<String, String[]> requestParams) { RouteFrame current = root; // if (root.status == RouteFrame.Status.MATCHED) { if (root.parent != null) { current = root.parent; } else { return null; } } else if (root.status != RouteFrame.Status.BEGIN) { throw new AssertionError("Unexpected status " + root.status); } // while (true) { if (current.status == RouteFrame.Status.BEGIN) { boolean matched = true; // We enter a frame for (RequestParam requestParamDef : current.route.requestParamArray) { String value = null; String[] values = requestParams.get(requestParamDef.matchName); if (values != null && values.length > 0 && values[0] != null) { value = values[0]; } if (value == null) { switch (requestParamDef.controlMode) { case OPTIONAL: // Do nothing break; case REQUIRED: matched = false; break; } } else if (!requestParamDef.matchValue(value)) { matched = false; break; } switch (requestParamDef.valueMapping) { case CANONICAL: break; case NEVER_EMPTY: if (value != null && value.length() == 0) { value = null; } break; case NEVER_NULL: if (value == null) { value = ""; } break; } if (value != null) { if (current.matches == null) { current.matches = new HashMap<QualifiedName, String>(); } current.matches.put(requestParamDef.name, value); } } // if (matched) { // We enter next state current.status = RouteFrame.Status.MATCHED_PARAMS; } else { current.status = RouteFrame.Status.END; } } else if (current.status == RouteFrame.Status.MATCHED_PARAMS) { RouteFrame.Status next; // Anything that does not begin with '/' returns null if (current.path.length() > 0 && current.path.charAt(0) == '/') { // The '/' means the current controller if any, otherwise it may be processed by the pattern matching if (current.path.length() == 1 && current.route.terminal) { next = RouteFrame.Status.MATCHED; } else { next = RouteFrame.Status.PROCESS_CHILDREN; } } else { next = RouteFrame.Status.END; } // current.status = next; } else if (current.status == RouteFrame.Status.PROCESS_CHILDREN) { if (current.childIndex < current.route.children.length) { Route child = current.route.children[current.childIndex++]; // The next frame RouteFrame next; // if (child instanceof SegmentRoute) { SegmentRoute segmentRoute = (SegmentRoute) child; // if (segmentRoute.name.length() == 0) { // Delegate the process to the next route next = new RouteFrame(current, segmentRoute, current.path); } else { // Find the next '/' for determining the segment and next path // JULIEN : this can be computed multiple times int pos = current.path.indexOf('/', 1); if (pos == -1) { pos = current.path.length(); } String segment = current.path.getValue().substring(1, pos); // Determine next path if (segmentRoute.name.equals(segment)) { // Lazy create next segment path // JULIEN : this can be computed multiple times Path nextSegmentPath; if (pos == current.path.length()) { // todo make a constant nextSegmentPath = Path.SLASH; } else { nextSegmentPath = current.path.subPath(pos); } // Delegate the process to the next route next = new RouteFrame(current, segmentRoute, nextSegmentPath); } else { next = null; } } } else if (child instanceof PatternRoute) { PatternRoute patternRoute = (PatternRoute) child; // Regex.Match[] matches = patternRoute.pattern.matcher().find(current.path.getValue()); // We match if (matches.length > 0) { // Build next controller context int nextPos = matches[0].getEnd(); Path nextPath; if (current.path.length() == nextPos) { nextPath = Path.SLASH; } else { if (nextPos > 0 && current.path.charAt(nextPos - 1) == '/') { nextPos--; } // nextPath = current.path.subPath(nextPos); } // Delegate to next patternRoute next = new RouteFrame(current, patternRoute, nextPath); // JULIEN : this can be done lazily // Append parameters int index = 1; for (int i = 0; i < patternRoute.params.length; i++) { PathParam param = patternRoute.params[i]; for (int j = 0; j < param.matchingRegex.length; j++) { Regex.Match match = matches[index + j]; if (match.getEnd() != -1) { String value; if (param.encodingMode == EncodingMode.FORM) { StringBuilder sb = new StringBuilder(); for (int from = match.getStart(); from < match.getEnd(); from++) { char c = current.path.charAt(from); if (c == child.router.separatorEscape && current.path.getRawLength(from) == 1) { c = '/'; } sb.append(c); } value = sb.toString(); } else { value = match.getValue(); } if (next.matches == null) { next.matches = new HashMap<QualifiedName, String>(); } next.matches.put(param.name, value); break; } else { // It can be the match of a particular disjunction // or an optional parameter } } index += param.matchingRegex.length; } } else { next = null; } } else { throw new AssertionError(); } // if (next != null) { current = next; } } else { current.status = RouteFrame.Status.END; } } else if (current.status == RouteFrame.Status.MATCHED) { // We found a solution break; } else if (current.status == RouteFrame.Status.END) { if (current.parent != null) { current = current.parent; } else { // The end of the search break; } } else { throw new AssertionError(); } } // return current; } final <R extends Route> R add(R route) throws MalformedRouteException { if (route == null) { throw new NullPointerException("No null route accepted"); } if (((Route) route).parent != null) { throw new IllegalArgumentException("No route with an existing parent can be accepted"); } // LinkedList<Param> ancestorParams = new LinkedList<Param>(); findAncestorOrSelfParams(ancestorParams); LinkedList<Param> descendantParams = new LinkedList<Param>(); for (Param param : ancestorParams) { ((Route) route).findDescendantOrSelfParams(param.name, descendantParams); if (descendantParams.size() > 0) { throw new MalformedRouteException("Duplicate parameter " + param.name); } } // if (route instanceof PatternRoute || route instanceof SegmentRoute) { children = Tools.appendTo(children, route); terminal = false; ((Route) route).parent = this; } else { throw new IllegalArgumentException("Only accept segment or pattern routes"); } // return route; } final Set<String> getSegmentNames() { Set<String> names = new HashSet<String>(); for (Route child : children) { if (child instanceof SegmentRoute) { SegmentRoute childSegment = (SegmentRoute) child; names.add(childSegment.name); } } return names; } final int getSegmentSize(String segmentName) { int size = 0; for (Route child : children) { if (child instanceof SegmentRoute) { SegmentRoute childSegment = (SegmentRoute) child; if (segmentName.equals(childSegment.name)) { size++; } } } return size; } final SegmentRoute getSegment(String segmentName, int index) { for (Route child : children) { if (child instanceof SegmentRoute) { SegmentRoute childSegment = (SegmentRoute) child; if (segmentName.equals(childSegment.name)) { if (index == 0) { return childSegment; } else { index--; } } } } return null; } final int getPatternSize() { int size = 0; for (Route route : children) { if (route instanceof PatternRoute) { size++; } } return size; } final PatternRoute getPattern(int index) { for (Route route : children) { if (route instanceof PatternRoute) { if (index == 0) { return (PatternRoute) route; } else { index--; } } } return null; } final Route append(RouteDescriptor descriptor) throws MalformedRouteException { Route route = append(descriptor.getPathParams(), descriptor.getPath()); // for (RouteParamDescriptor routeParamDesc : descriptor.getRouteParams()) { route.add(RouteParam.create(routeParamDesc)); } // for (RequestParamDescriptor requestParamDesc : descriptor.getRequestParams()) { route.add(RequestParam.create(requestParamDesc, router)); } // for (RouteDescriptor childDescriptor : descriptor.getChildren()) { route.append(childDescriptor); } // return route; } final Route add(RouteParam param) throws MalformedRouteException { Param existing = findParam(param.name); if (existing != null) { throw new MalformedRouteException("Duplicate parameter " + param.name); } if (routeParamArray.length == 0) { routeParamMap = new HashMap<QualifiedName, RouteParam>(); } routeParamMap.put(param.name, param); routeParamArray = Tools.appendTo(routeParamArray, param); return this; } final Route add(RequestParam param) throws MalformedRouteException { Param existing = findParam(param.name); if (existing != null) { throw new MalformedRouteException("Duplicate parameter " + param.name); } if (requestParamArray.length == 0) { requestParamMap = new HashMap<String, RequestParam>(); } requestParamMap.put(param.matchName, param); requestParamArray = Tools.appendTo(requestParamArray, param); return this; } /** * Append a path, creates the necessary routes and returns the last route added. * * @param pathParamDescriptors the path param descriptors * @param path the path to append * @return the last route added */ private Route append(Map<QualifiedName, PathParamDescriptor> pathParamDescriptors, String path) throws MalformedRouteException { if (path.length() == 0 || path.charAt(0) != '/') { throw new MalformedRouteException(); } // int pos = path.length(); int level = 0; List<Integer> start = new ArrayList<Integer>(); List<Integer> end = new ArrayList<Integer>(); for (int i = 1; i < path.length(); i++) { char c = path.charAt(i); if (c == '{') { if (level++ == 0) { start.add(i); } } else if (c == '}') { if (--level == 0) { end.add(i); } } else if (c == '/') { if (level == 0) { pos = i; break; } } } // Route next; if (start.isEmpty()) { String segment = path.substring(1, pos); SegmentRoute route = new SegmentRoute(router, segment); add(route); next = route; } else { if (start.size() == end.size()) { PatternBuilder builder = new PatternBuilder(); builder.expr("^").expr('/'); List<String> chunks = new ArrayList<String>(); List<PathParam> parameterPatterns = new ArrayList<PathParam>(); // int previous = 1; for (int i = 0; i < start.size(); i++) { builder.litteral(path, previous, start.get(i)); chunks.add(path.substring(previous, start.get(i))); String parameterName = path.substring(start.get(i) + 1, end.get(i)); // QualifiedName parameterQName = QualifiedName.parse(parameterName); // Now get path param metadata PathParamDescriptor parameterDescriptor = pathParamDescriptors.get(parameterQName); // PathParam param; if (parameterDescriptor != null) { param = PathParam.create(parameterDescriptor, router); } else { param = PathParam.create(parameterQName, router); } // Append routing regex to the route regex surrounded by a non capturing regex // to isolate routingRegex like a|b or a(.)b builder.expr("(?:").expr(param.routingRegex).expr(")"); // Add the path param with the rendering regex parameterPatterns.add(param); previous = end.get(i) + 1; } // builder.litteral(path, previous, pos); // We want to satisfy one of the following conditions // - the next char after the matched expression is '/' // - the expression matched until the end // - the match expression is the '/' expression builder.expr("(?:(?<=^/)|(?=/)|$)"); // chunks.add(path.substring(previous, pos)); PatternRoute route = new PatternRoute(router, router.compile(builder.build()), parameterPatterns, chunks); // Wire add(route); // next = route; } else { throw new UnsupportedOperationException("Report error"); } } // if (pos < path.length()) { return next.append(pathParamDescriptors, path.substring(pos)); } else { return next; } } private Param getParam(QualifiedName name) { Param param = routeParamMap.get(name); if (param == null) { for (RequestParam requestParam : requestParamArray) { if (requestParam.name.equals(name)) { param = requestParam; break; } } if (param == null && this instanceof PatternRoute) { for (PathParam pathParam : ((PatternRoute) this).params) { if (pathParam.name.equals(name)) { param = pathParam; break; } } } } return param; } private Param findParam(QualifiedName name) { Param param = getParam(name); if (param == null && parent != null) { param = parent.findParam(name); } return param; } private void findParams(List<Param> params) { Collections.addAll(params, routeParamArray); for (RequestParam param : requestParamArray) { params.add(param); } if (this instanceof PatternRoute) { Collections.addAll(params, ((PatternRoute) this).params); } } private void findAncestorOrSelfParams(List<Param> params) { findParams(params); if (parent != null) { parent.findAncestorOrSelfParams(params); } } /** * Find the params having the specified <code>name</code> among this route or its descendants. * * @param name the name * @param params the list collecting the found params */ private void findDescendantOrSelfParams(QualifiedName name, List<Param> params) { Param param = getParam(name); if (param != null) { params.add(param); } } }