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

org.spincast.plugins.routing.SpincastRouter Maven / Gradle / Ivy

The newest version!
package org.spincast.plugins.routing;

import java.io.File;
import java.net.URL;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.regex.Pattern;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spincast.core.config.ISpincastConfig;
import org.spincast.core.config.ISpincastDictionary;
import org.spincast.core.exchange.IRequestContext;
import org.spincast.core.filters.ISpincastFilters;
import org.spincast.core.routing.HttpMethod;
import org.spincast.core.routing.IHandler;
import org.spincast.core.routing.IRoute;
import org.spincast.core.routing.IRouteBuilder;
import org.spincast.core.routing.IRouteBuilderFactory;
import org.spincast.core.routing.IRouteHandlerMatch;
import org.spincast.core.routing.IRouter;
import org.spincast.core.routing.IRoutingResult;
import org.spincast.core.routing.IStaticResource;
import org.spincast.core.routing.IStaticResourceBuilder;
import org.spincast.core.routing.IStaticResourceBuilderFactory;
import org.spincast.core.routing.RoutingType;
import org.spincast.core.server.IServer;
import org.spincast.core.utils.SpincastStatics;
import org.spincast.core.websocket.IWebsocketContext;
import org.spincast.core.websocket.IWebsocketRoute;
import org.spincast.core.websocket.IWebsocketRouteBuilder;
import org.spincast.core.websocket.IWebsocketRouteBuilderFactory;
import org.spincast.core.websocket.IWebsocketRouteHandlerFactory;
import org.spincast.shaded.org.apache.commons.lang3.StringUtils;

import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.net.HttpHeaders;
import com.google.inject.Inject;

/**
 * Spincast router 
 */
public class SpincastRouter, W extends IWebsocketContext> implements IRouter {

    protected final Logger logger = LoggerFactory.getLogger(SpincastRouter.class);

    private final IRouteHandlerMatchFactory routeHandlerMatchFactory;
    private final IRouteBuilderFactory routeBuilderFactory;
    private final IStaticResourceBuilderFactory staticResourceBuilderFactory;
    private final IStaticResourceFactory staticResourceFactory;
    private final ISpincastRouterConfig spincastRouterConfig;
    private final IRouteFactory routeFactory;
    private final ISpincastConfig spincastConfig;
    private final ISpincastDictionary spincastDictionary;
    private final ISpincastFilters spincastFilters;
    private final IWebsocketRouteBuilderFactory websocketRouteBuilderFactory;
    private final IWebsocketRouteHandlerFactory websocketRouteHandlerFactory;

    private TreeMap>> globalBeforeFiltersPerPosition;
    private TreeMap>> globalAfterFiltersPerPosition;

    private List> globalBeforeFilters;
    private List> globalAfterFilters;
    private List> mainRoutes;

    private final IServer server;

    private final Map routeParamPatternAliases = new HashMap();

    private final Map patternCache = new HashMap();

    @Inject
    public SpincastRouter(ISpincastRouterConfig spincastRouterConfig,
                          IRouteFactory routeFactory,
                          ISpincastConfig spincastConfig,
                          ISpincastDictionary spincastDictionary,
                          IServer server,
                          ISpincastFilters spincastFilters,
                          IRouteBuilderFactory routeBuilderFactory,
                          IStaticResourceBuilderFactory staticResourceBuilderFactory,
                          IRouteHandlerMatchFactory routeHandlerMatchFactory,
                          IStaticResourceFactory staticResourceFactory,
                          IWebsocketRouteBuilderFactory websocketRouteBuilderFactory,
                          IWebsocketRouteHandlerFactory websocketRouteHandlerFactory) {
        this.spincastRouterConfig = spincastRouterConfig;
        this.routeFactory = routeFactory;
        this.spincastConfig = spincastConfig;
        this.spincastDictionary = spincastDictionary;
        this.server = server;
        this.spincastFilters = spincastFilters;
        this.routeBuilderFactory = routeBuilderFactory;
        this.staticResourceBuilderFactory = staticResourceBuilderFactory;
        this.routeHandlerMatchFactory = routeHandlerMatchFactory;
        this.staticResourceFactory = staticResourceFactory;
        this.websocketRouteBuilderFactory = websocketRouteBuilderFactory;
        this.websocketRouteHandlerFactory = websocketRouteHandlerFactory;
    }

    @Inject
    protected void init() {
        validation();
    }

    protected void validation() {
        int corsFilterPosition = getSpincastRouterConfig().getCorsFilterPosition();
        if(corsFilterPosition >= 0) {
            throw new RuntimeException("The position of the Cors filter must be less than 0. " +
                                       "Currently : " + corsFilterPosition);
        }
    }

    protected ISpincastRouterConfig getSpincastRouterConfig() {
        return this.spincastRouterConfig;
    }

    protected IRouteFactory getRouteFactory() {
        return this.routeFactory;
    }

    protected ISpincastConfig getSpincastConfig() {
        return this.spincastConfig;
    }

    protected ISpincastDictionary getSpincastDictionary() {
        return this.spincastDictionary;
    }

    protected IServer getServer() {
        return this.server;
    }

    protected ISpincastFilters getSpincastFilters() {
        return this.spincastFilters;
    }

    protected IRouteBuilderFactory getRouteBuilderFactory() {
        return this.routeBuilderFactory;
    }

    protected IWebsocketRouteBuilderFactory getWebsocketRouteBuilderFactory() {
        return this.websocketRouteBuilderFactory;
    }

    protected IWebsocketRouteHandlerFactory getWebsocketRouteHandlerFactory() {
        return this.websocketRouteHandlerFactory;
    }

    protected IStaticResourceBuilderFactory getStaticResourceBuilderFactory() {
        return this.staticResourceBuilderFactory;
    }

    protected IRouteHandlerMatchFactory getRouteHandlerMatchFactory() {
        return this.routeHandlerMatchFactory;
    }

    protected IStaticResourceFactory getStaticResourceFactory() {
        return this.staticResourceFactory;
    }

