private void addRoutes(Object handler) {
    Class clazz = handler.getClass();
    for (Method method : clazz.getMethods()) {
      int modifier = method.getModifiers();
      if (!(Modifier.isPublic(modifier))) continue;

      Class<?>[] paramTypes = method.getParameterTypes();
      if (paramTypes.length != 2
          || !paramTypes[0].equals(Request.class)
          || !paramTypes[1].equals(Response.class)) continue;

      Before beforeAnn = method.getAnnotation(Before.class);
      After afterAnn = method.getAnnotation(After.class);
      Route routeAnn = method.getAnnotation(Route.class);

      if (routeAnn != null) routes.add(new MatchEntity(handler, method, routeAnn));
      else if (beforeAnn != null) before.add(new MatchEntity(handler, method, beforeAnn));
      else if (afterAnn != null) after.add(new MatchEntity(handler, method, afterAnn));
    }

    if (L.isInfoEnabled()) {
      for (MatchEntity e : routes) L.info(null, "route " + ObjectUtils.toString(e));
      for (MatchEntity e : before) L.info(null, "before " + ObjectUtils.toString(e));
      for (MatchEntity e : after) L.info(null, "after " + ObjectUtils.toString(e));
    }
  }
 protected void fireAfterAll(final Request req, final Response resp) {
   try {
     fireAccessHook(
         new AccessHookCallback() {
           @Override
           public void callHook(AccessHook hook) {
             hook.afterAll(req, resp);
           }
         });
   } catch (Exception e) {
     L.warn(null, e, "fireAfterAll error");
   }
 }
 protected void fireExceptionCaught(final Request req, final Response resp, final Throwable t) {
   try {
     fireAccessHook(
         new AccessHookCallback() {
           @Override
           public void callHook(AccessHook hook) {
             hook.exceptionCaught(req, resp, t);
           }
         });
   } catch (Exception e) {
     L.warn(null, e, "fireExceptionCaught error");
   }
 }
  protected void process(HttpServletRequest hreq, HttpServletResponse hresp)
      throws ServletException, IOException {
    Request req = createRequest(hreq, hresp);
    if (L.isDebugEnabled()) L.debug(null, req.toString());

    Response resp = createResponse(hreq, hresp);
    try {
      fireBeforeAll(req, resp);
      if (routeSummary
          && "$"
              .equals(StringUtils.removeStart(StringUtils.trimToEmpty(hreq.getPathInfo()), "/"))) {
        resp.body(RawText.of(makeRouteSummary()));
        resp.type("text/plain");
      } else {
        invokeAllMatched(before, req, resp);
        boolean matched = invokeFirstMatched(routes, req, resp);
        invokeAllMatched(after, req, resp);
        if (!matched) TopazHelper.halt(404, "Route error");

        if (L.isDebugEnabled()) L.debug(null, "ok");
      }
      fireSuccess(req, resp);
    } catch (Throwable t) {
      fireExceptionCaught(req, resp, t);
      Throwable c =
          (t instanceof InvocationTargetException)
              ? ((InvocationTargetException) t).getTargetException()
              : t;
      if (!(c instanceof QuietHaltException)) resp.error(c);

      L.warn(null, c, "error");
    } finally {
      fireBeforeOutput(req, resp);
      resp.doOutput(Response.OutputOptions.fromRequest(req));
      fireAfterOutput(req, resp);
      req.deleteUploadedFiles();
      fireAfterAll(req, resp);
      if (L.isDebugEnabled()) L.debug(null, "complete");
    }
  }
public class TopazServlet extends HttpServlet implements Initializable {
  private static final Logger L = Logger.get(TopazServlet.class);

  private final List<MatchEntity> before = new ArrayList<MatchEntity>();
  private final List<MatchEntity> after = new ArrayList<MatchEntity>();
  private final List<MatchEntity> routes = new ArrayList<MatchEntity>();

  private List<Object> handlers;

  protected boolean errorDetail = false;
  protected boolean outputCompress = false;
  protected boolean routeSummary = false;
  protected List<AccessHook> accessHooks;

  protected TopazServlet() {}

  private void addRoutes(Object handler) {
    Class clazz = handler.getClass();
    for (Method method : clazz.getMethods()) {
      int modifier = method.getModifiers();
      if (!(Modifier.isPublic(modifier))) continue;

      Class<?>[] paramTypes = method.getParameterTypes();
      if (paramTypes.length != 2
          || !paramTypes[0].equals(Request.class)
          || !paramTypes[1].equals(Response.class)) continue;

      Before beforeAnn = method.getAnnotation(Before.class);
      After afterAnn = method.getAnnotation(After.class);
      Route routeAnn = method.getAnnotation(Route.class);

      if (routeAnn != null) routes.add(new MatchEntity(handler, method, routeAnn));
      else if (beforeAnn != null) before.add(new MatchEntity(handler, method, beforeAnn));
      else if (afterAnn != null) after.add(new MatchEntity(handler, method, afterAnn));
    }

    if (L.isInfoEnabled()) {
      for (MatchEntity e : routes) L.info(null, "route " + ObjectUtils.toString(e));
      for (MatchEntity e : before) L.info(null, "before " + ObjectUtils.toString(e));
      for (MatchEntity e : after) L.info(null, "after " + ObjectUtils.toString(e));
    }
  }

