Package com.elibom.jogger.middleware.router

Source Code of com.elibom.jogger.middleware.router.RouterMiddleware

package com.elibom.jogger.middleware.router;

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

import com.elibom.jogger.Middleware;
import com.elibom.jogger.MiddlewareChain;
import com.elibom.jogger.http.Path;
import com.elibom.jogger.http.Request;
import com.elibom.jogger.http.Response;
import com.elibom.jogger.middleware.router.Route.HttpMethod;
import com.elibom.jogger.middleware.router.interceptor.Action;
import com.elibom.jogger.middleware.router.interceptor.Controller;
import com.elibom.jogger.middleware.router.interceptor.Interceptor;
import com.elibom.jogger.middleware.router.interceptor.InterceptorEntry;
import com.elibom.jogger.middleware.router.interceptor.InterceptorExecution;
import com.elibom.jogger.util.Preconditions;

/**
* This middleware routes requests through <em>interceptors</em> and <em>controller actions</em> using the <em>routes</em>
* provided by the user.
*
* @author German Escobar
*/
public class RouterMiddleware implements Middleware {
 
  /**
   * The list of routes.
   */
  private List<Route> routes = new CopyOnWriteArrayList<Route>();

  /**
   * The list of interceptors.
   */
  private List<InterceptorEntry> interceptors = new CopyOnWriteArrayList<InterceptorEntry>();
 
  @Override
  public void handle(Request request, Response response, MiddlewareChain chain) throws Exception {
    Route route = getRoute(request.getMethod(), request.getPath());
    if (route == null) {
      chain.next();
      return;
    }
   
    request.setRoute(route);
    response.status(Response.OK);
   
    // load the interceptors of the request
    List<Interceptor> requestInterceptors = getInterceptors(request.getPath());

    // execute the controller
    ControllerExecutor controllerExecutor = new ControllerExecutor(route, request, response, requestInterceptors);
    controllerExecutor.proceed();
  }
 
  /**
   * Returns a list of interceptors that match a path
   *
   * @param path
   * @return a list of {@link Interceptor} objects.
   */
  private List<Interceptor> getInterceptors(String path) {
    List<Interceptor> ret = new ArrayList<Interceptor>();

    for (InterceptorEntry entry : getInterceptors()) {
      if (matches(path, entry.getPaths())) {
        ret.add(entry.getInterceptor());
      }
    }

    return ret;
  }

  /**
   * Helper method. Checks if the <code>path</code> is in the array of <code>paths</code>.
   *
   * @param path the path we want to check.
   * @param paths the paths to match.
   *
   * @return true if the <code>path</code> matches the <code>paths</code> (at least one), false otherwise.
   */
  private boolean matches(String path, String... paths) {
    if (paths.length == 0) {
      return true;
    }

    for (String p : paths) {
      /* TODO we should use the same mechanism servlets use to match paths */
      if (p.equalsIgnoreCase(path)) {
        return true;
      }
    }

    return false;
  }
 
  /**
   * Retrieves the {@link Route} that matches the specified <code>httpMethod</code> and <code>path</code>.
   *
   * @param httpMethod the HTTP method to match. Should not be null or empty.
   * @param path the path to match. Should not be null but can be empty (which is interpreted as /)
   *
   * @return a {@link Route} object that matches the arguments or null if no route matches.
   */
  private Route getRoute(String httpMethod, String path) {
    Preconditions.notEmpty(httpMethod, "no httpMethod provided.");
    Preconditions.notNull(path, "no path provided.");

    String cleanPath = parsePath(path);

    for (Route route : routes) {
      if (matchesPath(route.getPath(), cleanPath) && route.getHttpMethod().toString().equalsIgnoreCase(httpMethod)) {
        return route;
      }
    }

    return null;
  }
 
  private String parsePath(String path) {
    path = Path.fixPath(path);

    try {
      URI uri = new URI(path);
      return uri.getPath();
    } catch (URISyntaxException e) {
      return null;
    }
  }

  /**
   * Helper method. Tells if the the HTTP path matches the route path.
   *
   * @param routePath the path defined for the route.
   * @param pathToMatch the path from the HTTP request.
   *
   * @return true if the path matches, false otherwise.
   */
  private boolean matchesPath(String routePath, String pathToMatch) {
    routePath = routePath.replaceAll(Path.VAR_REGEXP, Path.VAR_REPLACE);
    return pathToMatch.matches("(?i)" + routePath);
  }
 
  public List<Route> getRoutes() {
    return routes;
  }

  public void setRoutes(List<Route> routes) {
    Preconditions.notNull(routes, "no routes provided");
    this.routes = routes;
  }
 
  /**
   * Adds a route to the list of routes using a {@link Route} object.
   *
   * @param route the route to be added.
   */
  public void addRoute(Route route) {
    Preconditions.notNull(route, "no route provided");
    this.routes.add(route);
  }
 
  /**
   * Creates a {@link Route} object from the received arguments and adds it to the list of routes.
   *
   * @param httpMethod the HTTP method to which this route is going to respond.
   * @param path the path to which this route is going to respond.
   * @param controller the object that will be invoked when this route matches.
   * @param methodName the name of the method in the <code>controller</code> object that will be invoked when this
   * route matches.
   *
   * @throws NoSuchMethodException if the <code>methodName</code> is not found or doesn't have the right signature.
   */
  public void addRoute(HttpMethod httpMethod, String path, Object controller, String methodName) throws NoSuchMethodException {
    Preconditions.notNull(controller, "no controller provided");
    Method method = controller.getClass().getMethod(methodName, Request.class, Response.class);
    addRoute(httpMethod, path, controller, method);
  }
 
  /**
   * Creates a {@link Route} object from the received arguments and adds it to the list of routes.
   *
   * @param httpMethod the HTTP method to which this route is going to respond.
   * @param path the path to which this route is going to respond.
   * @param controller the object that will be invoked when this route matches.
   * @param method the Method that will be invoked when this route matches.
   */
  public void addRoute(HttpMethod httpMethod, String path, Object controller, Method method) {
    // validate signature
    Class<?>[] paramTypes = method.getParameterTypes();
    if (paramTypes.length != 2 || !paramTypes[0].equals(Request.class) || !paramTypes[1].equals(Response.class)) {
      throw new RoutesException("Expecting two params of type com.elibom.jogger.http.Request and com.elibom.jogger.http.Response "
          + "respectively");
    }

    method.setAccessible(true); // to access methods from anonymous classes
    routes.add(new Route(httpMethod, path, controller, method));
  }
 
  /**
   * Creates a {@link Route} object and adds it to the routes list. It will respond to the GET HTTP method and the
   * specified <code>path</code> invoking the {@link RouteHandler} object.
   *
   * @param path the path to which this route will respond.
   * @param handler the object that will be invoked when the route matches.
   */
  public void get(String path, RouteHandler handler) {
    try {
      addRoute(HttpMethod.GET, path, handler, "handle");
    } catch (NoSuchMethodException e) {
      // shouldn't happen unless we change the name of the method in RouteHandler
      throw new RuntimeException(e);
    }
  }
 
  /**
   * Creates a {@link Route} object and adds it to the routes list. It will respond to the POST HTTP method and the
   * specified <code>path</code> invoking the {@link RouteHandler} object.
   *
   * @param path the path to which this route will respond.
   * @param handler the object that will be invoked when the route matches.
   */
  public void post(String path, RouteHandler handler) {
    try {
      addRoute(HttpMethod.POST, path, handler, "handle");
    } catch (NoSuchMethodException e) {
      // shouldn't happen unless we change the name of the method in RouteHandler
      throw new RuntimeException(e);
    }
  }
 
  /**
   * Creates a {@link Route} object and adds it to the routes list. It will respond to the PUT HTTP method and the
   * specified <code>path</code> invoking the {@link RouteHandler} object.
   *
   * @param path the path to which this route will respond.
   * @param handler the object that will be invoked when the route matches.
   */
  public void put(String path, RouteHandler handler) {
    try {
      addRoute(HttpMethod.PUT, path, handler, "handle");
    } catch (NoSuchMethodException e) {
      // shouldn't happen unless we change the name of the method in RouteHandler
      throw new RuntimeException(e);
    }
  }
 
  /**
   * Creates a {@link Route} object and adds it to the routes list. It will respond to the DELETE HTTP method and the
   * specified <code>path</code> invoking the {@link RouteHandler} object.
   *
   * @param path the path to which this route will respond.
   * @param handler the object that will be invoked when the route matches.
   */
  public void delete(String path, RouteHandler handler) {
    try {
      addRoute(HttpMethod.DELETE, path, handler, "handle");
    } catch (NoSuchMethodException e) {
      // shouldn't happen unless we change the name of the method in RouteHandler
      throw new RuntimeException(e);
    }
  }
 
  public List<InterceptorEntry> getInterceptors() {
    return interceptors;
  }

  public void setInterceptors(List<InterceptorEntry> interceptors) {
    Preconditions.notNull(interceptors, "no interceptors provided");
    this.interceptors = interceptors;
  }

  /**
   * Adds the <code>interceptor</code> to the list of interceptors that will matches the specified
   * <code>paths</code>.
   *
   * @param interceptor the interceptor object to be added.
   * @param paths the paths in which this interceptor will be invoked, an empty array to respond to all paths.
   */
  public void addInterceptor(Interceptor interceptor, String... paths) {
    Preconditions.notNull(interceptor, "no interceptor provided");
    interceptors.add(new InterceptorEntry(interceptor, paths));
  }
 
  /**
   * This is a helper class that executes the interceptor chain and calls the controller. Notice that this class uses
   * recursion to call each interceptor and the controller. It uses an index to keep track of the next interceptor to be
   * executed. Finally, it calls the controller.
   *
   * @author German Escobar
   */
  private class ControllerExecutor implements InterceptorExecution {

    private Route route;

    private Request request;

    private Response response;

    private List<Interceptor> interceptors;

    private int index = 0;

    /**
     * Constructor. Initializes the object with the specified parameters.
     *
     * @param route
     * @param request the Jogger HTTP request.
     * @param response the Jogger HTTP response.
     * @param interceptors a list of interceptors that we need to execute before calling the action.
     */
    public ControllerExecutor(Route route, Request request, Response response, List<Interceptor> interceptors) {
      this.route = route;
      this.request = request;
      this.response = response;
      this.interceptors = interceptors;
    }

    @Override
    public void proceed() throws Exception {
      // if we finished executing all the interceptors, call the controller method
      if (index == interceptors.size()) {

        Object controller = route.getController();
        Method method = route.getAction();

        try {
          method.invoke(controller, request, response);
        } catch (InvocationTargetException e) {
          throw (Exception) e.getCause();
        }

        return;
      }

      // retrieve the interceptor and increase the index
      Interceptor interceptor = interceptors.get(index);
      index++;

      // execute the interceptor - notice that the interceptor can eventually call the proceed() method recursively.
      interceptor.intercept(request, response, this);
    }

    @Override
    public Controller getController() {
      // create and return a new instance of the Controller class
      return new Controller() {
        public <A extends Annotation> A getAnnotation(Class<A> annotation) {
          return route.getController().getClass().getAnnotation(annotation);
        }
      };
    }

    @Override
    public Action getAction() {
      // create and return a new instance of the Action class
      return new Action() {
        public <A extends Annotation> A getAnnotation(Class<A> annotationClass) {
          return findAnnotation(route.getAction(), annotationClass);
        }

        /**
         * Helper method. Tries to find the annotation in the method or its super methods. Annotations on
         * methods are not inherited by default, so we need to handle this explicitly.
         *
         * @param method the method from which we want to find the annotation.
         * @param annotationClass the class of the annotation we are searching for.
         *
         * @return the annotation or null if not found.
         */
        private <A extends Annotation> A findAnnotation(Method method, Class<A> annotationClass) {
          A annotation = method.getAnnotation(annotationClass);
          if (annotation != null) {
            return annotation;
          }

          Method superMethod = getSuperMethod(method);
          if (superMethod != null) {
            return findAnnotation(superMethod, annotationClass);
          }

          return null;
        }

        private Method getSuperMethod(Method method) {
          Class<?> superClass = method.getDeclaringClass().getSuperclass();
          try {
            return superClass.getDeclaredMethod(method.getName(), method.getParameterTypes());
          } catch (NoSuchMethodException e) {
            return null;
          }
        }
      };
    }
  }
}
TOP

Related Classes of com.elibom.jogger.middleware.router.RouterMiddleware

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.