Java tutorial
/* * Copyright 2013 Rackspace * * Licensed 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.rackspacecloud.blueflood.http; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.FullHttpRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * A class responsible for matching URI to a pattern and routing * requests to the appropriate {@link HttpRequestHandler} */ public class RouteMatcher { private final Map<Pattern, PatternRouteBinding> getBindings; private final Map<Pattern, PatternRouteBinding> putBindings; private final Map<Pattern, PatternRouteBinding> postBindings; private final Map<Pattern, PatternRouteBinding> deleteBindings; private final Map<Pattern, PatternRouteBinding> headBindings; private final Map<Pattern, PatternRouteBinding> optionsBindings; private final Map<Pattern, PatternRouteBinding> traceBindings; private final Map<Pattern, PatternRouteBinding> connectBindings; private final Map<Pattern, PatternRouteBinding> patchBindings; private HttpRequestHandler noRouteHandler; private HttpRequestHandler unsupportedMethodHandler; private HttpRequestHandler unsupportedVerbsHandler; private Map<Pattern, Set<String>> supportedMethodsForURLs; private List<Pattern> knownPatterns; private final Set<String> implementedVerbs; private static final Logger log = LoggerFactory.getLogger(RouteMatcher.class); public RouteMatcher() { this.getBindings = new HashMap<Pattern, PatternRouteBinding>(); this.putBindings = new HashMap<Pattern, PatternRouteBinding>(); this.postBindings = new HashMap<Pattern, PatternRouteBinding>(); this.deleteBindings = new HashMap<Pattern, PatternRouteBinding>(); this.headBindings = new HashMap<Pattern, PatternRouteBinding>(); this.optionsBindings = new HashMap<Pattern, PatternRouteBinding>(); this.connectBindings = new HashMap<Pattern, PatternRouteBinding>(); this.patchBindings = new HashMap<Pattern, PatternRouteBinding>(); this.traceBindings = new HashMap<Pattern, PatternRouteBinding>(); this.implementedVerbs = new HashSet<String>(); this.noRouteHandler = new NoRouteHandler(); this.unsupportedMethodHandler = new UnsupportedMethodHandler(this); this.unsupportedVerbsHandler = new UnsupportedVerbsHandler(); this.supportedMethodsForURLs = new HashMap<Pattern, Set<String>>(); this.knownPatterns = new ArrayList<Pattern>(); } public RouteMatcher withNoRouteHandler(HttpRequestHandler noRouteHandler) { this.noRouteHandler = noRouteHandler; return this; } public void route(ChannelHandlerContext context, FullHttpRequest request) { final String method = request.getMethod().name(); final String URI = request.getUri(); // Method not implemented for any resource. So return 501. if (method == null || !implementedVerbs.contains(method)) { route(context, request, unsupportedVerbsHandler); return; } final Pattern pattern = getMatchingPatternForURL(URI); // No methods registered for this pattern i.e. URL isn't registered. Return 404. if (pattern == null) { route(context, request, noRouteHandler); return; } final Set<String> supportedMethods = getSupportedMethods(pattern); if (supportedMethods == null) { log.warn("No supported methods registered for a known pattern " + pattern); route(context, request, noRouteHandler); return; } // The method requested is not available for the resource. Return 405. if (!supportedMethods.contains(method)) { route(context, request, unsupportedMethodHandler); return; } PatternRouteBinding binding = null; if (method.equals(HttpMethod.GET.name())) { binding = getBindings.get(pattern); } else if (method.equals(HttpMethod.PUT.name())) { binding = putBindings.get(pattern); } else if (method.equals(HttpMethod.POST.name())) { binding = postBindings.get(pattern); } else if (method.equals(HttpMethod.DELETE.name())) { binding = deleteBindings.get(pattern); } else if (method.equals(HttpMethod.PATCH.name())) { binding = deleteBindings.get(pattern); } else if (method.equals(HttpMethod.OPTIONS.name())) { binding = optionsBindings.get(pattern); } else if (method.equals(HttpMethod.HEAD.name())) { binding = headBindings.get(pattern); } else if (method.equals(HttpMethod.TRACE.name())) { binding = traceBindings.get(pattern); } else if (method.equals(HttpMethod.CONNECT.name())) { binding = connectBindings.get(pattern); } if (binding != null) { request = updateRequestHeaders(request, binding); route(context, request, binding.handler); } else { throw new RuntimeException("Cannot find a valid binding for URL " + URI); } } public void get(String pattern, HttpRequestHandler handler) { addBinding(pattern, HttpMethod.GET.name(), handler, getBindings); } public void put(String pattern, HttpRequestHandler handler) { addBinding(pattern, HttpMethod.PUT.name(), handler, putBindings); } public void post(String pattern, HttpRequestHandler handler) { addBinding(pattern, HttpMethod.POST.name(), handler, postBindings); } public void delete(String pattern, HttpRequestHandler handler) { addBinding(pattern, HttpMethod.DELETE.name(), handler, deleteBindings); } public void head(String pattern, HttpRequestHandler handler) { addBinding(pattern, HttpMethod.HEAD.name(), handler, headBindings); } public void options(String pattern, HttpRequestHandler handler) { addBinding(pattern, HttpMethod.OPTIONS.name(), handler, optionsBindings); } public void connect(String pattern, HttpRequestHandler handler) { addBinding(pattern, HttpMethod.CONNECT.name(), handler, connectBindings); } public void patch(String pattern, HttpRequestHandler handler) { addBinding(pattern, HttpMethod.PATCH.name(), handler, patchBindings); } public Set<String> getSupportedMethodsForURL(String URL) { final Pattern pattern = getMatchingPatternForURL(URL); return getSupportedMethods(pattern); } private FullHttpRequest updateRequestHeaders(FullHttpRequest request, PatternRouteBinding binding) { Matcher m = binding.pattern.matcher(request.getUri()); if (m.matches()) { Map<String, String> headers = new HashMap<String, String>(m.groupCount()); if (binding.paramsPositionMap != null) { for (String header : binding.paramsPositionMap.keySet()) { headers.put(header, m.group(binding.paramsPositionMap.get(header))); } } else { for (int i = 0; i < m.groupCount(); i++) { headers.put("param" + i, m.group(i + 1)); } } for (Map.Entry<String, String> header : headers.entrySet()) { request.headers().add(header.getKey(), header.getValue()); } } return request; } private void route(ChannelHandlerContext context, FullHttpRequest request, HttpRequestHandler handler) { if (handler == null) { handler = unsupportedVerbsHandler; } handler.handle(context, request); } private Pattern getMatchingPatternForURL(String URL) { for (Pattern pattern : knownPatterns) { if (pattern.matcher(URL).matches()) { return pattern; } } return null; } private Set<String> getSupportedMethods(Pattern pattern) { if (pattern == null) { return null; } return supportedMethodsForURLs.get(pattern); } private void addBinding(String URLPattern, String method, HttpRequestHandler handler, Map<Pattern, PatternRouteBinding> bindings) { if (method == null || URLPattern == null || URLPattern.isEmpty() || method.isEmpty()) { return; } if (!method.isEmpty() && !URLPattern.isEmpty()) { implementedVerbs.add(method); } final PatternRouteBinding routeBinding = getPatternRouteBinding(URLPattern, handler); knownPatterns.add(routeBinding.pattern); Set<String> supportedMethods = supportedMethodsForURLs.get(routeBinding.pattern); if (supportedMethods == null) { supportedMethods = new HashSet<String>(); } supportedMethods.add(method); supportedMethodsForURLs.put(routeBinding.pattern, supportedMethods); bindings.put(routeBinding.pattern, routeBinding); } private PatternRouteBinding getPatternRouteBinding(String URLPattern, HttpRequestHandler handler) { Pattern pattern = getMatchingPatternForURL(URLPattern); Map<String, Integer> groups = null; if (pattern == null) { // We need to search for any :<token name> tokens in the String and replace them with named capture groups Matcher m = Pattern.compile(":([A-Za-z][A-Za-z0-9_]*)").matcher(URLPattern); StringBuffer sb = new StringBuffer(); groups = new HashMap<String, Integer>(); int pos = 1; // group 0 is the whole expression while (m.find()) { String group = m.group().substring(1); if (groups.containsKey(group)) { throw new IllegalArgumentException( "Cannot use identifier " + group + " more than once in pattern string"); } m.appendReplacement(sb, "([^/]+)"); groups.put(group, pos++); } m.appendTail(sb); final String regex = sb.toString(); pattern = Pattern.compile(regex); } return new PatternRouteBinding(pattern, groups, handler); } private class PatternRouteBinding { final HttpRequestHandler handler; // TODO: Java 7 has named groups so you don't have to maintain this map explicitly. final Map<String, Integer> paramsPositionMap; final Pattern pattern; private PatternRouteBinding(Pattern pattern, Map<String, Integer> params, HttpRequestHandler handler) { this.pattern = pattern; this.paramsPositionMap = params; this.handler = handler; } } }