com.rackspacecloud.blueflood.http.RouteMatcher.java Source code

Java tutorial

Introduction

Here is the source code for com.rackspacecloud.blueflood.http.RouteMatcher.java

Source

/*
 * 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;
        }
    }
}