    protected Pattern getPattern(String patternStr) {
        Pattern pattern = this.patternCache.get(patternStr);
        if(pattern == null) {
            pattern = Pattern.compile(patternStr);
            this.patternCache.put(patternStr, pattern);
        }
        return pattern;
    }

    @Override
    public Map getRouteParamPatternAliases() {
        return this.routeParamPatternAliases;
    }

    @Override
    public IRoute getRoute(String routeId) {

        for(IRoute route : getGlobalBeforeFiltersRoutes()) {
            if((routeId == null && route.getId() == null) || (routeId != null && routeId.equals(route.getId()))) {
                return route;
            }
        }
        for(IRoute route : getMainRoutes()) {
            if((routeId == null && route.getId() == null) || (routeId != null && routeId.equals(route.getId()))) {
                return route;
            }
        }
        for(IRoute route : getGlobalAfterFiltersRoutes()) {
            if((routeId == null && route.getId() == null) || (routeId != null && routeId.equals(route.getId()))) {
                return route;
            }
        }

        return null;
    }

    protected Map>> getGlobalBeforeFiltersPerPosition() {
        if(this.globalBeforeFiltersPerPosition == null) {
            this.globalBeforeFiltersPerPosition = new TreeMap>>();
        }
        return this.globalBeforeFiltersPerPosition;
    }

    @Override
    public List> getGlobalBeforeFiltersRoutes() {

        if(this.globalBeforeFilters == null) {
            this.globalBeforeFilters = new ArrayList>();

            Collection>> routesLists = getGlobalBeforeFiltersPerPosition().values();
            if(routesLists != null) {
                for(List> routeList : routesLists) {
                    this.globalBeforeFilters.addAll(routeList);
                }
            }
        }

        return this.globalBeforeFilters;
    }

    protected Map>> getGlobalAfterFiltersPerPosition() {
        if(this.globalAfterFiltersPerPosition == null) {
            this.globalAfterFiltersPerPosition = new TreeMap>>();
        }
        return this.globalAfterFiltersPerPosition;
    }

    @Override
    public List> getGlobalAfterFiltersRoutes() {
        if(this.globalAfterFilters == null) {
            this.globalAfterFilters = new ArrayList>();

            Collection>> routesLists = getGlobalAfterFiltersPerPosition().values();
            if(routesLists != null) {
                for(List> routeList : routesLists) {
                    this.globalAfterFilters.addAll(routeList);
                }
            }
        }

        return this.globalAfterFilters;
    }

    @Override
    public List> getMainRoutes() {

        if(this.mainRoutes == null) {
            this.mainRoutes = new ArrayList<>();
        }
        return this.mainRoutes;
    }

    @Override
    public void addRoute(IRoute route) {

        if(route == null ||
           route.getMainHandler() == null ||
           route.getHttpMethods() == null) {
            return;
        }

        validateId(route.getId());

        validatePath(route.getPath());

        List positions = route.getPositions();
        for(int position : positions) {
            if(position < 0) {
                this.globalBeforeFilters = null; // reset cache
                List> routes = getGlobalBeforeFiltersPerPosition().get(position);
                if(routes == null) {
                    routes = new ArrayList>();
                    getGlobalBeforeFiltersPerPosition().put(position, routes);
                }
                routes.add(route);

            } else if(position == 0) {
                // Keep main routes in order they are added.
                getMainRoutes().add(route);
            } else {
                this.globalAfterFilters = null; // reset cache
                List> routes = getGlobalAfterFiltersPerPosition().get(position);
                if(routes == null) {
                    routes = new ArrayList>();
                    getGlobalAfterFiltersPerPosition().put(position, routes);
                }
                routes.add(route);
            }
        }
    }

    protected void validateId(String id) {
        if(id == null) {
            return; //ok
        }

        IRoute sameIdRoute = null;
        for(IRoute route : getGlobalBeforeFiltersRoutes()) {
            if(id.equals(route.getId())) {
                sameIdRoute = route;
                break;
            }
        }
        if(sameIdRoute == null) {
            for(IRoute route : getGlobalAfterFiltersRoutes()) {
                if(id.equals(route.getId())) {
                    sameIdRoute = route;
                    break;
                }
            }
        }
        if(sameIdRoute == null) {
            for(IRoute route : getMainRoutes()) {
                if(id.equals(route.getId())) {
                    sameIdRoute = route;
                    break;
                }
            }
        }

        if(sameIdRoute != null) {
            throw new RuntimeException("A route already use the id '" + id + "' : " + sameIdRoute + ". Ids " +
                                       "must be uniques!");
        }
    }

    /**
     * Validate the path of a route.
     * Throws an exception if not valide.
     */
    protected void validatePath(String path) {
        if(path == null) {
            return;
        }

        Set paramNames = new HashSet();
        String[] pathTokens = path.split("/");
        boolean splatFound = false;
        for(String pathToken : pathTokens) {

            if(StringUtils.isBlank(pathToken)) {
                continue;
            }

            if(pathToken.startsWith("${") || pathToken.startsWith("*{")) {

                if(!pathToken.endsWith("}")) {
                    throw new RuntimeException("A parameter in the path of a route must end with '}'. Incorrect parameter : " +
                                               pathToken);
                }

                if(pathToken.startsWith("*{")) {
                    if(splatFound) {
                        throw new RuntimeException("The path of a route can only contain one " +
                                                   "splat parameter (the one starting with a '*{'). The path is : " +
                                                   path);
                    }

                    if(pathToken.contains(":")) {
                        throw new RuntimeException("A splat parameter can't contain a pattern (so no ':' allowed) : " +
                                                   pathToken);
                    }

                    splatFound = true;
                } else {

                    String token = pathToken.substring(2, pathToken.length() - 1);

                    int posColon = token.indexOf(":");
                    if(posColon > -1) {

                        token = token.substring(posColon + 1);

                        //==========================================
                        // Pattern aliases
                        //==========================================
                        if(token.startsWith("<")) {

                            if(!token.endsWith(">")) {
                                throw new RuntimeException("A parameter with an pattern alias must have a closing '>' : " +
                                                           pathToken);
                            }
                            token = token.substring(1, token.length() - 1);
                            String pattern = getPatternFromAlias(token);
                            if(pattern == null) {
                                throw new RuntimeException("Pattern not found using alias : " + token);
                            }
                        }
                    }
                }

                String paramName = pathToken.substring(2, pathToken.length() - 1);
                if(!StringUtils.isBlank(paramName) && paramNames.contains(paramName)) {
                    throw new RuntimeException("Two parameters with the same name, '" + paramName + "', in route with path : " +
                                               path);
                }
                paramNames.add(paramName);
            }
        }
    }

