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

ro.pippo.core.route.DefaultRouter Maven / Gradle / Ivy

There is a newer version: 1.8.0
Show newest version
/*
 * Copyright (C) 2014 the original author or authors.
 *
 * 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 ro.pippo.core.route;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ro.pippo.core.HttpConstants;
import ro.pippo.core.PippoConstants;
import ro.pippo.core.PippoRuntimeException;
import ro.pippo.core.util.StringUtils;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * The routes are matched in the order they are defined.
 *
 * @author Decebal Suiu
 * @author James Moger
 */
public class DefaultRouter implements Router {

    private static final Logger log = LoggerFactory.getLogger(DefaultRouter.class);

    // Matches: {id} AND {id: .*?}
    // group(1) extracts the name of the group (in that case "id").
    // group(3) extracts the regex if defined
    private static final Pattern PATTERN_FOR_VARIABLE_PARTS_OF_ROUTE = Pattern.compile("\\{(.*?)(:\\s(.*?))?\\}");

    // This regex matches everything in between path slashes.
    private static final String VARIABLE_ROUTES_DEFAULT_REGEX = "([^/]*)";

    // This regex works for both {myParam} AND {myParam: .*}
    private static final String VARIABLE_PART_PATTERN_WITH_PLACEHOLDER = "\\{(%s)(:\\s([^}]*))?\\}";

    // key = request method
    private Map> bindingsCache;

    private List routes;
    private Set ignorePaths;
    private Map> cache; // key = request method
    private String contextPath;
    private String applicationPath;

    public DefaultRouter() {
        routes = new ArrayList<>();
        ignorePaths = new TreeSet<>();
        cache = new HashMap<>();
        bindingsCache = new HashMap<>();
        contextPath = "";
        applicationPath = "";
    }

    @Override
    public String getContextPath() {
        return contextPath;
    }

    @Override
    public void setContextPath(String contextPath) {
        if (StringUtils.isNullOrEmpty(contextPath) || "/".equals(contextPath.trim())) {
            this.contextPath = "";
        } else {
            this.contextPath = StringUtils.addStart(contextPath, "/");
        }
    }

    /**
     * Prefix the given path with the application path.
     *
     * @param path
     * @return an absolute path
     */
    protected String prefixApplicationPath(String path) {
        return applicationPath + StringUtils.addStart(path, "/");
    }

    @Override
    public Set getIgnorePaths() {
        return ignorePaths;
    }

    @Override
    public void ignorePaths(String... pathPrefixes) {
        for (String pathPrefix : pathPrefixes) {
            this.ignorePaths.add(StringUtils.addStart(pathPrefix, "/"));
        }
    }

    @Override
    public final List getRoutes() {
        return Collections.unmodifiableList(routes);
    }

    public List getRoutes(String requestMethod) {
        List routes = cache.get(requestMethod);
        if (routes != null) {
            routes = Collections.unmodifiableList(routes);
        } else {
            routes = Collections.emptyList();
        }

        return routes;
    }

    protected void validateRoute(Route route) {
        // validate the request method
        if (StringUtils.isNullOrEmpty(route.getRequestMethod())) {
            throw new PippoRuntimeException("Unspecified request method!");
        }

        // validate the uri pattern
        String uriPattern = route.getUriPattern();
        if (StringUtils.isNullOrEmpty(uriPattern)) {
            throw new PippoRuntimeException("The uri pattern cannot be null or empty");
        }
    }

    @Override
    public List findRoutes(String requestMethod, String requestUri) {
        log.trace("Finding route matches for {} '{}'", requestMethod, requestUri);

        List routeMatches = new ArrayList<>();

        List bindings = getBindings(requestMethod);
        for (Route route : routes) { // to preserve the routes order
            for (PatternBinding binding : bindings) {
                if (route.equals(binding.getRoute()) && binding.getPattern().matcher(requestUri).matches()) {
                    // TODO improve (it's possible to have the same uriPattern for many routes => same parameters)
                    routeMatches.add(new RouteMatch(route, getParameters(binding, requestUri)));
                    break;
                }
            }
        }

        log.debug("Found {} route matches for {} '{}'", routeMatches.size(), requestMethod, requestUri);

        return routeMatches;
    }

    @Override
    public void addRoute(Route route) {
        log.debug("Add route for {} '{}'", route.getRequestMethod(), route.getUriPattern());
        validateRoute(route);
        routes.add(route);

        List cacheEntry = cache.get(route.getRequestMethod());
        if (cacheEntry == null) {
            cacheEntry = new ArrayList<>();
        }
        cacheEntry.add(route);
        cache.put(route.getRequestMethod(), cacheEntry);

        addBinding(route);
    }

    @Override
    public void removeRoute(Route route) {
        log.debug("Removing route for {} '{}'", route.getRequestMethod(), route.getUriPattern());
        routes.remove(route);

        List cacheEntry = cache.get(route.getRequestMethod());
        if (cacheEntry != null) {
            cacheEntry.remove(route);
        }

        removeBinding(route);
    }

    @Override
    public String uriFor(String relativeUri) {
        return prefixApplicationPath(relativeUri);
    }

    /**
     * Generate an URI string for a route (referenced by an uriPattern) with some parameters.
     * For example:
     * 

*

     * // add a route
     * GET("/user", (routeContext)-> {...});
     *
     * // get an uri string for the above route
     * Map parameters = new HashMap<>();
     * parameters.put("admin", true);
     * parameters.put("company", "Home Office")
     * String uri = uriFor("/user", parameters);
     * // the result is "/user?admin=true&company=Home+Office"
     * 
* The parameters values are automatically encoded by this method. * * @param nameOrUriPattern * @param parameters * @return */ @Override public String uriFor(String nameOrUriPattern, Map parameters) { PatternBinding binding = getBinding(nameOrUriPattern); return (binding != null) ? prefixApplicationPath(uriFor(binding, parameters)) : null; } @Override public String uriPatternFor(Class resourceHandlerClass) { Route route = getRoute(resourceHandlerClass); return (route != null) ? route.getUriPattern() : null; } @Override public String getApplicationPath() { return applicationPath; } @Override public void setApplicationPath(String applicationPath) { if (StringUtils.isNullOrEmpty(applicationPath) || "/".equals(applicationPath.trim())) { this.applicationPath = ""; } else { this.applicationPath = StringUtils.removeEnd(StringUtils.addStart(applicationPath, "/"), "/"); } } private Route getRoute(Class resourceHandlerClass) { List routes = getRoutes(); for (Route route : routes) { RouteHandler routeHandler = route.getRouteHandler(); if (resourceHandlerClass.isAssignableFrom(routeHandler.getClass())) { ClasspathResourceHandler resourceHandler = (ClasspathResourceHandler) routeHandler; if (resourceHandlerClass == resourceHandler.getClass()) { return route; } } } return null; } private void addBinding(Route route) { String uriPattern = route.getUriPattern(); // TODO improve (it's possible to have the same uriPattern for many routes => same pattern) String regex = getRegex(uriPattern); Pattern pattern = Pattern.compile(regex); List parameterNames = getParameterNames(uriPattern); PatternBinding binding = new PatternBinding(pattern, route, parameterNames); String requestMethod = route.getRequestMethod(); if (!bindingsCache.containsKey(requestMethod)) { bindingsCache.put(requestMethod, new ArrayList()); } bindingsCache.get(requestMethod).add(binding); } private void removeBinding(Route route) { String nameOrUriPattern = StringUtils.isNullOrEmpty(route.getName()) ? route.getUriPattern() : route.getName(); PatternBinding binding = getBinding(nameOrUriPattern); bindingsCache.get(route.getRequestMethod()).remove(binding); } private PatternBinding getBinding(String nameOrUriPattern) { Collection> values = bindingsCache.values(); Route route; for (List bindings : values) { for (PatternBinding binding : bindings) { route = binding.getRoute(); if (nameOrUriPattern.equals(route.getName()) || nameOrUriPattern.equals(route.getUriPattern())) { return binding; } } } return null; } private List getBindings(String requestMethod) { List bindings = new ArrayList<>(); if (bindingsCache.containsKey(requestMethod)) { bindings.addAll(bindingsCache.get(requestMethod)); } if (bindingsCache.containsKey(HttpConstants.Method.ALL)) { bindings.addAll(bindingsCache.get(HttpConstants.Method.ALL)); } return bindings; } /** * Transforms an url pattern like "/{name}/id/*" into a regex like "/([^/]*)/id/*." *

* Also handles regular expressions if defined inside routes: * For instance "/users/{username: [a-zA-Z][a-zA-Z_0-9]}" becomes * "/users/([a-zA-Z][a-zA-Z_0-9])" * * @return The converted regex with default matching regex - or the regex * specified by the user. */ private String getRegex(String urlPattern) { StringBuffer buffer = new StringBuffer(); Matcher matcher = PATTERN_FOR_VARIABLE_PARTS_OF_ROUTE.matcher(urlPattern); while (matcher.find()) { // By convention group 3 is the regex if provided by the user. // If it is not provided by the user the group 3 is null. String namedVariablePartOfRoute = matcher.group(3); String namedVariablePartOfORouteReplacedWithRegex; if (namedVariablePartOfRoute != null) { // we convert that into a regex matcher group itself String variableRegex = replacePosixClasses(namedVariablePartOfRoute); namedVariablePartOfORouteReplacedWithRegex = "(" + Matcher.quoteReplacement(variableRegex) + ")"; } else { // we convert that into the default namedVariablePartOfRoute regex group namedVariablePartOfORouteReplacedWithRegex = VARIABLE_ROUTES_DEFAULT_REGEX; } // we replace the current namedVariablePartOfRoute group matcher.appendReplacement(buffer, namedVariablePartOfORouteReplacedWithRegex); } // .. and we append the tail to complete the stringBuffer matcher.appendTail(buffer); return buffer.toString(); } /** * Replace any specified POSIX character classes with the Java equivalent. * * @param input * @return a Java regex */ private String replacePosixClasses(String input) { return input .replace(":alnum:", "\\p{Alnum}") .replace(":alpha:", "\\p{L}") .replace(":ascii:", "\\p{ASCII}") .replace(":digit:", "\\p{Digit}") .replace(":xdigit:", "\\p{XDigit}"); } /** * Extracts the name of the parameters from a route *

* /{my_id}/{my_name} *

* would return a List with "my_id" and "my_name" * * @param uriPattern * @return a list with the names of all parameters in the url pattern */ private List getParameterNames(String uriPattern) { List list = new ArrayList<>(); Matcher matcher = PATTERN_FOR_VARIABLE_PARTS_OF_ROUTE.matcher(uriPattern); while (matcher.find()) { // group(1) is the name of the group. Must be always there... // "/assets/{file}" and "/assets/{file: [a-zA-Z][a-zA-Z_0-9]}" // will return file. list.add(matcher.group(1)); } return list; } private Map getParameters(PatternBinding binding, String requestUri) { if (binding.getParameterNames().isEmpty()) { return Collections.EMPTY_MAP; } Map parameters = new HashMap<>(); List parameterNames = binding.getParameterNames(); Matcher matcher = binding.getPattern().matcher(requestUri); matcher.matches(); int groupCount = matcher.groupCount(); if (groupCount > 0) { for (int i = 0; i < parameterNames.size(); i++) { parameters.put(parameterNames.get(i), matcher.group(i + 1)); } } return parameters; } private String uriFor(PatternBinding binding, Map parameters) { Route route = binding.getRoute(); boolean isResourceRoute = ResourceHandler.class.isAssignableFrom(route.getRouteHandler().getClass()); List parameterNames = binding.getParameterNames(); if (!parameters.keySet().containsAll(parameterNames)) { log.error("You must provide values for all path parameters. {} vs {}", parameterNames, parameters.keySet()); } Map queryParameters = new HashMap<>(parameters.size()); String uri = route.getUriPattern(); for (Entry parameterPair : parameters.entrySet()) { boolean foundAsPathParameter = false; StringBuffer sb = new StringBuffer(); String regex = String.format(VARIABLE_PART_PATTERN_WITH_PLACEHOLDER, parameterPair.getKey()); Pattern pattern = Pattern.compile(regex); Matcher matcher = pattern.matcher(uri); while (matcher.find()) { String pathValue = parameterPair.getValue().toString(); if (isResourceRoute && ResourceHandler.PATH_PARAMETER.equals(parameterPair.getKey())) { ResourceHandler resourceHandler = (ResourceHandler) route.getRouteHandler(); if (resourceHandler.isVersioned()) { pathValue = resourceHandler.injectVersion(pathValue); } } matcher.appendReplacement(sb, pathValue); foundAsPathParameter = true; } matcher.appendTail(sb); uri = sb.toString(); if (!foundAsPathParameter) { queryParameters.put(parameterPair.getKey(), parameterPair.getValue()); } } // now prepare the query string for this url if we got some query params if (!queryParameters.isEmpty()) { // add remaining parameters as query parameters StringBuilder query = new StringBuilder(); Iterator> iterator = queryParameters.entrySet().iterator(); while (iterator.hasNext()) { Entry parameterEntry = iterator.next(); String parameterName = parameterEntry.getKey(); Object parameterValue = parameterEntry.getValue(); String encodedParameterValue; try { encodedParameterValue = URLEncoder.encode(parameterValue.toString(), PippoConstants.UTF8); } catch (UnsupportedEncodingException e) { throw new PippoRuntimeException(e, "Cannot encode the parameter value '{}'", parameterValue.toString()); } query.append(parameterName).append("=").append(encodedParameterValue); if (iterator.hasNext()) { query.append("&"); } } uri += "?" + query; } return uri; } private class PatternBinding { private final Pattern pattern; private final Route route; private final List parameterNames; private PatternBinding(Pattern pattern, Route route, List parameterNames) { this.pattern = pattern; this.route = route; this.parameterNames = parameterNames; } public Pattern getPattern() { return pattern; } public Route getRoute() { return route; } public List getParameterNames() { return parameterNames; } @Override public String toString() { return "PatternBinding{" + "pattern=" + pattern + ", route=" + route + ", parameterNames=" + parameterNames + '}'; } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy