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

com.rackspacecloud.blueflood.http.RouteMatcher Maven / Gradle / Ivy

/*
 * 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 org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.handler.codec.http.HttpMethod;
import org.jboss.netty.handler.codec.http.HttpRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class RouteMatcher {
    private final Map getBindings;
    private final Map putBindings;
    private final Map postBindings;
    private final Map deleteBindings;
    private final Map headBindings;
    private final Map optionsBindings;
    private final Map traceBindings;
    private final Map connectBindings;
    private final Map patchBindings;
    private HttpRequestHandler noRouteHandler;
    private HttpRequestHandler unsupportedMethodHandler;
    private HttpRequestHandler unsupportedVerbsHandler;
    private Map> supportedMethodsForURLs;
    private List knownPatterns;

    private final Set implementedVerbs;
    private static final Logger log = LoggerFactory.getLogger(RouteMatcher.class);

    public RouteMatcher() {
        this.getBindings = new HashMap();
        this.putBindings = new HashMap();
        this.postBindings = new HashMap();
        this.deleteBindings = new HashMap();
        this.headBindings = new HashMap();
        this.optionsBindings = new HashMap();
        this.connectBindings = new HashMap();
        this.patchBindings = new HashMap();
        this.traceBindings = new HashMap();
        this.implementedVerbs = new HashSet();
        this.noRouteHandler = new NoRouteHandler();
        this.unsupportedMethodHandler = new UnsupportedMethodHandler(this);
        this.unsupportedVerbsHandler = new UnsupportedVerbsHandler();
        this.supportedMethodsForURLs = new HashMap>();
        this.knownPatterns = new ArrayList();
    }

    public RouteMatcher withNoRouteHandler(HttpRequestHandler noRouteHandler) {
        this.noRouteHandler = noRouteHandler;

        return this;
    }

    public void route(ChannelHandlerContext context, HttpRequest request) {
        final String method = request.getMethod().getName();
        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 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.getName())) {
            binding = getBindings.get(pattern);
        } else if (method.equals(HttpMethod.PUT.getName())) {
            binding = putBindings.get(pattern);
        } else if (method.equals(HttpMethod.POST.getName())) {
            binding = postBindings.get(pattern);
        } else if (method.equals(HttpMethod.DELETE.getName())) {
            binding = deleteBindings.get(pattern);
        } else if (method.equals(HttpMethod.PATCH.getName())) {
            binding = deleteBindings.get(pattern);
        } else if (method.equals(HttpMethod.OPTIONS.getName())) {
            binding = optionsBindings.get(pattern);
         } else if (method.equals(HttpMethod.HEAD.getName())) {
            binding = headBindings.get(pattern);
        } else if (method.equals(HttpMethod.TRACE.getName())) {
            binding = traceBindings.get(pattern);
        } else if (method.equals(HttpMethod.CONNECT.getName())) {
            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.getName(), handler, getBindings);
    }

    public void put(String pattern, HttpRequestHandler handler) {
        addBinding(pattern, HttpMethod.PUT.getName(), handler, putBindings);
    }

    public void post(String pattern, HttpRequestHandler handler) {
        addBinding(pattern, HttpMethod.POST.getName(), handler, postBindings);
    }

    public void delete(String pattern, HttpRequestHandler handler) {
        addBinding(pattern, HttpMethod.DELETE.getName(), handler, deleteBindings);
    }

    public void head(String pattern, HttpRequestHandler handler) {
        addBinding(pattern, HttpMethod.HEAD.getName(), handler, headBindings);
    }

    public void options(String pattern, HttpRequestHandler handler) {
        addBinding(pattern, HttpMethod.OPTIONS.getName(), handler, optionsBindings);
    }

    public void connect(String pattern, HttpRequestHandler handler) {
        addBinding(pattern, HttpMethod.CONNECT.getName(), handler, connectBindings);
    }

    public void patch(String pattern, HttpRequestHandler handler) {
        addBinding(pattern, HttpMethod.PATCH.getName(), handler, patchBindings);
    }

    public Set getSupportedMethodsForURL(String URL) {
        final Pattern pattern = getMatchingPatternForURL(URL);
        return getSupportedMethods(pattern);
    }

    private HttpRequest updateRequestHeaders(HttpRequest request, PatternRouteBinding binding) {
        Matcher m = binding.pattern.matcher(request.getUri());
        if (m.matches()) {
            Map headers = new HashMap(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 header : headers.entrySet()) {
                request.addHeader(header.getKey(), header.getValue());
            }
        }

        return request;
    }

    private void route(ChannelHandlerContext context, HttpRequest 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 getSupportedMethods(Pattern pattern) {
        if (pattern == null) {
            return null;
        }

        return supportedMethodsForURLs.get(pattern);
    }

    private void addBinding(String URLPattern, String method, HttpRequestHandler handler,
                            Map 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 supportedMethods = supportedMethodsForURLs.get(routeBinding.pattern);

        if (supportedMethods == null) {
            supportedMethods = new HashSet();
        }

        supportedMethods.add(method);
        supportedMethodsForURLs.put(routeBinding.pattern, supportedMethods);
        bindings.put(routeBinding.pattern, routeBinding);
    }

    private PatternRouteBinding getPatternRouteBinding(String URLPattern, HttpRequestHandler handler) {
        // 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(URLPattern);

        StringBuffer sb = new StringBuffer();
        Map groups = new HashMap();
        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();
        final Pattern 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 paramsPositionMap;
        final Pattern pattern;

        private PatternRouteBinding(Pattern pattern, Map params, HttpRequestHandler handler) {
            this.pattern = pattern;
            this.paramsPositionMap = params;
            this.handler = handler;
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy