All Downloads are FREE. Search and download functionalities are using the official Maven repository.

juzu.impl.router.Route Maven / Gradle / Ivy

/*
 * Copyright 2013 eXo Platform SAS
 *
 * 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.
 */

package juzu.impl.router;

//import javanet.staxutils.IndentingXMLStreamWriter;

import juzu.impl.common.UriBuilder;
import juzu.impl.router.parser.RouteParser;
import juzu.impl.router.parser.RouteParserHandler;
import juzu.impl.router.regex.RE;
import juzu.impl.router.regex.SyntaxException;
import juzu.impl.common.Tools;

import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
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;

/**
 * The implementation of the routing algorithm.
 *
 * @author Julien Viet
 * @version $Revision$
 */
public class Route {

  /** . */
  static final int TERMINATION_NONE = 0;

  /** . */
  static final int TERMINATION_SEGMENT = 1;

  /** . */
  static final int TERMINATION_SEPARATOR = 2;

  /** . */
  static final int TERMINATION_ANY = 3;

  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).append("}");
      }
      path.append(pr.chunks[pr.chunks.length - 1]);
      writer.writeStartElement("pattern");
      writer.writeAttribute("path", path.toString());
      writer.writeAttribute("terminal", Integer.toString(terminal));
      for (PathParam param : pr.params) {
        writer.writeStartElement("path-param");
        writer.writeAttribute("qname", param.name);
        writer.writeAttribute("preservePath", "" + param.preservePath);
        writer.writeAttribute("pattern", param.matchingRegex.toString());
        writer.writeEndElement();
      }
    }
    else {
      writer.writeStartElement("route");
    }

    //
/*
      for (Map.Entry 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 final Router router;

  /** . */
  private final int terminal;

  /** . */
  private Route parent;

  /** . */
  private List path;

  /** . */
  private Route[] children;

  Route(Router router, int terminal) {

    // Invoked by Router subclass ... not pretty but simple and does the work
    if (router == null) {
      router = (Router)this;
    }

    //
    this.router = router;

    //
    this.path = Collections.singletonList(this);
    this.parent = null;
    this.terminal = terminal;
    this.children = EMPTY_ROUTE_ARRAY;
  }

  /**
   * Clear this route of the children it may have, when cleared this route becomes a terminal route.
   */
  public final void clearChildren() {
    this.children = EMPTY_ROUTE_ARRAY;
  }

  /**
   * Returns the parent route or null when the route is the root route.
   *
   * @return the parent route
   */
  public final Route getParent() {
    return parent;
  }

  /**
   * Returns the path of routes from the root to this route.
   *
   * @return the path
   */
  public final List getPath() {
    return path;
  }

  final boolean renderPath(RouteMatch match, UriBuilder writer, boolean hasChildren) throws IOException {

    //
    boolean endWithSlash = parent != null && parent.renderPath(match, writer, true);

    //
    if (this instanceof SegmentRoute) {
      SegmentRoute sr = (SegmentRoute)this;
      if (!endWithSlash) {
        writer.append('/');
      }
      String name = sr.encodedName;
      writer.append(name);
      endWithSlash = false;
    }
    else if (this instanceof EmptyRoute) {
      if (!endWithSlash) {
        writer.append('/');
        endWithSlash = true;
      }
    }
    else if (this instanceof PatternRoute) {
      PatternRoute pr = (PatternRoute)this;
      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 = match.matched.get(def);
        count += value.length();

        // Write value
        for (int len = value.length(), j = 0;j < len;j++) {
          char c = value.charAt(j);
          if (c == router.separatorEscape) {
            if (def.preservePath) {
              writer.append('_');
            }
            else {
              writer.append('%');
              writer.append(router.separatorEscapeNible1);
              writer.append(router.separatorEscapeNible2);
            }
          }
          else if (c == '/') {
            writer.append(def.preservePath ? '/' : 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;
  }

  public final RouteMatch matches(Map parameters) {

    //
    HashMap unmatched = new HashMap(parameters);
    HashMap matched = new HashMap();

    //
    if (_matches(unmatched, matched)) {
      return new RouteMatch(this, unmatched, matched);
    } else {
      return null;
    }
  }

  private boolean _matches(HashMap context, HashMap matched) {
    return (parent == null || parent._matches(context, matched)) && matches(context, matched);
  }

  private boolean matches(HashMap context, HashMap abc) {

    // 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];
        String s = context.get(param.name);
        String matched = null;
        if (s != null) {
          for (int j = 0;j < param.matchingRegex.length;j++) {
            RERef renderingRegex = param.matchingRegex[j];
            if (renderingRegex.re.matcher().matches(s)) {
              matched = param.templatePrefixes[j] + s + param.templateSuffixes[j];
              break;
            }
          }
        }
        if (matched != null) {
          context.remove(param.name);
          abc.put(param, matched);
        }
        else {
          return false;
        }
      }
    }

    //
    return true;
  }

  public final RouteMatch route(String path) {
    return route(path, Collections.emptyMap());
  }

  public final RouteMatch route(String path, Map queryParams) {
    Iterator matcher = matcher(path, queryParams);
    return matcher.hasNext() ? matcher.next() : null;
  }

  /**
   * Create a route matcher for the a request.
   *
   * @param path          the path
   * @param requestParams the query parameters
   * @return the route matcher
   */
  public final Iterator matcher(String path, Map requestParams) {

    // Always start with a '/'
    if (!path.startsWith("/")) {
      path = "/" + path;
    }

    //
    return new RouteMatcher(this, Path.parse(path), requestParams);
  }

  static class RouteFrame {

    /** Defines the status of a frame. */
    static enum Status {
      BEGIN,

      PROCESS_CHILDREN,

      DO_CHECK,

      MATCHED,

      END

    }

    /** . */
    private final RouteFrame parent;

    /** . */
    private final Route route;

    /** . */
    private final Path path;

    /** . */
    private Status status;

    /** The matches. */
    private Map matches;

    /** The index when iterating child in {@link juzu.impl.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 getParameters() {
      Map parameters = null;
      for (RouteFrame frame = this;frame != null;frame = frame.parent) {
        if (frame.matches != null) {
          if (parameters == null) {
            parameters = new HashMap();
          }
          parameters.putAll(frame.matches);
        }
      }
      return parameters != null ? parameters : Collections.emptyMap();
    }
  }

  static class RouteMatcher implements Iterator {

    /** . */
    private final Map requestParams;

    /** . */
    private RouteFrame frame;

    /** . */
    private RouteFrame next;

    RouteMatcher(Route route, Path path, Map 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 RouteMatch next() {
      if (!hasNext()) {
        throw new NoSuchElementException();
      }
      Map parameters = next.getParameters();
      Route route = next.route;
      next = null;
      return new RouteMatch(route, parameters);
    }

    public void remove() {
      throw new UnsupportedOperationException();
    }
  }

  private static RouteFrame route(RouteFrame root, Map 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) {
        current.status = RouteFrame.Status.PROCESS_CHILDREN;
      }
      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 EmptyRoute) {
            next = new RouteFrame(current, child, current.path);
          }
          else if (child instanceof SegmentRoute) {
            SegmentRoute segmentRoute = (SegmentRoute)child;

            // Remove any leading slashes
            int POS = 0;
            while (POS < current.path.length() && current.path.charAt(POS) == '/') {
              POS++;
            }

            // Find the next '/' for determining the segment and next path
            // JULIEN : this can be computed multiple times
            int pos = current.path.indexOf('/', POS);
            if (pos == -1) {
              pos = current.path.length();
            }

            //
            String segment = current.path.getValue().substring(POS, pos);

            // Determine next path
            if (segmentRoute.name.equals(segment)) {
              // Lazy create next segment path
              // JULIEN : this can be computed multiple times
              Path 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;

            // We skip one '/' , should we skip more ? this raise issues with path encoding that manages '/'
            // need to figure out later
            Path path = current.path;
            if (path.length() > 0 && path.charAt(0) == '/') {
              path = path.subPath(1);
            }

            //
            RE.Match[] matches = patternRoute.pattern.re.matcher().find(path.getValue());

            // We match
            if (matches.length > 0) {
              // Build next controller context
              int nextPos = matches[0].getEnd();
              Path nextPath = 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++) {
                  RE.Match match = matches[index + j];
                  if (match.getEnd() != -1) {
                    String value;
                    if (!param.preservePath) {
                      StringBuilder sb = new StringBuilder();
                      for (int from = match.getStart();from < match.getEnd();from++) {
                        char c = path.charAt(from);
                        if (c == child.router.separatorEscape && !path.isEscaped(from)) {
                          c = '/';
                        }
                        sb.append(c);
                      }
                      value = sb.toString();
                    }
                    else {
                      value = match.getValue();
                    }
                    if (next.matches == null) {
                      next.matches = new HashMap();
                    }
                    next.matches.put(param, 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.DO_CHECK;
        }
      }
      else if (current.status == RouteFrame.Status.DO_CHECK) {

        // Find the index of the first char that is not a '/'
        int pos = 0;
        while (pos < current.path.length() && current.path.charAt(pos) == '/') {
          pos++;
        }

        // Are we done ?
        RouteFrame.Status next;
        if (pos == current.path.length()) {
          if (current.route instanceof EmptyRoute) {
            next = RouteFrame.Status.MATCHED;
          } else {
            switch (current.route.terminal) {
              case TERMINATION_NONE:
                next = RouteFrame.Status.END;
                break;
              case TERMINATION_SEGMENT:
                next = pos == 0 ? RouteFrame.Status.MATCHED : RouteFrame.Status.END;
                break;
              case TERMINATION_SEPARATOR:
                next = pos == 0 ? RouteFrame.Status.END : RouteFrame.Status.MATCHED;
                break;
              case TERMINATION_ANY:
                next = RouteFrame.Status.MATCHED;
                break;
              default:
                throw new AssertionError();
            }
          }
        } else {
          next = RouteFrame.Status.END;
        }

        //
        current.status = next;
      }
      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;
  }

  private void add(Route route) throws MalformedRouteException {
    if (route == null) {
      throw new NullPointerException("No null route accepted");
    }
    if (route.parent != null) {
      throw new IllegalArgumentException("No route with an existing parent can be accepted");
    }

    //
    LinkedList ancestorParams = new LinkedList();
    findAncestorOrSelfParams(ancestorParams);
    LinkedList descendantParams = new LinkedList();
    for (PathParam param : ancestorParams) {
      route.findDescendantOrSelfParams(param.name, descendantParams);
      if (descendantParams.size() > 0) {
        throw new MalformedRouteException("Duplicate parameter " + param.name);
      }
    }

    //
    if (route instanceof PatternRoute || route instanceof SegmentRoute || route instanceof EmptyRoute) {
      children = Tools.appendTo(children, route);

      // Compute path
      List path = new ArrayList(this.path.size() + 1);
      path.addAll(this.path);
      path.add(route);

      //
      route.parent = this;
      route.path = Collections.unmodifiableList(path);
    }
    else {
      throw new IllegalArgumentException("Only accept segment or pattern routes");
    }
  }

  final Set getSegmentNames() {
    Set names = new HashSet();
    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;
  }

  private PathParam getParam(String name) {
    PathParam param = null;
    if (this instanceof PatternRoute) {
      for (PathParam pathParam : ((PatternRoute)this).params) {
        if (pathParam.name.equals(name)) {
          param = pathParam;
          break;
        }
      }
    }
    return param;
  }

  private PathParam findParam(String name) {
    PathParam param = getParam(name);
    if (param == null && parent != null) {
      param = parent.findParam(name);
    }
    return param;
  }

  private void findParams(List params) {
    if (this instanceof PatternRoute) {
      Collections.addAll(params, ((PatternRoute)this).params);
    }
  }

  private void findAncestorOrSelfParams(List params) {
    findParams(params);
    if (parent != null) {
      parent.findAncestorOrSelfParams(params);
    }
  }

  /**
   * Find the params having the specified name among this route or its descendants.
   *
   * @param name   the name
   * @param params the list collecting the found params
   */
  private void findDescendantOrSelfParams(String name, List params) {
    PathParam param = getParam(name);
    if (param != null) {
      params.add(param);
    }
  }







  public Route append(String path) {
    return append(path, RouteKind.MATCH);
  }

  public Route append(String path, final RouteKind kind) {
    return append(path, kind, Collections.emptyMap());
  }

  public Route append(String path, Map params) {
    return append(path, RouteKind.MATCH, params);
  }

  static class Data {

    PatternBuilder builder = new PatternBuilder().expr("");
    List chunks = new ArrayList();
    List parameterPatterns = new ArrayList();
    String paramName = null;
    boolean lastSegment = false;
  }

  public Route append(
      String path,
      final RouteKind kind,
      final Map params) throws NullPointerException {
    if (path == null) {
      throw new NullPointerException("No null route path accepted");
    }
    if (kind == null) {
      throw new NullPointerException("No null route kind accepted");
    }
    if (params == null) {
      throw new NullPointerException("No null route params accepted");
    }

    //
    class Assembler implements RouteParserHandler {

      LinkedList datas = new LinkedList();
      Route last = null;

      public void segmentOpen() {
        datas.add(new Data());
      }

      public void segmentChunk(CharSequence s, int from, int to) {
        datas.peekLast().builder.litteral(s, from, to);
        datas.peekLast().chunks.add(s.subSequence(from, to).toString());
        datas.peekLast().lastSegment = true;
      }

      public void segmentClose() {
        if (!datas.peekLast().lastSegment) {
          datas.peekLast().chunks.add("");
        }
      }

      public void exprOpen() {
        if (!datas.peekLast().lastSegment) {
          datas.peekLast().chunks.add("");
        }
      }

      public void exprIdent(CharSequence s, int from, int to) {
        String parameterName = s.subSequence(from, to).toString();
        datas.peekLast().paramName = parameterName;
      }

      public void exprClose() {
        datas.peekLast().lastSegment = false;

        //
        PathParam.Builder desc = params.get(datas.peekLast().paramName);
        if (desc == null) {
          desc = new PathParam.Builder();
        }

        //
        PathParam param = desc.build(router, datas.peekLast().paramName);
        // Append routing regex to the route regex surrounded by a non capturing regex
        // to isolate routingRegex like a|b or a(.)b
        datas.peekLast().builder.expr("(?:").expr(param.routingRegex).expr(")");
        datas.peekLast().parameterPatterns.add(param);
      }

      public void pathClose(boolean slash) {

        // The path was empty or /
        if (datas.isEmpty()) {
          Route.this.add(last = new EmptyRoute(router, kind.getTerminal(slash)));
        } else {
          last = Route.this;
          while (datas.size() > 0) {
            Data data = datas.removeFirst();
            int terminal = datas.isEmpty() ? kind.getTerminal(slash) : TERMINATION_NONE;
            Route next;
            if (data.parameterPatterns.isEmpty()) {
              next = new SegmentRoute(router, data.chunks.get(0), terminal);
            } else {
              // 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 empty expression
              data.builder.expr("(?:(?<=^)|(?=/)|$)");
              next = new PatternRoute(router, router.compile(data.builder.build()), data.parameterPatterns, data.chunks, terminal);
            }
            last.add(next);
            last = next;
          }
        }
      }
    }

    //
    Assembler asm = new Assembler();
    try {
      RouteParser.parse(path, asm);
    }
    catch (SyntaxException e) {
      throw new MalformedRouteException(e);
    }

    //
    return asm.last;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy