Java tutorial
/******************************************************************************* * Copyright (c) 2014 Red Hat, Inc.. * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Public License v3.0 * which accompanies this distribution, and is available at * http://www.gnu.org/licenses/gpl.html * * Contributors: * Red Hat, Inc. - initial API and implementation ******************************************************************************/ package org.commonjava.vertx.vabr; import static org.apache.commons.lang.StringUtils.isEmpty; import static org.commonjava.vertx.vabr.util.AnnotationUtils.getHandlerKey; import static org.commonjava.vertx.vabr.util.RouterUtils.requestUri; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.lang.StringUtils; import org.commonjava.vertx.vabr.anno.Handles; import org.commonjava.vertx.vabr.bind.BindingContext; import org.commonjava.vertx.vabr.bind.BindingKey; import org.commonjava.vertx.vabr.bind.PatternFilterBinding; import org.commonjava.vertx.vabr.bind.PatternRouteBinding; import org.commonjava.vertx.vabr.bind.filter.FilterBinding; import org.commonjava.vertx.vabr.bind.filter.FilterCollection; import org.commonjava.vertx.vabr.bind.route.RouteBinding; import org.commonjava.vertx.vabr.bind.route.RouteCollection; import org.commonjava.vertx.vabr.helper.AcceptInfo; import org.commonjava.vertx.vabr.helper.ExecutionChainHandler; import org.commonjava.vertx.vabr.helper.RequestHandler; import org.commonjava.vertx.vabr.helper.RoutingCriteria; import org.commonjava.vertx.vabr.types.ApplicationHeader; import org.commonjava.vertx.vabr.types.BuiltInParam; import org.commonjava.vertx.vabr.types.Method; import org.commonjava.vertx.vabr.util.Query; import org.commonjava.vertx.vabr.util.RouteHeader; import org.commonjava.vertx.vabr.util.RouterUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.vertx.java.core.Handler; import org.vertx.java.core.MultiMap; import org.vertx.java.core.http.HttpServerRequest; import org.vertx.java.core.impl.CaseInsensitiveMultiMap; public class ApplicationRouter implements Handler<HttpServerRequest> { protected final Logger logger = LoggerFactory.getLogger(getClass()); private Map<BindingKey, List<PatternRouteBinding>> routeBindings = new HashMap<>(); private Map<BindingKey, List<PatternFilterBinding>> filterBindings = new HashMap<>(); private final Map<String, Object> handlers = new HashMap<>(); private Handler<HttpServerRequest> noMatchHandler; private String prefix; private String appAcceptId = "app"; private String defaultVersion = "v1"; private ExecutorService handlerExecutor; private final Map<String, String> routeAliases; public ApplicationRouter() { this(new ApplicationRouterConfig()); } public ApplicationRouter(final ApplicationRouterConfig config) { this.prefix = config.getPrefix(); this.noMatchHandler = config.getNoMatchHandler(); this.routeAliases = config.getRouteAliases(); if (config.getAppAcceptId() != null) { this.appAcceptId = config.getAppAcceptId(); } if (config.getDefaultVersion() != null) { this.defaultVersion = config.getDefaultVersion(); } this.handlerExecutor = config.getHandlerExecutor(); final Set<RequestHandler> h = config.getHandlers(); final List<RouteCollection> routeCollections = config.getRouteCollections(); final List<FilterCollection> filterCollections = config.getFilterCollections(); bindHandlers(h); bindRouteCollections(routeCollections); bindFilterCollections(filterCollections); } public void setHandlerExecutor(final ExecutorService executor) { this.handlerExecutor = executor; } public synchronized ExecutorService getHandlerExecutor() { if (handlerExecutor == null) { handlerExecutor = Executors.newCachedThreadPool(); } return handlerExecutor; } public void bindFilters(final Iterable<?> handlers, final Iterable<FilterCollection> filterCollections) { bindHandlers(handlers); bindFilterCollections(filterCollections); } public void bindRoutes(final Iterable<?> handlers, final Iterable<RouteCollection> routeCollections) { bindHandlers(handlers); bindRouteCollections(routeCollections); } public void bindHandlers(final Iterable<?> handlers) { if (handlers != null) { for (final Object handler : handlers) { final String key = getHandlerKey(handler.getClass()); if (this.handlers.containsKey(key)) { continue; } logger.info("Handlers += {} ({})", key, handler.getClass().getName()); this.handlers.put(key, handler); } } } public void bindFilterCollections(final Iterable<FilterCollection> filterCollections) { if (filterCollections != null) { for (final FilterCollection fc : filterCollections) { logger.info("Binding filters in collection: {}", fc.getClass().getName()); for (final FilterBinding fb : fc) { if (!this.handlers.containsKey(fb.getHandlerKey())) { logger.error("Route handler '{}' not found for binding: {}", fb.getHandlerKey(), fb); } bind(fb); } } } } public void bindRouteCollections(final Iterable<RouteCollection> routeCollections) { if (routeCollections != null) { for (final RouteCollection rc : routeCollections) { logger.info("Binding routes in collection: {}", rc.getClass().getName()); for (final RouteBinding rb : rc) { if (!this.handlers.containsKey(rb.getHandlerKey())) { logger.error("Route handler '{}' not found for binding: {}", rb.getHandlerKey(), rb); } logger.info("Routes += {} ({})", rb.getPath(), rb.getMethod()); bind(rb); } } } } public <T> T getResourceInstance(final Class<T> cls) { final Object handler = handlers.get(getHandlerKey(cls)); return handler == null ? null : cls.cast(handler); } @Override public void handle(final HttpServerRequest request) { request.pause(); try { if (!routeRequest(request.path(), request)) { if (noMatchHandler != null) { noMatchHandler.handle(request); } else { // Default 404 request.resume().response().setStatusCode(404).setStatusMessage("Not Found").setChunked(true) .end("No handler found"); } } } catch (final Throwable t) { logger.error(String.format("ERROR: %s", t.getMessage()), t); request.resume().response().setStatusCode(500).setStatusMessage("Internal Server Error") .setChunked(true).end("Error occurred during processing. See logs for more information."); } } public boolean routeRequest(String path, final HttpServerRequest request) throws Exception { logger.info("Originating path: {}", path); path = RouterUtils.trimPrefix(prefix, path); if (path == null) { return false; } for (final Map.Entry<String, String> entry : routeAliases.entrySet()) { final String alias = entry.getKey(); if (path.startsWith(alias)) { final String oldPath = path; path = Paths.get(entry.getValue(), path.substring(alias.length())).toString(); logger.info("ALIAS:\n{}\n\nwill be forwarded to:\n\n{}\n\n", oldPath, path); request.response().putHeader(ApplicationHeader.deprecated.key(), path); break; } } final Method method = Method.valueOf(request.method()); final RoutingCriteria routingCriteria = RoutingCriteria.parse(request, this.appAcceptId, this.defaultVersion); for (final AcceptInfo info : routingCriteria) { final String version = info.getVersion(); final BindingKey key = new BindingKey(method, version); logger.info("REQUEST>>> {} {}\n", key, path); final BindingContext ctx = findBinding(key, path, routingCriteria); if (ctx != null) { final RouteBinding handler = ctx.getPatternRouteBinding().getHandler(); if (!info.getRawAccept().equals(RoutingCriteria.ACCEPT_ANY)) { request.headers().add(RouteHeader.recommended_content_type.header(), info.getRawAccept()); request.headers().add(RouteHeader.base_accept.header(), info.getBaseAccept()); } request.headers().add(RouteHeader.recommended_content_version.header(), version); logger.info("MATCH: {}\n", handler); parseParams(ctx, request); new ExecutionChainHandler(this, ctx, request).execute(); return true; } } return false; } protected void parseParams(final BindingContext ctx, final HttpServerRequest request) { final Matcher matcher = ctx.getMatcher(); final MultiMap params = new CaseInsensitiveMultiMap(); final String fullPath = request.path(); final String uri = requestUri(request); final PatternRouteBinding routeBinding = ctx.getPatternRouteBinding(); final List<String> paramNames = routeBinding.getParamNames(); final RouteBinding handler = routeBinding.getHandler(); int i = 1; if (matcher.groupCount() > 0) { final int firstIdx = matcher.start(i) + prefix.length(); // be defensive in case the first param is optional... String routeBase = (firstIdx > 0) ? fullPath.substring(0, firstIdx) : fullPath; if (routeBase.endsWith("/")) { routeBase = routeBase.substring(0, routeBase.length() - 1); } params.add(BuiltInParam._routeBase.key(), routeBase); int idx = uri.indexOf(routeBase) + routeBase.length(); final String routeContextUrl = uri.substring(0, idx); params.add(BuiltInParam._routeContextUrl.key(), routeContextUrl); idx = fullPath.indexOf(routeBase); String classBase = null; String classContext = null; if (idx > -1) { idx += routeBase.length(); classBase = fullPath.substring(0, idx); params.add(BuiltInParam._classBase.key(), classBase); idx = uri.indexOf(routeBase) + routeBase.length(); classContext = uri.substring(0, idx); params.add(BuiltInParam._classContextUrl.key(), classContext); } } else { final Class<?> handlerCls = handler.getHandlesClass(); final Handles handles = handlerCls.getAnnotation(Handles.class); String pathPrefix = handles.value(); if (isEmpty(pathPrefix)) { pathPrefix = handles.prefix(); } final String find = pathPrefix.length() > 0 ? pathPrefix : prefix; int idx = fullPath.indexOf(find); String classBase = null; String classContext = null; if (idx > -1) { idx += find.length(); classBase = fullPath.substring(0, idx); params.add(BuiltInParam._classBase.key(), classBase); idx = uri.indexOf(find) + find.length(); classContext = uri.substring(0, idx); params.add(BuiltInParam._classContextUrl.key(), classContext); } } if (paramNames != null) { // Named params for (final String param : paramNames) { final String v = matcher.group(i); if (v != null) { logger.info("PARAM {} = {}", param, v); params.add(param, v); } i++; } } // Un-named params for (; i < matcher.groupCount(); i++) { final String v = matcher.group(i); if (v != null) { logger.info("PARAM param{} = {}", i, v); params.add("param" + i, v); } } final Query query = Query.from(request); for (final String name : paramNames) { params.add("q:" + name, query.getAll(name)); } // logger.info( "PARAMS: {}\n", params ); request.params().add(params); } protected BindingContext findBinding(final BindingKey key, final String path, final RoutingCriteria routingCriteria) { final List<PatternFilterBinding> allFilterBindings = this.filterBindings.get(key); PatternFilterBinding filterBinding = null; if (allFilterBindings != null) { for (final PatternFilterBinding binding : allFilterBindings) { if (binding.getPattern().matcher(path).matches()) { filterBinding = binding; break; } } } logger.debug("Searching for bindings matching key: {}", key); final List<PatternRouteBinding> routeBindings = this.routeBindings.get(key); logger.debug("Available bindings:\n {}\n", new Object() { @Override public String toString() { return StringUtils.join(routeBindings, "\n "); } }); if (routeBindings != null) { for (final PatternRouteBinding binding : routeBindings) { final Matcher m = Pattern.compile(binding.getPattern()).matcher(path); if (m.matches()) { logger.debug("Route pattern: {} matches: {}", binding.getPattern(), path); final String produces = binding.getHandler().getContentType(); if (produces != null) { for (final AcceptInfo info : routingCriteria) { logger.debug("Checking produces: {} vs accept header: {}", produces, info.getBaseAccept()); if (info.getBaseAccept().equals(RoutingCriteria.ACCEPT_ANY) || info.getBaseAccept().equals(produces.toLowerCase()) || info.getRawAccept().equals(produces.toLowerCase())) { logger.debug("Using binding: {}", binding); return new BindingContext(m, binding, filterBinding); } else { logger.warn( "Accept content-type: '{}' DID NOT MATCH produced content-type: '{}' for: {}. NOT A MATCH.", info.getBaseAccept(), produces, binding); } } } else { logger.debug("No produces; Using binding: {}", binding); return new BindingContext(m, binding, filterBinding); } } else { logger.debug("Route pattern: {} did NOT match: {}", binding.getPattern(), path); } } } logger.debug("No matching route bindings. Aborting request handling in: {}", this.getClass().getSimpleName()); return null; } /** * Specify a handler that will be called for all HTTP methods * @param pattern The simple pattern * @param handler The handler to call */ public void bind(final RouteBinding handler) { final Method method = handler.getMethod(); logger.info("Using appId: {} and default version: {}", appAcceptId, defaultVersion); List<String> versions = handler.getVersions(); if (versions == null || versions.isEmpty()) { versions = Collections.singletonList(defaultVersion); } for (final String version : versions) { final Set<Method> methods = new HashSet<>(); if (method == Method.ANY) { for (final Method m : Method.values()) { methods.add(m); } } else { methods.add(method); } for (final Method m : methods) { final BindingKey key = new BindingKey(m, version); List<PatternRouteBinding> b = routeBindings.get(key); if (b == null) { b = new ArrayList<>(); routeBindings.put(key, b); } logger.info("ADD: {}, Pattern: {}, Route: {}\n", key, handler.getPath(), handler); addPattern(handler, b); } } } /** * Specify a filter handler that will be used to wrap route executions * @param pattern The simple pattern * @param handler The handler to call */ public void bind(final FilterBinding handler) { final Method method = handler.getMethod(); final String path = handler.getPath(); logger.info("Using appId: {} and default version: {}", appAcceptId, defaultVersion); List<String> versions = handler.getVersions(); if (versions == null || versions.isEmpty()) { versions = Collections.singletonList(defaultVersion); } for (final String version : versions) { final Set<Method> methods = new HashSet<>(); if (method == Method.ANY) { for (final Method m : Method.values()) { methods.add(m); } } else { methods.add(method); } for (final Method m : methods) { final BindingKey key = new BindingKey(m, version); logger.info("ADD: {}, Pattern: {}, Filter: {}\n", key, path, handler); List<PatternFilterBinding> allFilterBindings = this.filterBindings.get(key); if (allFilterBindings == null) { allFilterBindings = new ArrayList<>(); this.filterBindings.put(key, allFilterBindings); } boolean found = false; for (final PatternFilterBinding binding : allFilterBindings) { if (binding.getPattern().pattern().equals(handler.getPath())) { binding.addFilter(handler); found = true; break; } } if (!found) { final PatternFilterBinding binding = new PatternFilterBinding(handler.getPath(), handler); allFilterBindings.add(binding); } } } } /** * Specify a handler that will be called when no other handlers match. * If this handler is not specified default behaviour is to return a 404 * @param handler */ public void noMatch(final Handler<HttpServerRequest> handler) { noMatchHandler = handler; } protected void addPattern(final RouteBinding handler, final List<PatternRouteBinding> bindings) { // logger.info( "BIND regex: {}, groups: {}, route: {}\n", regex, groups, handler ); final PatternRouteBinding binding = PatternRouteBinding.parse(handler); bindings.add(binding); Collections.sort(bindings); } public Map<BindingKey, List<PatternRouteBinding>> getRouteBindings() { return routeBindings; } public Map<BindingKey, List<PatternFilterBinding>> getFilterBindings() { return filterBindings; } public Map<String, ?> getHandlers() { return handlers; } public Handler<HttpServerRequest> getNoMatchHandler() { return noMatchHandler; } public String getPrefix() { return prefix; } public void setNoMatchHandler(final Handler<HttpServerRequest> noMatchHandler) { this.noMatchHandler = noMatchHandler; } public void setPrefix(final String prefix) { this.prefix = prefix; } public void setRouteBindings(final Map<BindingKey, List<PatternRouteBinding>> bindings) { this.routeBindings = bindings; } public void setFilterBindings(final Map<BindingKey, List<PatternFilterBinding>> bindings) { this.filterBindings = bindings; } public void setHandlers(final Map<String, Object> handlers) { this.handlers.clear(); this.handlers.putAll(handlers); } public String getAppAcceptId() { return appAcceptId; } public void setAppAcceptId(final String appAcceptId) { this.appAcceptId = appAcceptId; } public String getDefaultVersion() { return defaultVersion; } public void setDefaultVersion(final String defaultVersion) { this.defaultVersion = defaultVersion; } protected Logger getLogger() { return logger; } }