    @Override
    public void removeAllRoutes() {
        getGlobalBeforeFiltersPerPosition().clear();
        this.globalBeforeFilters = null; // reset cache
        getMainRoutes().clear();
        getGlobalAfterFiltersPerPosition().clear();
        this.globalAfterFilters = null; // reset cache
    }

    @Override
    public void removeRoute(String routeId) {

        if(routeId == null) {
            return;
        }

        Collection>> routeLists = getGlobalBeforeFiltersPerPosition().values();
        for(List> routes : routeLists) {
            for(int i = routes.size() - 1; i >= 0; i--) {
                IRoute route = routes.get(i);
                if(route != null && routeId.equals(route.getId())) {
                    routes.remove(i);
                }
            }
        }
        this.globalBeforeFilters = null; // reset cache

        routeLists = getGlobalAfterFiltersPerPosition().values();
        for(List> routes : routeLists) {
            for(int i = routes.size() - 1; i >= 0; i--) {
                IRoute route = routes.get(i);
                if(route != null && routeId.equals(route.getId())) {
                    routes.remove(i);
                }
            }
        }
        this.globalAfterFilters = null; // reset cache

        List> routes = getMainRoutes();
        for(int i = routes.size() - 1; i >= 0; i--) {
            IRoute route = routes.get(i);
            if(route != null && routeId.equals(route.getId())) {
                routes.remove(i);
            }
        }
    }

    @Override
    public IRoutingResult route(R requestContext) {
        return route(requestContext, requestContext.request().getFullUrl(), RoutingType.FOUND);
    }

    @Override
    public IRoutingResult route(R requestContext,
                                   RoutingType routingType) {
        return route(requestContext, requestContext.request().getFullUrl(), routingType);
    }

    public IRoutingResult route(R requestContext,
                                   String fullUrl,
                                   RoutingType routingType) {
        try {

            URL url = new URL(fullUrl);
            HttpMethod httpMethod = requestContext.request().getHttpMethod();
            List acceptedContentTypes = requestContext.request().getHeader(HttpHeaders.ACCEPT);
            if(acceptedContentTypes == null) {
                acceptedContentTypes = new ArrayList();
            }

            List> routeHandlerMatches = new ArrayList>();

            //==========================================
            // First check if there is a main handler for this request.
            // We only keep the first match here!
            //==========================================
            List> mainRouteHandlerMatches = null;
            for(IRoute route : getMainRoutes()) {

                List> routeHandlerMatch = createRegularHandlerMatches(routingType,
                                                                                            route,
                                                                                            httpMethod,
                                                                                            acceptedContentTypes,
                                                                                            url,
                                                                                            0);
                if(routeHandlerMatch != null && routeHandlerMatch.size() > 0) {
                    mainRouteHandlerMatches = routeHandlerMatch;
                    break;
                }
            }

            //==========================================
            // No main matches? Then no "before" or "after"
            // filters either!
            //==========================================
            if(mainRouteHandlerMatches != null) {

                //==========================================
                // First, the global "before" filters.
                //==========================================
                for(IRoute route : getGlobalBeforeFiltersRoutes()) {

                    if(!isRoutingTypeMatch(routingType, route)) {
                        continue;
                    }

                    List> beforeRouteHandlerMatches = createRegularHandlerMatches(routingType,
                                                                                                        route,
                                                                                                        httpMethod,
                                                                                                        acceptedContentTypes,
                                                                                                        url,
                                                                                                        -1);
                    if(beforeRouteHandlerMatches != null) {
                        routeHandlerMatches.addAll(beforeRouteHandlerMatches);
                    }
                }

                //==========================================
                // The main handler match.
                //==========================================
                routeHandlerMatches.addAll(mainRouteHandlerMatches);

                //==========================================
                // Finally, the global "after" filters.
                //==========================================
                for(IRoute route : getGlobalAfterFiltersRoutes()) {

                    if(!isRoutingTypeMatch(routingType, route)) {
                        continue;
                    }

                    List> afterRouteHandlerMatches =
                            createRegularHandlerMatches(routingType,
                                                        route,
                                                        httpMethod,
                                                        acceptedContentTypes,
                                                        url,
                                                        1);
                    if(afterRouteHandlerMatches != null) {
                        routeHandlerMatches.addAll(afterRouteHandlerMatches);
                    }
                }
            }

            if(routeHandlerMatches.size() == 0) {
                return null;
            }

            IRoutingResult routingResult = createRoutingResult(routeHandlerMatches);
            return routingResult;
        } catch(Exception ex) {
            throw SpincastStatics.runtimize(ex);
        }
    }

    protected boolean isRoutingTypeMatch(RoutingType routingType, IRoute route) {

        Objects.requireNonNull(routingType, "routingType can't be NULL");
        Objects.requireNonNull(route, "route can't be NULL");

        return route.getRoutingTypes() != null && route.getRoutingTypes().contains(routingType);
    }

    protected IRoutingResult createRoutingResult(List> routeHandlerMatches) {
        IRoutingResult routingResult = new RoutingResult(routeHandlerMatches);
        return routingResult;
    }

    /**
     * Get the matches (filters and main handle) if the route matches the URL and
     * HTTP method, or returns NULL otherwise.
     */
    protected List> createRegularHandlerMatches(RoutingType routingType,
                                                                      IRoute route,
                                                                      HttpMethod httpMethod,
                                                                      List acceptedContentTypes,
                                                                      URL url,
                                                                      int position) {

        if(!isRoutingTypeMatch(routingType, route)) {
            return null;
        }

        //==========================================
        // Validate the HTTP method.
        //==========================================
        if(!isRouteMatchHttpMethod(route, httpMethod)) {
            return null;
        }

        //==========================================
        // Validate the Accept content-types.
        //==========================================
        if(!isRouteMatchAcceptedContentType(route, acceptedContentTypes)) {
            return null;
        }

        //==========================================
        // Validate the route path.
        //==========================================
        String routePath = route.getPath();
        Map matchingParams = validatePath(routePath, url);
        if(matchingParams == null) {
            return null;
        }

        //==========================================
        // Match!
        //==========================================
        List> matches = new ArrayList>();
        IRouteHandlerMatch routeHandlerMatch = getRouteHandlerMatchFactory().create(route,
                                                                                       route.getMainHandler(),
                                                                                       matchingParams,
                                                                                       position);
        matches.add(routeHandlerMatch);

        //==========================================
        // If the main handler has inline "before" filters, 
        // we add them with the same configurations as it.
        //==========================================
        List> beforeFilters = route.getBeforeFilters();
        if(beforeFilters != null) {
            for(IHandler beforeFilter : beforeFilters) {
                if(beforeFilter != null) {
                    IRouteHandlerMatch beforeMethodRouteHandlerMatch =
                            createHandlerMatchForBeforeOrAfterFilter(routeHandlerMatch,
                                                                     beforeFilter,
                                                                     -1);
                    matches.add(0, beforeMethodRouteHandlerMatch);
                }
            }
        }

        //==========================================
        // If the main handler has inline "after" filters, 
        // we add them with the same configurations as it.
        //==========================================
        List> afterFilters = route.getAfterFilters();
        if(afterFilters != null) {

            for(IHandler afterFilter : afterFilters) {
                if(afterFilter != null) {
                    IRouteHandlerMatch afterMethodRouteHandlerMatch =
                            createHandlerMatchForBeforeOrAfterFilter(routeHandlerMatch,
                                                                     afterFilter,
                                                                     1);
                    matches.add(afterMethodRouteHandlerMatch);
                }
            }
        }

        return matches;
    }

    protected boolean isRouteMatchAcceptedContentType(IRoute route,
                                                      List requestContentTypes) {

        if(requestContentTypes == null || requestContentTypes.size() == 0) {
            return true;
        }

        Set routeContentTypes = route.getAcceptedContentTypes();
        if(routeContentTypes == null || routeContentTypes.size() == 0) {
            return true;
        }

        for(String contentType : requestContentTypes) {
            if(contentType != null && routeContentTypes.contains(contentType.toLowerCase())) {
                return true;
            }
        }

        return false;
    }

    /**
     * Creates an handler match with no matching params.
     */
    protected IRouteHandlerMatch createNoMatchingParamsHandlerMatch(IRoute route,
                                                                       String id,
                                                                       IHandler handler,
                                                                       int position) {
        return getRouteHandlerMatchFactory().create(route,
                                                    handler,
                                                    null,
                                                    position);
    }

    /**
     * Creates a new match for a "before" or "after" handler specific
     * to a route. THis measn keeping the same informations as the
     * main handler.
     */
    protected IRouteHandlerMatch createHandlerMatchForBeforeOrAfterFilter(IRouteHandlerMatch mainRouteHandlerMatch,
                                                                             IHandler beforeOrAfterMethod,
                                                                             int position) {

        IRouteHandlerMatch routeHandlerMatch = getRouteHandlerMatchFactory().create(mainRouteHandlerMatch.getSourceRoute(),
                                                                                       beforeOrAfterMethod,
                                                                                       mainRouteHandlerMatch.getParameters(),
                                                                                       position);
        return routeHandlerMatch;
    }

    /**
     * Validate if a route matches the given HTTP method.
     */
    protected boolean isRouteMatchHttpMethod(IRoute route, HttpMethod httpMethod) {
        return route.getHttpMethods().contains(httpMethod);
    }

    /**
     * Validate if url matches the path of the route and if so, returns the
     * parsed parameters, if any.
     * 
     * Returns NULL if there is no match.
     */
    protected Map validatePath(String routePath, URL url) {

        String urlPath = url.getPath();
        String urlPathSlashesStriped = StringUtils.strip(urlPath, "/ ");

        String routePathSlashesStriped = StringUtils.strip(routePath, "/ ");

        boolean routesAreCaseSensitive = getSpincastConfig().isRoutesCaseSensitive();

        //==========================================
        // URL and route path are the same : match!
        //==========================================
        if(routesAreCaseSensitive) {
            if(urlPathSlashesStriped.equals(routePathSlashesStriped)) {
                return new HashMap();
            }
        } else {
            if(urlPathSlashesStriped.equalsIgnoreCase(routePathSlashesStriped)) {
                return new HashMap();
            }
        }

        //==========================================
        // If the route path doesn't contain any "${" or "*{" there no
        // need to validate furthermore...
        //==========================================
        int pos = routePathSlashesStriped.indexOf("*{");
        boolean hasSplat = true;
        if(pos < 0) {
            hasSplat = false;
            pos = routePathSlashesStriped.indexOf("${");
            if(pos < 0) {
                return null;
            }
        }

        String[] routePathTokens = routePathSlashesStriped.split("/");
        if(routePathTokens.length == 1 && routePathTokens[0].equals("")) {
            routePathTokens = new String[0];
        }

        String[] urlPathTokens = urlPathSlashesStriped.split("/");
        if(urlPathTokens.length == 1 && urlPathTokens[0].equals("")) {
            urlPathTokens = new String[0];
        }

        //==========================================
        // The Url should have at least as many tokens
        // than the route path but can also have more in case
        // a splat "*{" token is used in the route path!
        //==========================================
        if(!hasSplat && urlPathTokens.length > routePathTokens.length) {
            return null;
        }

        Map params = new HashMap();

        int routePathTokenPos = 0;
        int urlTokenPos = 0;
        for(; routePathTokenPos < routePathTokens.length; routePathTokenPos++) {

            String routePathToken = routePathTokens[routePathTokenPos];

            String urlPathToken = "";
            if(!(routePathToken.startsWith("*{") && urlTokenPos >= urlPathTokens.length)) {

                if(urlTokenPos + 1 > urlPathTokens.length) {
                    return null;
                }

                urlPathToken = urlPathTokens[urlTokenPos];
            }

            //==========================================
            // The position of the url's tokens may increase
            // faster than the one of the route path because 
            // of splat tokens!
            //==========================================
            urlTokenPos++;

            //==========================================
            // For a token that doesn't start with "${" or "*{", the
            // same value must be in url and in the route path.
            //==========================================
            if(!routePathToken.startsWith("${") && !routePathToken.startsWith("*{")) {
                if(routesAreCaseSensitive) {
                    if(!urlPathToken.equals(routePathToken)) {
                        return null;
                    }
                } else {
                    if(!urlPathToken.equalsIgnoreCase(routePathToken)) {
                        return null;
                    }
                }
            } else {

                String paramName = routePathToken.substring(2, routePathToken.length() - 1);
                String paramValue;
                try {
                    paramValue = URLDecoder.decode(urlPathToken, "UTF-8");
                } catch(Exception ex) {
                    throw SpincastStatics.runtimize(ex);
                }

                //==========================================
                // If there a pattern?
                //==========================================
                String pattern = null;
                if(routePathToken.startsWith("${")) {

                    int posComma = paramName.indexOf(":");
                    if(posComma > -1) {
                        pattern = paramName.substring(posComma + 1);
                        paramName = paramName.substring(0, posComma);

                        if(StringUtils.isBlank(pattern)) {
                            pattern = null;
                        } else {

                            //==========================================
                            // Is it a pattern alias?
                            //==========================================
                            if(pattern.startsWith("<") && pattern.endsWith(">")) {
                                pattern = getPatternFromAlias(pattern.substring(1, pattern.length() - 1));
                            }
                        }
                    }

                    if(pattern != null && !getPattern(pattern).matcher(urlPathToken).matches()) {
                        this.logger.debug("Url token '" + urlPathToken + "' doesn't match pattern '" + pattern + "'.");
                        return null;
                    }

                }
                //==========================================
                // Splat param : the value can contain multiple
                // tokens from the url.
                //==========================================
                else if(routePathToken.startsWith("*{")) {

                    int nbrTokensInSplat = urlPathTokens.length - routePathTokens.length + 1;
                    if(nbrTokensInSplat > 1) {

                        StringBuilder builder = new StringBuilder(paramValue);
                        for(int i = 1; i < nbrTokensInSplat; i++) {
                            builder.append("/").append(urlPathTokens[urlTokenPos]);
                            urlTokenPos++;
                        }
                        paramValue = builder.toString();
                    }
                }

                //==========================================
                // We do not collect the parameters without names.
                //==========================================
                if(!StringUtils.isBlank(paramName)) {
                    params.put(paramName, paramValue);
                }
            }
        }

        return params;
    }

    /**
     * Get a path pattern from its alias.
     * @return the pattern or NULL if not found.
     */
    protected String getPatternFromAlias(String alias) {

        if(alias == null) {
            return null;
        }

        for(Entry entry : getRouteParamPatternAliases().entrySet()) {
            if(alias.equals(entry.getKey())) {
                return entry.getValue();
            }
        }

        return null;
    }

    @Override
    public void addRouteParamPatternAlias(String alias, String pattern) {

        if(StringUtils.isBlank(alias)) {
            throw new RuntimeException("The alias can't be empty.");
        }
        if(StringUtils.isBlank(pattern)) {
            throw new RuntimeException("The pattern can't be empty.");
        }

        getRouteParamPatternAliases().put(alias, pattern);
    }

    @Override
    public IRouteBuilder GET(String path) {

        IRouteBuilder builder = getRouteBuilderFactory().create(this);
        builder = builder.GET();
        builder = builder.path(path);

        return builder;
    }

    @Override
    public IRouteBuilder POST(String path) {
        IRouteBuilder builder = getRouteBuilderFactory().create(this);
        builder = builder.POST();
        builder = builder.path(path);

        return builder;
    }

    @Override
    public IRouteBuilder PUT(String path) {
        IRouteBuilder builder = getRouteBuilderFactory().create(this);
        builder = builder.PUT();
        builder = builder.path(path);

        return builder;
    }

    @Override
    public IRouteBuilder DELETE(String path) {
        IRouteBuilder builder = getRouteBuilderFactory().create(this);
        builder = builder.DELETE();
        builder = builder.path(path);

        return builder;
    }

    @Override
    public IRouteBuilder OPTIONS(String path) {
        IRouteBuilder builder = getRouteBuilderFactory().create(this);
        builder = builder.OPTIONS();
        builder = builder.path(path);

        return builder;
    }

    @Override
    public IRouteBuilder TRACE(String path) {
        IRouteBuilder builder = getRouteBuilderFactory().create(this);
        builder = builder.TRACE();
        builder = builder.path(path);

        return builder;
    }

    @Override
    public IRouteBuilder HEAD(String path) {
        IRouteBuilder builder = getRouteBuilderFactory().create(this);
        builder = builder.HEAD();
        builder = builder.path(path);

        return builder;
    }

    @Override
    public IRouteBuilder PATCH(String path) {
        IRouteBuilder builder = getRouteBuilderFactory().create(this);
        builder = builder.PATCH();
        builder = builder.path(path);

        return builder;
    }

    @Override
    public IRouteBuilder ALL(String path) {
        IRouteBuilder builder = getRouteBuilderFactory().create(this);
        builder = builder.ALL();
        builder = builder.path(path);

        return builder;
    }

    @Override
    public IRouteBuilder SOME(String path, HttpMethod... httpMethods) {

        if(httpMethods.length == 0) {
            throw new RuntimeException("Using SOME(...), you have to specify at least one HTTP method.");
        }

        return SOME(path, Sets.newHashSet(httpMethods));
    }

    @Override
    public IRouteBuilder SOME(String path, Set httpMethods) {

        if(httpMethods == null || httpMethods.size() == 0) {
            throw new RuntimeException("Using SOME(...), you have to specify at least one HTTP method.");
        }

        IRouteBuilder builder = getRouteBuilderFactory().create(this);
        builder = builder.SOME(httpMethods);
        builder = builder.path(path);

        return builder;
    }

    @Override
    public void before(IHandler handler) {
        before(DEFAULT_ROUTE_PATH, handler);
    }

    @Override
    public void before(String path, IHandler handler) {

        IRouteBuilder builder = getRouteBuilderFactory().create(this);
        builder = builder.ALL();
        builder = builder.pos(-1);
        builder = builder.path(path);
        builder = addFilterDefaultRoutingTypes(builder);

        builder.save(handler);
    }

    @Override
    public void after(IHandler handler) {
        after(DEFAULT_ROUTE_PATH, handler);
    }

    @Override
    public void after(String path, IHandler handler) {
        IRouteBuilder builder = getRouteBuilderFactory().create(this);
        builder = builder.ALL();
        builder = builder.pos(1);
        builder = builder.path(path);
        builder = addFilterDefaultRoutingTypes(builder);

        builder.save(handler);
    }

    @Override
    public void beforeAndAfter(IHandler handler) {
        beforeAndAfter(DEFAULT_ROUTE_PATH, handler);
    }

    @Override
    public void beforeAndAfter(String path, IHandler handler) {
        before(path, handler);
        after(path, handler);
    }

    protected IRouteBuilder addFilterDefaultRoutingTypes(IRouteBuilder builder) {

        Set defaultRoutingTypes = getSpincastRouterConfig().getFilterDefaultRoutingTypes();
        for(RoutingType routingType : defaultRoutingTypes) {
            if(routingType == RoutingType.FOUND) {
                builder.found();
            } else if(routingType == RoutingType.NOT_FOUND) {
                builder.notFound();
            } else if(routingType == RoutingType.EXCEPTION) {
                builder.exception();
            } else {
                throw new RuntimeException("Not managed : " + routingType);
            }
        }

        return builder;
    }

    @Override
    public void exception(IHandler handler) {
        exception(DEFAULT_ROUTE_PATH, handler);
    }

    @Override
    public void exception(String path, IHandler handler) {
        ALL(path).exception().save(handler);
    }

    @Override
    public void notFound(IHandler handler) {
        notFound(DEFAULT_ROUTE_PATH, handler);
    }

    @Override
    public void notFound(String path, IHandler handler) {
        ALL(path).notFound().save(handler);
    }

    @Override
    public void cors() {
        cors(DEFAULT_ROUTE_PATH);
    }

    @Override
    public void cors(Set allowedOrigins) {
        cors(DEFAULT_ROUTE_PATH,
             allowedOrigins);
    }

    @Override
    public void cors(Set allowedOrigins, Set extraHeadersAllowedToBeRead) {
        cors(DEFAULT_ROUTE_PATH,
             allowedOrigins,
             extraHeadersAllowedToBeRead);
    }

    @Override
    public void cors(Set allowedOrigins, Set extraHeadersAllowedToBeRead,
                     Set extraHeadersAllowedToBeSent) {
        cors(DEFAULT_ROUTE_PATH,
             allowedOrigins,
             extraHeadersAllowedToBeRead,
             extraHeadersAllowedToBeSent);
    }

    @Override
    public void cors(Set allowedOrigins, Set extraHeadersAllowedToBeRead, Set extraHeadersAllowedToBeSent,
                     boolean allowCookies) {
        cors(DEFAULT_ROUTE_PATH,
             allowedOrigins,
             extraHeadersAllowedToBeRead,
             extraHeadersAllowedToBeSent,
             allowCookies);
    }

    @Override
    public void cors(Set allowedOrigins, Set extraHeadersAllowedToBeRead, Set extraHeadersAllowedToBeSent,
                     boolean allowCookies, Set allowedMethods) {
        cors(DEFAULT_ROUTE_PATH,
             allowedOrigins,
             extraHeadersAllowedToBeRead,
             extraHeadersAllowedToBeSent,
             allowCookies,
             allowedMethods);
    }

    @Override
    public void cors(Set allowedOrigins, Set extraHeadersAllowedToBeRead, Set extraHeadersAllowedToBeSent,
                     boolean allowCookies, Set allowedMethods, int maxAgeInSeconds) {
        cors(DEFAULT_ROUTE_PATH,
             allowedOrigins,
             extraHeadersAllowedToBeRead,
             extraHeadersAllowedToBeSent,
             allowCookies,
             allowedMethods,
             maxAgeInSeconds);
    }

    @Override
    public void cors(String path) {

        ALL(path).pos(getSpincastRouterConfig().getCorsFilterPosition())
                 .found().notFound().save(new IHandler() {

                     @Override
                     public void handle(R context) {
                         getSpincastFilters().cors(context);
                     }
                 });
    }

    @Override
    public void cors(String path,
                     final Set allowedOrigins) {

        ALL(path).pos(getSpincastRouterConfig().getCorsFilterPosition())
                 .found().notFound().save(new IHandler() {

                     @Override
                     public void handle(R context) {
                         getSpincastFilters().cors(context,
                                                   allowedOrigins);
                     }
                 });
    }

    @Override
    public void cors(String path,
                     final Set allowedOrigins,
                     final Set extraHeadersAllowedToBeRead) {

        ALL(path).pos(getSpincastRouterConfig().getCorsFilterPosition())
                 .found().notFound().save(new IHandler() {

                     @Override
                     public void handle(R context) {
                         getSpincastFilters().cors(context,
                                                   allowedOrigins,
                                                   extraHeadersAllowedToBeRead);
                     }
                 });
    }

    @Override
    public void cors(String path,
                     final Set allowedOrigins,
                     final Set extraHeadersAllowedToBeRead,
                     final Set extraHeadersAllowedToBeSent) {

        ALL(path).pos(getSpincastRouterConfig().getCorsFilterPosition())
                 .found().notFound().save(new IHandler() {

                     @Override
                     public void handle(R context) {
                         getSpincastFilters().cors(context,
                                                   allowedOrigins,
                                                   extraHeadersAllowedToBeRead,
                                                   extraHeadersAllowedToBeSent);
                     }
                 });
    }

