content: /*
   * Copyright 2015 The Netty Project
   *
   * The Netty Project licenses this file to you 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 com.redant.core.router;
  
  import io.netty.handler.codec.http.HttpMethod;
  import io.netty.handler.codec.http.QueryStringDecoder;
  
  import java.util.ArrayList;
  import java.util.Collection;
  import java.util.Collections;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.List;
  import java.util.Map;
  import java.util.Map.Entry;
  import java.util.Set;
  
  /**
   * Router that contains information about both route matching orders and
   * HTTP request methods.
   *
   * <p>Routes are devided into 3 sections: "first", "last", and "other".
   * Routes in "first" are matched first, then in "other", then in "last".
   *
   * <h3>Create router</h3>
   *
   * <p>Route targets can be any type. In the below example, targets are classes:
   *
   * <pre>
   * {@code
   * Router<Class> router = new Router<Class>()
   *   .GET      ("/articles",     IndexHandler.class)
   *   .GET      ("/articles/:id", ShowHandler.class)
   *   .POST     ("/articles",     CreateHandler.class)
   *   .GET      ("/download/:*",  DownloadHandler.class)  // ":*" must be the last token
   *   .GET_FIRST("/articles/new", NewHandler.class);      // This will be matched first
   * }
   * </pre>
   *
   * <p>Slashes at both ends are ignored. These are the same:
   *
   * <pre>
   * {@code
   * router.GET("articles",   IndexHandler.class);
   * router.GET("/articles",  IndexHandler.class);
   * router.GET("/articles/", IndexHandler.class);
   * }
   * </pre>
   *
   * <p>You can remove routes by target or by path pattern:
   *
   * <pre>
   * {@code
   * router.removeTarget(IndexHandler.class);
   * router.removePathPattern("/articles");
   * }
   * </pre>
   *
   * <h3>Match with request method and URI</h3>
   *
   * <p>Use {@link #route(HttpMethod, String)}.
   *
   * <p>From the {@link RouteResult} you can extract params embedded in
   * the path and from the query part of the request URI.
   *
   * <h3>404 Not Found target</h3>
   *
   * <p>Use {@link #notFound(Object)}. It will be used as the target
   * when there's no match.
   *
   * <pre>
   * {@code
   * router.notFound(My404Handler.class);
   * }
   * </pre>
   *
   * <h3>Create reverse route</h3>
   *
   * <p>Use {@code #uri}:
   *
   * <pre>
   * {@code
   * router.uri(HttpMethod.GET, IndexHandler.class);
   * // Returns "/articles"
   * }
   * </pre>
   *
   * <p>You can skip HTTP method if there's no confusion:
   *
   * <pre>
   * {@code
   * router.uri(CreateHandler.class);
   * // Also returns "/articles"
   * }
   * </pre>
   *
   * <p>You can specify params as map:
   *
   * <pre>
   * {@code
   * // Things in params will be converted to String
   * Map<Object, Object> params = new HashMap<Object, Object>();
   * params.put("id", 123);
   * router.uri(ShowHandler.class, params);
   * // Returns "/articles/123"
   * }
   * </pre>
   *
   * <p>Convenient way to specify params:
   *
   * <pre>
   * {@code
   * router.uri(ShowHandler.class, "id", 123);
   * // Returns "/articles/123"
   * }
   * </pre>
   *
   * <h3>Allowed methods</h3>
   *
   * <p>If you want to implement
   * <a href="https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Request_methods">OPTIONS</a>
   * or
   * <a href="https://en.wikipedia.org/wiki/Cross-origin_resource_sharing">CORS</a>,
   * you can use {@link #allowedMethods(String)}.
   *
   * <p>For {@code OPTIONS *}, use {@link #allAllowedMethods()}.
   */
  public class Router<T> {
      private final Map<HttpMethod, MethodlessRouter<T>> routers =
              new HashMap<HttpMethod, MethodlessRouter<T>>();
  
      private final MethodlessRouter<T> anyMethodRouter =
              new MethodlessRouter<T>();
  
      private T notFound;
  
      //--------------------------------------------------------------------------
      // Design decision:
      // We do not allow access to routers and anyMethodRouter, because we don't
      // want to expose MethodlessRouter, OrderlessRouter, and PathPattern.
      // Exposing those will complicate the use of this package.
  
      /**
       * Returns the fallback target for use when there's no match at
       * {@link #route(HttpMethod, String)}.
       */
      public T notFound() {
          return notFound;
      }
  
      /**
       * Returns the number of routes in this router.
       */
      public int size() {
          int ret = anyMethodRouter.size();
  
          for (MethodlessRouter<T> router : routers.values()) {
              ret += router.size();
          }
  
          return ret;
      }
  
      //--------------------------------------------------------------------------
  
      /**
       * Add route to the "first" section.
       *
       * <p>A path pattern can only point to one target. This method does nothing if the pattern
       * has already been added.
       */
      public Router<T> addRouteFirst(HttpMethod method, String pathPattern, T target) {
          getMethodlessRouter(method).addRouteFirst(pathPattern, target);
          return this;
      }
  
      /**
       * Add route to the "other" section.
       *
       * <p>A path pattern can only point to one target. This method does nothing if the pattern
       * has already been added.
       */
      public Router<T> addRoute(HttpMethod method, String pathPattern, T target) {
          getMethodlessRouter(method).addRoute(pathPattern, target);
          return this;
      }
  
      /**
       * Add route to the "last" section.
       *
       * <p>A path pattern can only point to one target. This method does nothing if the pattern
       * has already been added.
       */
      public Router<T> addRouteLast(HttpMethod method, String pathPattern, T target) {
          getMethodlessRouter(method).addRouteLast(pathPattern, target);
          return this;
      }
  
      /**
       * Sets the fallback target for use when there's no match at
       * {@link #route(HttpMethod, String)}.
       */
      public Router<T> notFound(T target) {
          this.notFound = target;
          return this;
      }
  
      private MethodlessRouter<T> getMethodlessRouter(HttpMethod method) {
          if (method == null) {
              return anyMethodRouter;
          }
  
          MethodlessRouter<T> r = routers.get(method);
          if (r == null) {
              r = new MethodlessRouter<T>();
              routers.put(method, r);
          }
  
          return r;
      }
  
      //--------------------------------------------------------------------------
  
      /**
       * Removes the route specified by the path pattern.
       */
      public void removePathPattern(String pathPattern) {
          for (MethodlessRouter<T> r : routers.values()) {
              r.removePathPattern(pathPattern);
          }
          anyMethodRouter.removePathPattern(pathPattern);
      }
  
      /**
       * Removes all routes leading to the target.
       */
      public void removeTarget(T target) {
          for (MethodlessRouter<T> r : routers.values()) {
              r.removeTarget(target);
          }
          anyMethodRouter.removeTarget(target);
      }
  
      //--------------------------------------------------------------------------
  
      /**
       * If there's no match, returns the result with {@link #notFound(Object) notFound}
       * as the target if it is set, otherwise returns {@code null}.
       */
      public RouteResult<T> route(HttpMethod method, String uri) {
          MethodlessRouter<T> router = routers.get(method);
          if (router == null) {
              router = anyMethodRouter;
          }
  
          QueryStringDecoder decoder = new QueryStringDecoder(uri);
          String[] tokens = decodePathTokens(uri);
  
          RouteResult<T> ret = router.route(uri, decoder.path(), tokens);
          if (ret != null) {
              return new RouteResult<T>(uri, decoder.path(), ret.pathParams(), decoder.parameters(), ret.target());
          }
  
          if (router != anyMethodRouter) {
              ret = anyMethodRouter.route(uri, decoder.path(), tokens);
              if (ret != null) {
                  return new RouteResult<T>(uri, decoder.path(), ret.pathParams(), decoder.parameters(), ret.target());
              }
          }
  
          if (notFound != null) {
              return new RouteResult<T>(uri, decoder.path(), Collections.<String, String>emptyMap(), decoder.parameters(), notFound);
          }
  
          return null;
      }
  
      private String[] decodePathTokens(String uri) {
          // Need to split the original URI (instead of QueryStringDecoder#path) then decode the tokens (components),
          // otherwise /test1/123%2F456 will not match /test1/:p1
  
          int qPos = uri.indexOf("?");
          String encodedPath = (qPos >= 0) ? uri.substring(0, qPos) : uri;
  
          String[] encodedTokens = PathPattern.removeSlashesAtBothEnds(encodedPath).split("/");
  
          String[] decodedTokens = new String[encodedTokens.length];
          for (int i = 0; i < encodedTokens.length; i++) {
              String encodedToken = encodedTokens[i];
              decodedTokens[i] = QueryStringDecoder.decodeComponent(encodedToken);
          }
  
          return decodedTokens;
      }
  
      //--------------------------------------------------------------------------
      // For implementing OPTIONS and CORS.
  
      /**
       * Returns allowed methods for a specific URI.
       * <p>
       * For {@code OPTIONS *}, use {@link #allAllowedMethods()} instead of this method.
       */
      public Set<HttpMethod> allowedMethods(String uri) {
          QueryStringDecoder decoder = new QueryStringDecoder(uri);
          String[] tokens = PathPattern.removeSlashesAtBothEnds(decoder.path()).split("/");
  
          if (anyMethodRouter.anyMatched(tokens)) {
              return allAllowedMethods();
          }
  
          Set<HttpMethod> ret = new HashSet<HttpMethod>(routers.size());
          for (Entry<HttpMethod, MethodlessRouter<T>> entry : routers.entrySet()) {
              MethodlessRouter<T> router = entry.getValue();
              if (router.anyMatched(tokens)) {
                  HttpMethod method = entry.getKey();
                  ret.add(method);
              }
          }
  
          return ret;
      }
  
      /**
       * Returns all methods that this router handles. For {@code OPTIONS *}.
       */
      public Set<HttpMethod> allAllowedMethods() {
          if (anyMethodRouter.size() > 0) {
              Set<HttpMethod> ret = new HashSet<HttpMethod>(9);
              ret.add(HttpMethod.CONNECT);
              ret.add(HttpMethod.DELETE);
              ret.add(HttpMethod.GET);
              ret.add(HttpMethod.HEAD);
              ret.add(HttpMethod.OPTIONS);
              ret.add(HttpMethod.PATCH);
              ret.add(HttpMethod.POST);
              ret.add(HttpMethod.PUT);
              ret.add(HttpMethod.TRACE);
              return ret;
          } else {
              return new HashSet<HttpMethod>(routers.keySet());
          }
      }
  
      //--------------------------------------------------------------------------
      // Reverse routing.
  
      /**
       * Given a target and params, this method tries to do the reverse routing
       * and returns the URI.
       *
       * <p>Placeholders in the path pattern will be filled with the params.
       * The params can be a map of {@code placeholder name -> value}
       * or ordered values.
       *
       * <p>If a param doesn't have a corresponding placeholder, it will be put
       * to the query part of the result URI.
       *
       * @return {@code null} if there's no match
       */
      public String uri(HttpMethod method, T target, Object... params) {
          MethodlessRouter<T> router = (method == null) ? anyMethodRouter : routers.get(method);
  
          // Fallback to anyMethodRouter if no router is found for the method
          if (router == null) {
              router = anyMethodRouter;
          }
  
          String ret = router.uri(target, params);
          if (ret != null) {
              return ret;
          }
  
          // Fallback to anyMethodRouter if the router was not anyMethodRouter and no path is found
          return (router != anyMethodRouter) ? anyMethodRouter.uri(target, params) : null;
      }
  
      /**
       * Given a target and params, this method tries to do the reverse routing
       * and returns the URI.
       *
       * <p>Placeholders in the path pattern will be filled with the params.
       * The params can be a map of {@code placeholder name -> value}
       * or ordered values.
       *
       * <p>If a param doesn't have a corresponding placeholder, it will be put
       * to the query part of the result URI.
       *
       * @return {@code null} if there's no match
       */
      public String uri(T target, Object... params) {
          Collection<MethodlessRouter<T>> rs = routers.values();
          for (MethodlessRouter<T> r : rs) {
              String ret = r.uri(target, params);
              if (ret != null) {
                  return ret;
              }
          }
          return anyMethodRouter.uri(target, params);
      }
  
      //--------------------------------------------------------------------------
  
      /**
       * Returns visualized routing rules.
       */
      @Override
      public String toString() {
          // Step 1/2: Dump routers and anyMethodRouter in order
          int numRoutes = size();
          List<String> methods = new ArrayList<String>(numRoutes);
          List<String> patterns = new ArrayList<String>(numRoutes);
          List<String> targets = new ArrayList<String>(numRoutes);
  
          // For router
          for (Entry<HttpMethod, MethodlessRouter<T>> e : routers.entrySet()) {
              HttpMethod method = e.getKey();
              MethodlessRouter<T> router = e.getValue();
              aggregateRoutes(method.toString(), router.first().routes(), methods, patterns, targets);
              aggregateRoutes(method.toString(), router.other().routes(), methods, patterns, targets);
              aggregateRoutes(method.toString(), router.last().routes(), methods, patterns, targets);
          }
  
          // For anyMethodRouter
          aggregateRoutes("*", anyMethodRouter.first().routes(), methods, patterns, targets);
          aggregateRoutes("*", anyMethodRouter.other().routes(), methods, patterns, targets);
          aggregateRoutes("*", anyMethodRouter.last().routes(), methods, patterns, targets);
  
          // For notFound
          if (notFound != null) {
              methods.add("*");
              patterns.add("*");
              targets.add(targetToString(notFound));
          }
  
          // Step 2/2: Format the List into aligned columns: <method> <patterns> <target>
          int maxLengthMethod = maxLength(methods);
          int maxLengthPattern = maxLength(patterns);
          String format = "%-" + maxLengthMethod + "s  %-" + maxLengthPattern + "s  %s\n";
          int initialCapacity = (maxLengthMethod + 1 + maxLengthPattern + 1 + 20) * methods.size();
          StringBuilder b = new StringBuilder(initialCapacity);
          for (int i = 0; i < methods.size(); i++) {
              String method = methods.get(i);
              String pattern = patterns.get(i);
              String target = targets.get(i);
              b.append(String.format(format, method, pattern, target));
          }
          return b.toString();
      }
  
      /**
       * Helper for toString.
       */
      private static <T> void aggregateRoutes(
              String method, Map<PathPattern, T> routes,
              List<String> accMethods, List<String> accPatterns, List<String> accTargets) {
          for (Entry<PathPattern, T> entry : routes.entrySet()) {
              accMethods.add(method);
              accPatterns.add("/" + entry.getKey().pattern());
              accTargets.add(targetToString(entry.getValue()));
          }
      }
  
      /**
       * Helper for toString.
       */
      private static int maxLength(List<String> coll) {
          int max = 0;
          for (String e : coll) {
              int length = e.length();
              if (length > max) {
                  max = length;
              }
          }
          return max;
      }
  
      /**
       * Helper for toString.
       *
       * <p>For example, returns
       * "io.server.example.http.router.HttpRouterServerHandler" instead of
       * "class io.server.example.http.router.HttpRouterServerHandler"
       */
      private static String targetToString(Object target) {
          if (target instanceof Class) {
              return ((Class<?>) target).getName();
          } else {
              return target.toString();
          }
      }
  
      //--------------------------------------------------------------------------
  
      public Router<T> CONNECT(String path, T target) {
          return addRoute(HttpMethod.CONNECT, path, target);
      }
  
      public Router<T> DELETE(String path, T target) {
          return addRoute(HttpMethod.DELETE, path, target);
      }
  
      public Router<T> GET(String path, T target) {
          return addRoute(HttpMethod.GET, path, target);
      }
  
      public Router<T> HEAD(String path, T target) {
          return addRoute(HttpMethod.HEAD, path, target);
      }
  
      public Router<T> OPTIONS(String path, T target) {
          return addRoute(HttpMethod.OPTIONS, path, target);
      }
  
      public Router<T> PATCH(String path, T target) {
          return addRoute(HttpMethod.PATCH, path, target);
      }
  
      public Router<T> POST(String path, T target) {
          return addRoute(HttpMethod.POST, path, target);
      }
  
      public Router<T> PUT(String path, T target) {
          return addRoute(HttpMethod.PUT, path, target);
      }
  
      public Router<T> TRACE(String path, T target) {
          return addRoute(HttpMethod.TRACE, path, target);
      }
  
      public Router<T> ANY(String path, T target) {
          return addRoute(null, path, target);
      }
  
      //--------------------------------------------------------------------------
  
      public Router<T> CONNECT_FIRST(String path, T target) {
          return addRouteFirst(HttpMethod.CONNECT, path, target);
      }
  
      public Router<T> DELETE_FIRST(String path, T target) {
          return addRouteFirst(HttpMethod.DELETE, path, target);
      }
  
      public Router<T> GET_FIRST(String path, T target) {
          return addRouteFirst(HttpMethod.GET, path, target);
      }
  
      public Router<T> HEAD_FIRST(String path, T target) {
          return addRouteFirst(HttpMethod.HEAD, path, target);
      }
  
      public Router<T> OPTIONS_FIRST(String path, T target) {
          return addRouteFirst(HttpMethod.OPTIONS, path, target);
      }
  
      public Router<T> PATCH_FIRST(String path, T target) {
          return addRouteFirst(HttpMethod.PATCH, path, target);
      }
  
      public Router<T> POST_FIRST(String path, T target) {
          return addRouteFirst(HttpMethod.POST, path, target);
      }
  
      public Router<T> PUT_FIRST(String path, T target) {
          return addRouteFirst(HttpMethod.PUT, path, target);
      }
  
      public Router<T> TRACE_FIRST(String path, T target) {
          return addRouteFirst(HttpMethod.TRACE, path, target);
      }
  
      public Router<T> ANY_FIRST(String path, T target) {
          return addRouteFirst(null, path, target);
      }
  
      //--------------------------------------------------------------------------
  
      public Router<T> CONNECT_LAST(String path, T target) {
          return addRouteLast(HttpMethod.CONNECT, path, target);
      }
  
      public Router<T> DELETE_LAST(String path, T target) {
          return addRouteLast(HttpMethod.DELETE, path, target);
      }
  
      public Router<T> GET_LAST(String path, T target) {
          return addRouteLast(HttpMethod.GET, path, target);
      }
  
      public Router<T> HEAD_LAST(String path, T target) {
          return addRouteLast(HttpMethod.HEAD, path, target);
      }
  
      public Router<T> OPTIONS_LAST(String path, T target) {
          return addRouteLast(HttpMethod.OPTIONS, path, target);
      }
  
      public Router<T> PATCH_LAST(String path, T target) {
          return addRouteLast(HttpMethod.PATCH, path, target);
      }
  
      public Router<T> POST_LAST(String path, T target) {
          return addRouteLast(HttpMethod.POST, path, target);
      }
  
      public Router<T> PUT_LAST(String path, T target) {
          return addRouteLast(HttpMethod.PUT, path, target);
      }
  
      public Router<T> TRACE_LAST(String path, T target) {
          return addRouteLast(HttpMethod.TRACE, path, target);
      }
  
      public Router<T> ANY_LAST(String path, T target) {
          return addRouteLast(null, path, target);
      }
  }
  