  public List<Object> getHandlers() {
    return handlers;
  }

  public void setHandlers(List<Object> handlers) {
    this.handlers = handlers;
  }

  public List<AccessHook> getAccessHooks() {
    return accessHooks;
  }

  public void setAccessHooks(List<AccessHook> accessHooks) {
    this.accessHooks = accessHooks;
  }

  public boolean isOutputCompress() {
    return outputCompress;
  }

  public void setOutputCompress(boolean outputCompress) {
    this.outputCompress = outputCompress;
  }

  public boolean isErrorDetail() {
    return errorDetail;
  }

  public void setErrorDetail(boolean errorDetail) {
    this.errorDetail = errorDetail;
  }

  public boolean isRouteSummary() {
    return routeSummary;
  }

  public void setRouteSummary(boolean routeSummary) {
    this.routeSummary = routeSummary;
  }

  @Override
  public void init() throws ServletException {
    super.init();
    if (CollectionUtils.isEmpty(handlers)) return;

    for (Object handler : handlers) addRoutes(handler);
  }

  @Override
  protected void doTrace(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    process(req, resp);
  }

  @Override
  protected void doOptions(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    process(req, resp);
  }

  @Override
  protected void doDelete(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    process(req, resp);
  }

  @Override
  protected void doPut(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    process(req, resp);
  }

  @Override
  protected void doPost(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    process(req, resp);
  }

  @Override
  protected void doHead(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    process(req, resp);
  }

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    process(req, resp);
  }

  protected Request createRequest(HttpServletRequest hreq, HttpServletResponse hresp) {
    return new Request(hreq);
  }

  protected Response createResponse(HttpServletRequest hreq, HttpServletResponse hresp) {
    Response resp = new Response(hresp);
    resp.setErrorDetail(errorDetail);
    resp.setGzip(outputCompress);
    return resp;
  }

  private static interface AccessHookCallback {
    void callHook(AccessHook hook);
  }

  protected void fireAccessHook(AccessHookCallback callback) {
    List<AccessHook> hooks = accessHooks;
    if (CollectionUtils.isEmpty(hooks)) return;

    for (AccessHook hook : hooks) {
      if (hook != null) callback.callHook(hook);
    }
  }

  protected void fireBeforeAll(final Request req, final Response resp) {
    fireAccessHook(
        new AccessHookCallback() {
          @Override
          public void callHook(AccessHook hook) {
            hook.beforeAll(req, resp);
          }
        });
  }

  protected void fireExceptionCaught(final Request req, final Response resp, final Throwable t) {
    try {
      fireAccessHook(
          new AccessHookCallback() {
            @Override
            public void callHook(AccessHook hook) {
              hook.exceptionCaught(req, resp, t);
            }
          });
    } catch (Exception e) {
      L.warn(null, e, "fireExceptionCaught error");
    }
  }

  protected void fireSuccess(final Request req, final Response resp) {
    fireAccessHook(
        new AccessHookCallback() {
          @Override
          public void callHook(AccessHook hook) {
            hook.success(req, resp);
          }
        });
  }

  protected void fireBeforeOutput(final Request req, final Response resp) {
    try {
      fireAccessHook(
          new AccessHookCallback() {
            @Override
            public void callHook(AccessHook hook) {
              hook.beforeOutput(req, resp);
            }
          });
    } catch (Exception e) {
      L.warn(null, e, "fireBeforeOutput error");
    }
  }

  protected void fireAfterOutput(final Request req, final Response resp) {
    try {
      fireAccessHook(
          new AccessHookCallback() {
            @Override
            public void callHook(AccessHook hook) {
              hook.afterOutput(req, resp);
            }
          });
    } catch (Exception e) {
      L.warn(null, e, "fireAfterOutput error");
    }
  }

  protected void fireAfterAll(final Request req, final Response resp) {
    try {
      fireAccessHook(
          new AccessHookCallback() {
            @Override
            public void callHook(AccessHook hook) {
              hook.afterAll(req, resp);
            }
          });
    } catch (Exception e) {
      L.warn(null, e, "fireAfterAll error");
    }
  }

  protected void process(HttpServletRequest hreq, HttpServletResponse hresp)
      throws ServletException, IOException {
    Request req = createRequest(hreq, hresp);
    if (L.isDebugEnabled()) L.debug(null, req.toString());

    Response resp = createResponse(hreq, hresp);
    try {
      fireBeforeAll(req, resp);
      if (routeSummary
          && "$"
              .equals(StringUtils.removeStart(StringUtils.trimToEmpty(hreq.getPathInfo()), "/"))) {
        resp.body(RawText.of(makeRouteSummary()));
        resp.type("text/plain");
      } else {
        invokeAllMatched(before, req, resp);
        boolean matched = invokeFirstMatched(routes, req, resp);
        invokeAllMatched(after, req, resp);
        if (!matched) TopazHelper.halt(404, "Route error");

        if (L.isDebugEnabled()) L.debug(null, "ok");
      }
      fireSuccess(req, resp);
    } catch (Throwable t) {
      fireExceptionCaught(req, resp, t);
      Throwable c =
          (t instanceof InvocationTargetException)
              ? ((InvocationTargetException) t).getTargetException()
              : t;
      if (!(c instanceof QuietHaltException)) resp.error(c);

      L.warn(null, c, "error");
    } finally {
      fireBeforeOutput(req, resp);
      resp.doOutput(Response.OutputOptions.fromRequest(req));
      fireAfterOutput(req, resp);
      req.deleteUploadedFiles();
      fireAfterAll(req, resp);
      if (L.isDebugEnabled()) L.debug(null, "complete");
    }
  }

  private String makeRouteSummary() {
    StringBuilder buff = new StringBuilder();
    for (MatchEntity route : routes) {
      buff.append(route.toRouteString()).append("\n");
    }
    return buff.toString();
  }

  private static boolean invokeFirstMatched(
      List<MatchEntity> matchEntities, Request req, Response resp)
      throws InvocationTargetException, IllegalAccessException {
    for (MatchEntity matchEntity : matchEntities) {
      if (matchEntity == null) continue;

      if (matchEntity.match(req)) {
        matchEntity.invoke(req, resp);
        return true;
      }
    }
    return false;
  }

  private static boolean invokeAllMatched(
      List<MatchEntity> matchEntities, Request req, Response resp)
      throws InvocationTargetException, IllegalAccessException {
    boolean matched = false;
    for (MatchEntity matchEntity : matchEntities) {
      if (matchEntity == null) continue;

      if (matchEntity.match(req)) {
        matchEntity.invoke(req, resp);
        matched = true;
      }
    }
    return matched;
  }

  private static class MatchEntity {
    final Object instance;
    final Method method;
    final String[] urlPatterns;
    final String[] httpMethods;

    private MatchEntity(
        Object instance, Method method, String[] urlPatterns, String[] httpMethods) {
      this.instance = instance;
      this.method = method;
      this.urlPatterns = urlPatterns;
      this.httpMethods = httpMethods;
    }

    private MatchEntity(Object instance, Method method, Route route) {
      this(instance, method, route.url(), route.method());
    }

    private MatchEntity(Object instance, Method method, Before before) {
      this(instance, method, before.url(), before.method());
    }

    private MatchEntity(Object instance, Method method, After after) {
      this(instance, method, after.url(), after.method());
    }

    private static boolean match(String patt, String s, Request req) {
      HashMap<String, String> m = new HashMap<String, String>();
      if (!UrlMatcher.match(patt, s, m)) return false;

      for (Map.Entry<String, String> e : m.entrySet()) req.set(e.getKey(), e.getValue());

      return true;
    }

    public boolean match(Request req) {
      HttpServletRequest hreq = req.httpRequest;
      boolean matched = false;

      // HTTP method
      String reqMethod = req.httpRequest.getMethod();
      for (String httpMethod : httpMethods) {
        if (StringUtils.equalsIgnoreCase(httpMethod, reqMethod)) {
          matched = true;
          break;
        }
      }
      if (!matched) return false;

      // URL
      matched = false;
      // String url = StringHelper.joinIgnoreNull(hreq.getServletPath(), hreq.getPathInfo());
      String url = StringUtils.trimToEmpty(hreq.getPathInfo());
      if (!url.startsWith("/")) url = "/" + url;

      for (String urlPatt : urlPatterns) {
        if (match(urlPatt, url, req)) {
          matched = true;
          break;
        }
      }

      return matched;
    }

    public void invoke(Request req, Response resp)
        throws InvocationTargetException, IllegalAccessException {
      method.invoke(instance, req, resp);
    }

    @Override
    public String toString() {
      StringBuilder buff = new StringBuilder();
      buff.append(StringUtils.join(httpMethods, "|"));
      buff.append("  ").append(StringUtils.join(urlPatterns, "|"));
      buff.append(" => ");
      buff.append(instance.getClass().getName()).append(".").append(method.getName());
      return buff.toString();
    }

    public String toRouteString() {
      StringBuilder buff = new StringBuilder();
      buff.append(StringUtils.join(httpMethods, "|"));
      buff.append("  ").append(StringUtils.join(urlPatterns, "|"));
      return buff.toString();
    }
  }
}