    @Override
    public void cors(String path,
                     final Set allowedOrigins,
                     final Set extraHeadersAllowedToBeRead,
                     final Set extraHeadersAllowedToBeSent,
                     final boolean allowCookies) {

        ALL(path).pos(getSpincastRouterConfig().getCorsFilterPosition())
                 .found().notFound().save(new IHandler() {

                     @Override
                     public void handle(R context) {
                         getSpincastFilters().cors(context,
                                                   allowedOrigins,
                                                   extraHeadersAllowedToBeRead,
                                                   extraHeadersAllowedToBeSent,
                                                   allowCookies);
                     }
                 });
    }

    @Override
    public void cors(String path,
                     final Set allowedOrigins,
                     final Set extraHeadersAllowedToBeRead,
                     final Set extraHeadersAllowedToBeSent,
                     final boolean allowCookies,
                     final Set allowedMethods) {

        ALL(path).pos(getSpincastRouterConfig().getCorsFilterPosition())
                 .found().notFound().save(new IHandler() {

                     @Override
                     public void handle(R context) {
                         getSpincastFilters().cors(context,
                                                   allowedOrigins,
                                                   extraHeadersAllowedToBeRead,
                                                   extraHeadersAllowedToBeSent,
                                                   allowCookies,
                                                   allowedMethods);
                     }
                 });
    }

    @Override
    public void cors(String path,
                     final Set allowedOrigins,
                     final Set extraHeadersAllowedToBeRead,
                     final Set extraHeadersAllowedToBeSent,
                     final boolean allowCookies,
                     final Set allowedMethods,
                     final int maxAgeInSeconds) {

        ALL(path).pos(getSpincastRouterConfig().getCorsFilterPosition())
                 .found().notFound().save(new IHandler() {

                     @Override
                     public void handle(R context) {
                         getSpincastFilters().cors(context,
                                                   allowedOrigins,
                                                   extraHeadersAllowedToBeRead,
                                                   extraHeadersAllowedToBeSent,
                                                   allowCookies,
                                                   allowedMethods,
                                                   maxAgeInSeconds);
                     }
                 });
    }

    @Override
    public IStaticResourceBuilder file(String url) {
        IStaticResourceBuilder builder = getStaticResourceBuilderFactory().create(this, false);
        builder = builder.url(url);

        return builder;
    }

    @Override
    public IStaticResourceBuilder dir(String url) {
        IStaticResourceBuilder builder = getStaticResourceBuilderFactory().create(this, true);
        builder = builder.url(url);

        return builder;
    }

    @Override
    public void addStaticResource(final IStaticResource staticResource) {

        if(staticResource.getUrlPath() == null) {
            throw new RuntimeException("The URL to the resource must be specified!");
        }

        if(staticResource.getResourcePath() == null) {
            throw new RuntimeException("A classpath or a file system path must be specified!");
        }

        if(staticResource.isClasspath() && staticResource.getGenerator() != null) {
            throw new RuntimeException("A resource generator can only be specified when a file system " +
                                       "path is used, not a classpath path.");
        }

        String urlPath = staticResource.getUrlPath();

        //==========================================
        // A file resource can't contains any dynamic parameters.
        // The serving handler of the HTTP server may not understand them.
        //
        // For a dir resource, we still allow them in the url path, but those 
        // dynamic parameters must all
        // be *after* the regular tokens. This way, we can remove 
        // the dynamic part and the result is the prefix path to be used
        // by the server.
        //==========================================
        boolean splatParamFound = false;
        StringBuilder urlPathForServerBuilder = new StringBuilder("");
        String[] tokens = staticResource.getUrlPath().split("/");
        for(String token : tokens) {
            token = token.trim();
            if(staticResource.isFileResource()) {
                if(token.startsWith("${") || token.startsWith("*{")) {
                    throw new RuntimeException("A file resource path can't contain dynamic parameters. Use 'dir()' instead or " +
                                               "a regular route which won't be considered as a static resource.");
                }
            } else {
                if(StringUtils.isBlank(token)) {
                    continue;
                }

                if(token.startsWith("${")) {
                    throw new RuntimeException("A dir static resource path can't contains any standard dynamic parameter. It can only contain " +
                                               "a splat parameter, at the very end of the path : " + token);
                } else if(token.startsWith("*{")) {
                    splatParamFound = true;
                } else {
                    if(splatParamFound) {
                        throw new RuntimeException("A dir resource path can contain a splat parameter, only at the very end of the path! " +
                                                   "For example, this is invalid as a path : '/one/*{param1}/two', but this is valid : '/one/two/*{param1}'.");
                    } else {
                        urlPathForServerBuilder.append("/").append(token);
                    }
                }
            }
        }

        //==========================================
        // We remove the splat parameters from the
        // url path for the server.
        //==========================================
        String urlPathPrefixWithoutSplatParameter = staticResource.getUrlPath();
        if(splatParamFound) {

            urlPathPrefixWithoutSplatParameter = urlPathForServerBuilder.toString();
            if(staticResource.isDirResource()) {
                urlPathPrefixWithoutSplatParameter = urlPathForServerBuilder.toString();
                if(StringUtils.isBlank(urlPathPrefixWithoutSplatParameter)) {
                    urlPathPrefixWithoutSplatParameter = "/";
                }
            }

            IStaticResource staticResourceNoDynParams =
                    getStaticResourceFactory().create(staticResource.getStaticResourceType(),
                                                      urlPathPrefixWithoutSplatParameter,
                                                      staticResource.getResourcePath(),
                                                      staticResource.getGenerator(),
                                                      staticResource.getCorsConfig());
            getServer().addStaticResourceToServe(staticResourceNoDynParams);
        } else {
            getServer().addStaticResourceToServe(staticResource);
        }

        //==========================================
        // If the resource is dynamic, add its generator 
        // as a route for the same path! 
        // We also add an "after" filter which will try to
        // automatically save the generated resource. The headers
        // shouln't have been sent for that to work!
        //==========================================
        IRoute route = null;
        IHandler saveResourceFilter = null;
        if(staticResource.getGenerator() != null) {

            if(staticResource.isFileResource()) {
                saveResourceFilter = new IHandler() {

                    @Override
                    public void handle(R context) {
                        getSpincastFilters().saveGeneratedResource(context, staticResource.getResourcePath());
                    }
                };
            } else {

                //==========================================
                // We make the route listen on anything under
                // the specified path! The path may already contain
                // a splat param though!
                //==========================================
                if(!splatParamFound) {
                    urlPath = StringUtils.stripEnd(urlPath, "/") + DEFAULT_ROUTE_PATH;
                }

                final String urlPathPrefixWithoutDynamicParametersFinal = urlPathPrefixWithoutSplatParameter;
                saveResourceFilter = new IHandler() {

                    @Override
                    public void handle(R context) {

                        String urlPathPrefix = StringUtils.stripStart(urlPathPrefixWithoutDynamicParametersFinal, "/");

                        String requestPath = context.request().getRequestPath();
                        requestPath = StringUtils.stripStart(requestPath, "/");

                        if(!requestPath.startsWith(urlPathPrefix)) {
                            throw new RuntimeException("The requestPath '" + requestPath +
                                                       "' should starts with the urlPathPrefix '" + urlPathPrefix +
                                                       "' here!");
                        }

                        requestPath = requestPath.substring(urlPathPrefix.length());

                        // Make sure the path of the resource to generate is safe!
                        String resourceToGeneratePath;
                        try {
                            resourceToGeneratePath =
                                    new File(staticResource.getResourcePath() + "/" + requestPath).getCanonicalFile()
                                                                                                  .getAbsolutePath();
                            String resourcesRoot =
                                    new File(staticResource.getResourcePath()).getCanonicalFile().getAbsolutePath();

                            if(!resourceToGeneratePath.startsWith(resourcesRoot)) {
                                throw new RuntimeException("The requestPath '" + resourceToGeneratePath +
                                                           "' should be inside the root resources folder : " + resourcesRoot);
                            }
                        } catch(Exception ex) {
                            throw SpincastStatics.runtimize(ex);
                        }

                        getSpincastFilters().saveGeneratedResource(context, resourceToGeneratePath);
                    }
                };
            }

            route = getRouteFactory().createRoute(null,
                                                  Sets.newHashSet(HttpMethod.GET),
                                                  urlPath,
                                                  Sets.newHashSet(RoutingType.FOUND),
                                                  null,
                                                  staticResource.getGenerator(),
                                                  saveResourceFilter != null ? Arrays.asList(saveResourceFilter) : null,
                                                  Sets.newHashSet(0),
                                                  null);

            addRoute(route);
        }
    }

    @Override
    public void httpAuth(String pathPrefix, String realmName) {

        if(StringUtils.isBlank(realmName)) {
            throw new RuntimeException("The realm name can't be empty");
        }
        if(StringUtils.isBlank(pathPrefix)) {
            pathPrefix = "/";
        } else if(!pathPrefix.startsWith("/")) {
            pathPrefix = "/" + pathPrefix;
        }

        String[] tokens = pathPrefix.split("/");
        for(String token : tokens) {
            token = token.trim();
            if(token.startsWith("${") || token.startsWith("*{")) {
                throw new RuntimeException("The path prefix for an HTTP authenticated section can't contain " +
                                           "any dynamic parameters: " + token);
            }
        }

        getServer().createHttpAuthenticationRealm(pathPrefix, realmName);
    }

    @Override
    public IWebsocketRouteBuilder websocket(String path) {

        IWebsocketRouteBuilder builder = getWebsocketRouteBuilderFactory().create(this);
        builder = builder.path(path);

        return builder;
    }

    @Override
    public void addWebsocketRoute(IWebsocketRoute websocketRoute) {

        //==========================================
        // We create an HTTP route from the Websocket route
        // informations: this allows the inital request to
        // be routed exactly as a standard route and to have "before"
        // filters applied.
        //==========================================
        IRoute httpRoute = createHttpRouteFromWebsocketRoute(websocketRoute);
        addRoute(httpRoute);
    }

    protected IRoute createHttpRouteFromWebsocketRoute(final IWebsocketRoute websocketRoute) {

        //==========================================
        // We create the "main" route handler for this
        // route: its job will be to convert the HTTP request 
        // to a Websocket connection.
        //==========================================
        final IHandler routeHandler = getWebsocketRouteHandlerFactory().createWebsocketRouteHandler(websocketRoute);

        IRoute httpRoute = new IRoute() {

            @Override
            public String getId() {
                return websocketRoute.getId();
            }

            @Override
            public String getPath() {
                return websocketRoute.getPath();
            }

            @Override
            public Set getHttpMethods() {

                //==========================================
                // Websocket connection request only valid using a 
                // GET method.
                //==========================================
                return Sets.newHashSet(HttpMethod.GET);
            }

            @Override
            public Set getAcceptedContentTypes() {

                //==========================================
                // Not interesting for a Websocket connection.
                //==========================================
                return null;
            }

            @Override
            public Set getRoutingTypes() {
                return Sets.newHashSet(RoutingType.FOUND);
            }

            @Override
            public IHandler getMainHandler() {

                //==========================================
                // The Websocket route hander we just created...
                //==========================================
                return routeHandler;
            }

            @Override
            public List> getBeforeFilters() {
                return websocketRoute.getBeforeFilters();
            }

            @Override
            public List> getAfterFilters() {
                //==========================================
                // No "after" filter for a Websocket route:
                // if the Websocket connection is established, 
                // the HTTP request is not anymore.
                //==========================================
                return null;
            }

            @Override
            public List getPositions() {

                //==========================================
                // Websocket routes can't be used as filters.
                //==========================================
                return Lists.newArrayList(0);
            }
        };

        return httpRoute;
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy