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

com.jetdrone.vertx.yoke.middleware.Router Maven / Gradle / Ivy

There is a newer version: 3.0.0
Show newest version
/**
 * Copyright 2011-2014 the original author or authors.
 */
package com.jetdrone.vertx.yoke.middleware;

import com.jetdrone.vertx.yoke.Middleware;
import com.jetdrone.vertx.yoke.Yoke;
import com.jetdrone.vertx.yoke.annotations.*;
import com.jetdrone.vertx.yoke.jmx.RouteMBean;
import com.jetdrone.vertx.yoke.util.AsyncIterator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.vertx.java.core.Handler;
import org.vertx.java.core.MultiMap;

import javax.management.*;
import java.lang.management.ManagementFactory;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * # Router
 *
 * Route request by path or regular expression. All *HTTP* verbs are available:
 *
 * * `GET`
 * * `PUT`
 * * `POST`
 * * `DELETE`
 * * `OPTIONS`
 * * `HEAD`
 * * `TRACE`
 * * `CONNECT`
 * * `PATCH`
 */
public class Router extends Middleware {

    private final List getBindings = new ArrayList<>();
    private final List putBindings = new ArrayList<>();
    private final List postBindings = new ArrayList<>();
    private final List deleteBindings = new ArrayList<>();
    private final List optionsBindings = new ArrayList<>();
    private final List headBindings = new ArrayList<>();
    private final List traceBindings = new ArrayList<>();
    private final List connectBindings = new ArrayList<>();
    private final List patchBindings = new ArrayList<>();

    private final Map paramProcessors = new HashMap<>();

    /**
     * Create a new Router Middleware.
     *
     * 
     * new Router() {{
     *   get("/hello", new Handler<YokeRequest>() {
     *     public void handle(YokeRequest request) {
     *       request.response().end("Hello World!");
     *     }
     *   });
     * }}
     * 
*/ public Router() { } private void init(Yoke yoke, String mount, List bindings) { for (PatternBinding binding : bindings) { for (Middleware m : binding.middleware) { if (!m.isInitialized()) { m.init(yoke, mount); } } } } @Override public Middleware init(@NotNull final Yoke yoke, @NotNull final String mount) { super.init(yoke, mount); // since this call can happen after the bindings are in place we need to update all bindings to have a reference // to the vertx object init(yoke, mount, getBindings); init(yoke, mount, putBindings); init(yoke, mount, postBindings); init(yoke, mount, deleteBindings); init(yoke, mount, optionsBindings); init(yoke, mount, headBindings); init(yoke, mount, traceBindings); init(yoke, mount, connectBindings); for (String key : paramProcessors.keySet()) { final Middleware m = paramProcessors.get(key); if (!m.isInitialized()) { paramProcessors.get(key).init(yoke, mount); } } return this; } @Override public void handle(@NotNull final YokeRequest request, @NotNull final Handler next) { switch (request.method()) { case "GET": route(request, next, getBindings); break; case "PUT": route(request, next, putBindings); break; case "POST": route(request, next, postBindings); break; case "DELETE": route(request, next, deleteBindings); break; case "OPTIONS": route(request, next, optionsBindings); break; case "HEAD": route(request, next, headBindings); break; case "TRACE": route(request, next, traceBindings); break; case "PATCH": route(request, next, patchBindings); break; case "CONNECT": route(request, next, connectBindings); break; } } /** * Specify a middleware that will be called for a matching HTTP GET * @param pattern The simple pattern * @param handlers The middleware to call */ public Router get(@NotNull final String pattern, @NotNull final Middleware... handlers) { addPattern("GET", pattern, handlers, getBindings); return this; } /** * Specify a middleware that will be called for a matching HTTP GET * @param pattern The simple pattern * @param handler The middleware to call */ public Router get(@NotNull final String pattern, @NotNull final Handler handler) { return get(pattern, new Middleware() { @Override public void handle(YokeRequest request, Handler next) { handler.handle(request); } }); } /** * Specify a middleware that will be called for a matching HTTP PUT * @param pattern The simple pattern * @param handlers The middleware to call */ public Router put(@NotNull final String pattern, @NotNull final Middleware... handlers) { addPattern("PUT", pattern, handlers, putBindings); return this; } /** * Specify a middleware that will be called for a matching HTTP PUT * @param pattern The simple pattern * @param handler The middleware to call */ public Router put(@NotNull final String pattern, @NotNull final Handler handler) { return put(pattern, new Middleware() { @Override public void handle(YokeRequest request, Handler next) { handler.handle(request); } }); } /** * Specify a middleware that will be called for a matching HTTP POST * @param pattern The simple pattern * @param handlers The middleware to call */ public Router post(@NotNull final String pattern, @NotNull final Middleware... handlers) { addPattern("POST", pattern, handlers, postBindings); return this; } /** * Specify a middleware that will be called for a matching HTTP POST * @param pattern The simple pattern * @param handler The middleware to call */ public Router post(@NotNull final String pattern, @NotNull final Handler handler) { return post(pattern, new Middleware() { @Override public void handle(YokeRequest request, Handler next) { handler.handle(request); } }); } /** * Specify a middleware that will be called for a matching HTTP DELETE * @param pattern The simple pattern * @param handlers The middleware to call */ public Router delete(@NotNull final String pattern, @NotNull final Middleware... handlers) { addPattern("DELETE", pattern, handlers, deleteBindings); return this; } /** * Specify a middleware that will be called for a matching HTTP DELETE * @param pattern The simple pattern * @param handler The middleware to call */ public Router delete(@NotNull final String pattern, @NotNull final Handler handler) { return delete(pattern, new Middleware() { @Override public void handle(YokeRequest request, Handler next) { handler.handle(request); } }); } /** * Specify a middleware that will be called for a matching HTTP OPTIONS * @param pattern The simple pattern * @param handlers The middleware to call */ public Router options(@NotNull final String pattern, @NotNull final Middleware... handlers) { addPattern("OPTIONS", pattern, handlers, optionsBindings); return this; } /** * Specify a middleware that will be called for a matching HTTP OPTIONS * @param pattern The simple pattern * @param handler The middleware to call */ public Router options(@NotNull final String pattern, @NotNull final Handler handler) { return options(pattern, new Middleware() { @Override public void handle(YokeRequest request, Handler next) { handler.handle(request); } }); } /** * Specify a middleware that will be called for a matching HTTP HEAD * @param pattern The simple pattern * @param handlers The middleware to call */ public Router head(@NotNull final String pattern, @NotNull final Middleware... handlers) { addPattern("HEAD", pattern, handlers, headBindings); return this; } /** * Specify a middleware that will be called for a matching HTTP HEAD * @param pattern The simple pattern * @param handler The middleware to call */ public Router head(@NotNull final String pattern, @NotNull final Handler handler) { return head(pattern, new Middleware() { @Override public void handle(YokeRequest request, Handler next) { handler.handle(request); } }); } /** * Specify a middleware that will be called for a matching HTTP TRACE * @param pattern The simple pattern * @param handlers The middleware to call */ public Router trace(@NotNull final String pattern, @NotNull final Middleware... handlers) { addPattern("TRACE", pattern, handlers, traceBindings); return this; } /** * Specify a middleware that will be called for a matching HTTP TRACE * @param pattern The simple pattern * @param handler The middleware to call */ public Router trace(@NotNull final String pattern, @NotNull final Handler handler) { return trace(pattern, new Middleware() { @Override public void handle(YokeRequest request, Handler next) { handler.handle(request); } }); } /** * Specify a middleware that will be called for a matching HTTP CONNECT * @param pattern The simple pattern * @param handlers The middleware to call */ public Router connect(@NotNull final String pattern, @NotNull final Middleware... handlers) { addPattern("CONNECT", pattern, handlers, connectBindings); return this; } /** * Specify a middleware that will be called for a matching HTTP CONNECT * @param pattern The simple pattern * @param handler The middleware to call */ public Router connect(@NotNull final String pattern, @NotNull final Handler handler) { return connect(pattern, new Middleware() { @Override public void handle(YokeRequest request, Handler next) { handler.handle(request); } }); } /** * Specify a middleware that will be called for a matching HTTP PATCH * @param pattern The simple pattern * @param handlers The middleware to call */ public Router patch(@NotNull final String pattern, @NotNull final Middleware... handlers) { addPattern("PATCH", pattern, handlers, patchBindings); return this; } /** * Specify a middleware that will be called for a matching HTTP PATCH * @param pattern The simple pattern * @param handler The middleware to call */ public Router patch(@NotNull final String pattern, @NotNull final Handler handler) { return patch(pattern, new Middleware() { @Override public void handle(YokeRequest request, Handler next) { handler.handle(request); } }); } /** * Specify a middleware that will be called for all HTTP methods * @param pattern The simple pattern * @param handler The middleware to call */ public Router all(@NotNull final String pattern, @NotNull final Middleware... handler) { get(pattern, handler); put(pattern, handler); post(pattern, handler); delete(pattern, handler); options(pattern, handler); head(pattern, handler); trace(pattern, handler); connect(pattern, handler); patch(pattern, handler); return this; } /** * Specify a middleware that will be called for all HTTP methods * @param pattern The simple pattern * @param handler The middleware to call */ public Router all(@NotNull final String pattern, @NotNull final Handler handler) { get(pattern, handler); put(pattern, handler); post(pattern, handler); delete(pattern, handler); options(pattern, handler); head(pattern, handler); trace(pattern, handler); connect(pattern, handler); patch(pattern, handler); return this; } /** * Specify a middleware that will be called for a matching HTTP GET * @param regex A regular expression * @param handlers The middleware to call */ public Router get(@NotNull final Pattern regex, @NotNull final Middleware... handlers) { addRegEx("GET", regex, handlers, getBindings); return this; } /** * Specify a middleware that will be called for a matching HTTP GET * @param regex A regular expression * @param handler The middleware to call */ public Router get(@NotNull final Pattern regex, @NotNull final Handler handler) { return get(regex, new Middleware() { @Override public void handle(YokeRequest request, Handler next) { handler.handle(request); } }); } /** * Specify a middleware that will be called for a matching HTTP PUT * @param regex A regular expression * @param handlers The middleware to call */ public Router put(@NotNull final Pattern regex, @NotNull final Middleware... handlers) { addRegEx("PUT", regex, handlers, putBindings); return this; } /** * Specify a middleware that will be called for a matching HTTP PUT * @param regex A regular expression * @param handler The middleware to call */ public Router put(@NotNull final Pattern regex, @NotNull final Handler handler) { return put(regex, new Middleware() { @Override public void handle(YokeRequest request, Handler next) { handler.handle(request); } }); } /** * Specify a middleware that will be called for a matching HTTP POST * @param regex A regular expression * @param handlers The middleware to call */ public Router post(@NotNull final Pattern regex, @NotNull final Middleware... handlers) { addRegEx("POST", regex, handlers, postBindings); return this; } /** * Specify a middleware that will be called for a matching HTTP POST * @param regex A regular expression * @param handler The middleware to call */ public Router post(@NotNull final Pattern regex, @NotNull final Handler handler) { return post(regex, new Middleware() { @Override public void handle(YokeRequest request, Handler next) { handler.handle(request); } }); } /** * Specify a middleware that will be called for a matching HTTP DELETE * @param regex A regular expression * @param handlers The middleware to call */ public Router delete(@NotNull final Pattern regex, @NotNull final Middleware... handlers) { addRegEx("DELETE", regex, handlers, deleteBindings); return this; } /** * Specify a middleware that will be called for a matching HTTP DELETE * @param regex A regular expression * @param handler The middleware to call */ public Router delete(@NotNull final Pattern regex, @NotNull final Handler handler) { return delete(regex, new Middleware() { @Override public void handle(YokeRequest request, Handler next) { handler.handle(request); } }); } /** * Specify a middleware that will be called for a matching HTTP OPTIONS * @param regex A regular expression * @param handlers The middleware to call */ public Router options(@NotNull final Pattern regex, @NotNull final Middleware... handlers) { addRegEx("OPTIONS", regex, handlers, optionsBindings); return this; } /** * Specify a middleware that will be called for a matching HTTP OPTIONS * @param regex A regular expression * @param handler The middleware to call */ public Router options(@NotNull final Pattern regex, @NotNull final Handler handler) { return options(regex, new Middleware() { @Override public void handle(YokeRequest request, Handler next) { handler.handle(request); } }); } /** * Specify a middleware that will be called for a matching HTTP HEAD * @param regex A regular expression * @param handlers The middleware to call */ public Router head(@NotNull final Pattern regex, @NotNull final Middleware... handlers) { addRegEx("HEAD", regex, handlers, headBindings); return this; } /** * Specify a middleware that will be called for a matching HTTP HEAD * @param regex A regular expression * @param handler The middleware to call */ public Router head(@NotNull final Pattern regex, @NotNull final Handler handler) { return head(regex, new Middleware() { @Override public void handle(YokeRequest request, Handler next) { handler.handle(request); } }); } /** * Specify a middleware that will be called for a matching HTTP TRACE * @param regex A regular expression * @param handlers The middleware to call */ public Router trace(@NotNull final Pattern regex, @NotNull final Middleware... handlers) { addRegEx("TRACE", regex, handlers, traceBindings); return this; } /** * Specify a middleware that will be called for a matching HTTP TRACE * @param regex A regular expression * @param handler The middleware to call */ public Router trace(@NotNull final Pattern regex, @NotNull final Handler handler) { return trace(regex, new Middleware() { @Override public void handle(YokeRequest request, Handler next) { handler.handle(request); } }); } /** * Specify a middleware that will be called for a matching HTTP CONNECT * @param regex A regular expression * @param handlers The middleware to call */ public Router connect(@NotNull final Pattern regex, @NotNull final Middleware... handlers) { addRegEx("CONNECT", regex, handlers, connectBindings); return this; } /** * Specify a middleware that will be called for a matching HTTP CONNECT * @param regex A regular expression * @param handler The middleware to call */ public Router connect(@NotNull final Pattern regex, @NotNull final Handler handler) { return connect(regex, new Middleware() { @Override public void handle(YokeRequest request, Handler next) { handler.handle(request); } }); } /** * Specify a middleware that will be called for a matching HTTP PATCH * @param regex A regular expression * @param handlers The middleware to call */ public Router patch(@NotNull final Pattern regex, @NotNull final Middleware... handlers) { addRegEx("PATCH", regex, handlers, patchBindings); return this; } /** * Specify a middleware that will be called for a matching HTTP PATCH * @param regex A regular expression * @param handler The middleware to call */ public Router patch(@NotNull final Pattern regex, @NotNull final Handler handler) { return patch(regex, new Middleware() { @Override public void handle(YokeRequest request, Handler next) { handler.handle(request); } }); } /** * Specify a middleware that will be called for all HTTP methods * @param regex A regular expression * @param handler The middleware to call */ public Router all(@NotNull final Pattern regex, @NotNull final Middleware... handler) { get(regex, handler); put(regex, handler); post(regex, handler); delete(regex, handler); options(regex, handler); head(regex, handler); trace(regex, handler); connect(regex, handler); patch(regex, handler); return this; } /** * Specify a middleware that will be called for all HTTP methods * @param regex A regular expression * @param handler The middleware to call */ public Router all(@NotNull final Pattern regex, @NotNull final Handler handler) { get(regex, handler); put(regex, handler); post(regex, handler); delete(regex, handler); options(regex, handler); head(regex, handler); trace(regex, handler); connect(regex, handler); patch(regex, handler); return this; } public Router param(@NotNull final String paramName, @NotNull final Middleware handler) { // also pass the vertx object to the routes if (!handler.isInitialized() && isInitialized()) { handler.init(yoke, mount); } paramProcessors.put(paramName, handler); return this; } public Router param(@NotNull final String paramName, @NotNull final Pattern regex) { return param(paramName, new Middleware() { @Override public void handle(@NotNull final YokeRequest request, @NotNull final Handler next) { if (!regex.matcher(request.params().get(paramName)).matches()) { // Bad Request next.handle(400); return; } next.handle(null); } }); } private void addPattern(String verb, String input, Middleware[] handler, List bindings) { // We need to search for any : tokens in the String and replace them with named capture groups Matcher m = Pattern.compile(":([A-Za-z][A-Za-z0-9_]*)").matcher(input); StringBuffer sb = new StringBuffer(); Set groups = new HashSet<>(); while (m.find()) { String group = m.group().substring(1); if (groups.contains(group)) { throw new IllegalArgumentException("Cannot use identifier " + group + " more than once in pattern string"); } m.appendReplacement(sb, "(?<$1>[^\\/]+)"); groups.add(group); } m.appendTail(sb); // ignore tailing slash if not part of the input, not really REST but common on other frameworks if (sb.charAt(sb.length() - 1) != '/') { sb.append("\\/?$"); } Pattern regex = Pattern.compile(sb.toString()); boolean exists = false; // verify if the binding already exists, if yes add to it for (PatternBinding pb : bindings) { if (pb.pattern.equals(regex)) { pb.addMiddleware(handler); exists = true; break; } } if (!exists) { PatternBinding binding = new PatternBinding(verb, input, regex, groups, handler); bindings.add(binding); } // also pass the vertx object to the routes for (Middleware h : handler) { if (!h.isInitialized() && isInitialized()) { h.init(yoke, mount); } } } private void addRegEx(String verb, Pattern regex, Middleware handler[], List bindings) { boolean exists = false; // verify if the binding already exists, if yes add to it for (PatternBinding pb : bindings) { if (pb.pattern.equals(regex)) { pb.addMiddleware(handler); exists = true; break; } } if (!exists) { PatternBinding binding = new PatternBinding(verb, regex.pattern(), regex, null, handler); bindings.add(binding); } // also pass the vertx object to the routes for (Middleware h : handler) { if (!h.isInitialized() && isInitialized()) { h.init(yoke, mount); } } } private void route(final YokeRequest request, final Handler next, final List bindings) { new AsyncIterator(bindings) { @Override public void handle(PatternBinding binding) { if (hasNext()) { route(request, binding, new Handler() { @Override public void handle(Object err) { if (err == null) { next(); } else { next.handle(err); } } }); } else { // continue with yoke next.handle(null); } } }; } private void route(final YokeRequest request, final PatternBinding binding, final Handler next) { final Matcher m = binding.pattern.matcher(request.path()); if (m.matches()) { final MultiMap params = request.params(); if (binding.paramNames != null) { // Named params new AsyncIterator(binding.paramNames) { @Override public void handle(String param) { if (hasNext()) { params.set(param, m.group(param)); Middleware paramMiddleware = paramProcessors.get(param); if (paramMiddleware != null) { paramMiddleware.handle(request, new Handler() { @Override public void handle(Object err) { if (err == null) { next(); } else { next.handle(err); } } }); } else { next(); } } else { // middlewares new AsyncIterator(binding.middleware) { @Override public void handle(Middleware middleware) { if (hasNext()) { middleware.handle(request, new Handler() { @Override public void handle(Object err) { if (err == null) { next(); } else { next.handle(err); } } }); } else { next.handle(null); } } }; } } }; } else { // Un-named params for (int i = 0; i < m.groupCount(); i++) { params.set("param" + i, m.group(i + 1)); } // middlewares new AsyncIterator(binding.middleware) { @Override public void handle(Middleware middleware) { if (hasNext()) { middleware.handle(request, new Handler() { @Override public void handle(Object err) { if (err == null) { next(); } else { next.handle(err); } } }); } else { next.handle(null); } } }; } } else { next.handle(null); } } private static class PatternBinding { final Pattern pattern; final List middleware = new ArrayList<>(); final Set paramNames; private PatternBinding(@NotNull String verb, @Nullable String route, @NotNull Pattern pattern, Set paramNames, @NotNull Middleware[] middleware) { this.pattern = pattern; this.paramNames = paramNames; Collections.addAll(this.middleware, middleware); //Get the MBean server final MBeanServer mbs = ManagementFactory.getPlatformMBeanServer(); // register on JMX try { mbs.registerMBean(new RouteMBean(), new ObjectName("com.jetdrone.yoke.router:type=Route@" + hashCode() + ",method=" + verb + ",path=" + ObjectName.quote(route))); } catch (InstanceAlreadyExistsException e) { // ignore } catch (MalformedObjectNameException | MBeanRegistrationException | NotCompliantMBeanException e) { throw new RuntimeException(e); } } void addMiddleware(@NotNull Middleware[] middleware) { Collections.addAll(this.middleware, middleware); } } public static Router from(@NotNull Object... objs) { final Router router = new Router(); from(router, objs); return router; } /** * Builds a Router from an annotated Java Object */ public static Router from(@NotNull final Router router, @NotNull Object... objs) { for (Object o : objs) { Processor.process(router, o); } return router; } }