/*
 * Copyright 2019 Red Hat, Inc.
 *
 *  All rights reserved. This program and the accompanying materials
 *  are made available under the terms of the Eclipse Public License v1.0
 *  and Apache License v2.0 which accompanies this distribution.
 *
 *  The Eclipse Public License is available at
 *  http://www.eclipse.org/legal/epl-v10.html
 *
 *  The Apache License v2.0 is available at
 *  http://www.opensource.org/licenses/apache2.0.php
 *
 *  You may elect to redistribute this code under either of these licenses.
 */
package io.vertx.ext.web.impl;

import io.vertx.core.Handler;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.net.impl.URIDecoder;
import io.vertx.ext.web.MIMEHeader;
import io.vertx.ext.web.RoutingContext;

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

This class encapsulates the route state, all mutations are atomic and return a new state with the mutation.

This class is thread-safe

Author:Paulo Lopes
/** * This class encapsulates the route state, all mutations are atomic and return a new state with the mutation. * <p> * This class is thread-safe * * @author <a href="http://pmlopes@gmail.com">Paulo Lopes</a> */
final class RouteState { private final RouteImpl route; private final String path; private final String name; private final int order; private final boolean enabled; private final Set<HttpMethod> methods; private final Set<MIMEHeader> consumes; private final boolean emptyBodyPermittedWithConsumes; private final Set<MIMEHeader> produces; private final List<Handler<RoutingContext>> contextHandlers; private final List<Handler<RoutingContext>> failureHandlers; private final boolean added; private final Pattern pattern; private final List<String> groups; private final boolean useNormalizedPath; private final Set<String> namedGroupsInRegex; private final Pattern virtualHostPattern; private final boolean pathEndsWithSlash; private final boolean exclusive; private final boolean exactPath; private RouteState(RouteImpl route, String path, String name, int order, boolean enabled, Set<HttpMethod> methods, Set<MIMEHeader> consumes, boolean emptyBodyPermittedWithConsumes, Set<MIMEHeader> produces, List<Handler<RoutingContext>> contextHandlers, List<Handler<RoutingContext>> failureHandlers, boolean added, Pattern pattern, List<String> groups, boolean useNormalizedPath, Set<String> namedGroupsInRegex, Pattern virtualHostPattern, boolean pathEndsWithSlash, boolean exclusive, boolean exactPath) { this.route = route; this.path = path; this.name = name; this.order = order; this.enabled = enabled; this.methods = methods; this.consumes = consumes; this.emptyBodyPermittedWithConsumes = emptyBodyPermittedWithConsumes; this.produces = produces; this.contextHandlers = contextHandlers; this.failureHandlers = failureHandlers; this.added = added; this.pattern = pattern; this.groups = groups; this.useNormalizedPath = useNormalizedPath; this.namedGroupsInRegex = namedGroupsInRegex; this.virtualHostPattern = virtualHostPattern; this.pathEndsWithSlash = pathEndsWithSlash; this.exclusive = exclusive; this.exactPath = exactPath; } RouteState(RouteImpl route, int order) { this( route, null, null, order, true, null, null, false, null, null, null, false, null, null, true, null, null, false, false, false); } public RouteImpl getRoute() { return route; } public RouterImpl getRouter() { return route.router(); } public String getPath() { return path; } RouteState setPath(String path) { return new RouteState( this.route, path, this.name, this.order, this.enabled, this.methods, this.consumes, this.emptyBodyPermittedWithConsumes, this.produces, this.contextHandlers, this.failureHandlers, this.added, this.pattern, this.groups, this.useNormalizedPath, this.namedGroupsInRegex, this.virtualHostPattern, this.pathEndsWithSlash, this.exclusive, this.exactPath); } public int getOrder() { return order; } RouteState setOrder(int order) { return new RouteState( this.route, this.path, this.name, order, this.enabled, this.methods, this.consumes, this.emptyBodyPermittedWithConsumes, this.produces, this.contextHandlers, this.failureHandlers, this.added, this.pattern, this.groups, this.useNormalizedPath, this.namedGroupsInRegex, this.virtualHostPattern, this.pathEndsWithSlash, this.exclusive, this.exactPath); } public boolean isEnabled() { return enabled; } RouteState setEnabled(boolean enabled) { return new RouteState( this.route, this.path, this.name, this.order, enabled, this.methods, this.consumes, this.emptyBodyPermittedWithConsumes, this.produces, this.contextHandlers, this.failureHandlers, this.added, this.pattern, this.groups, this.useNormalizedPath, this.namedGroupsInRegex, this.virtualHostPattern, this.pathEndsWithSlash, this.exclusive, this.exactPath); } public Set<HttpMethod> getMethods() { return methods; } RouteState setMethods(Set<HttpMethod> methods) { return new RouteState( this.route, this.path, this.name, this.order, this.enabled, methods, this.consumes, this.emptyBodyPermittedWithConsumes, this.produces, this.contextHandlers, this.failureHandlers, this.added, this.pattern, this.groups, this.useNormalizedPath, this.namedGroupsInRegex, this.virtualHostPattern, this.pathEndsWithSlash, this.exclusive, this.exactPath); } public RouteState addMethod(HttpMethod method) { RouteState newState = new RouteState( this.route, this.path, this.name, this.order, this.enabled, this.methods == null ? new HashSet<>() : new HashSet<>(this.methods), this.consumes, this.emptyBodyPermittedWithConsumes, this.produces, this.contextHandlers, this.failureHandlers, this.added, this.pattern, this.groups, this.useNormalizedPath, this.namedGroupsInRegex, this.virtualHostPattern, this.pathEndsWithSlash, this.exclusive, this.exactPath); newState.methods.add(method); return newState; } public Set<MIMEHeader> getConsumes() { return consumes; } RouteState setConsumes(Set<MIMEHeader> consumes) { return new RouteState( this.route, this.path, this.name, this.order, this.enabled, this.methods, consumes, this.emptyBodyPermittedWithConsumes, this.produces, this.contextHandlers, this.failureHandlers, this.added, this.pattern, this.groups, this.useNormalizedPath, this.namedGroupsInRegex, this.virtualHostPattern, this.pathEndsWithSlash, this.exclusive, this.exactPath); } RouteState addConsume(MIMEHeader mime) { RouteState newState = new RouteState( this.route, this.path, this.name, this.order, this.enabled, this.methods, this.consumes == null ? new LinkedHashSet<>() : new LinkedHashSet<>(this.consumes), this.emptyBodyPermittedWithConsumes, this.produces, this.contextHandlers, this.failureHandlers, this.added, this.pattern, this.groups, this.useNormalizedPath, this.namedGroupsInRegex, this.virtualHostPattern, this.pathEndsWithSlash, this.exclusive, this.exactPath); newState.consumes.add(mime); return newState; } public boolean isEmptyBodyPermittedWithConsumes() { return emptyBodyPermittedWithConsumes; } RouteState setEmptyBodyPermittedWithConsumes(boolean emptyBodyPermittedWithConsumes) { return new RouteState( this.route, this.path, this.name, this.order, this.enabled, this.methods, this.consumes, emptyBodyPermittedWithConsumes, this.produces, this.contextHandlers, this.failureHandlers, this.added, this.pattern, this.groups, this.useNormalizedPath, this.namedGroupsInRegex, this.virtualHostPattern, this.pathEndsWithSlash, this.exclusive, this.exactPath); } public Set<MIMEHeader> getProduces() { return produces; } RouteState setProduces(Set<MIMEHeader> produces) { return new RouteState( this.route, this.path, this.name, this.order, this.enabled, this.methods, this.consumes, this.emptyBodyPermittedWithConsumes, produces, this.contextHandlers, this.failureHandlers, this.added, this.pattern, this.groups, this.useNormalizedPath, this.namedGroupsInRegex, this.virtualHostPattern, this.pathEndsWithSlash, this.exclusive, this.exactPath); } RouteState addProduce(MIMEHeader mime) { RouteState newState = new RouteState( this.route, this.path, this.name, this.order, this.enabled, this.methods, this.consumes, this.emptyBodyPermittedWithConsumes, this.produces == null ? new LinkedHashSet<>() : new LinkedHashSet<>(this.produces), this.contextHandlers, this.failureHandlers, this.added, this.pattern, this.groups, this.useNormalizedPath, this.namedGroupsInRegex, this.virtualHostPattern, this.pathEndsWithSlash, this.exclusive, this.exactPath); newState.produces.add(mime); return newState; } public List<Handler<RoutingContext>> getContextHandlers() { return contextHandlers; } public int getContextHandlersLength() { return contextHandlers == null ? 0 : contextHandlers.size(); } RouteState setContextHandlers(List<Handler<RoutingContext>> contextHandlers) { return new RouteState( this.route, this.path, this.name, this.order, this.enabled, this.methods, this.consumes, this.emptyBodyPermittedWithConsumes, this.produces, contextHandlers, this.failureHandlers, this.added, this.pattern, this.groups, this.useNormalizedPath, this.namedGroupsInRegex, this.virtualHostPattern, this.pathEndsWithSlash, this.exclusive, this.exactPath); } RouteState addContextHandler(Handler<RoutingContext> contextHandler) { RouteState newState = new RouteState( this.route, this.path, this.name, this.order, this.enabled, this.methods, this.consumes, this.emptyBodyPermittedWithConsumes, this.produces, this.contextHandlers == null ? new ArrayList<>() : new ArrayList<>(this.contextHandlers), this.failureHandlers, this.added, this.pattern, this.groups, this.useNormalizedPath, this.namedGroupsInRegex, this.virtualHostPattern, this.pathEndsWithSlash, this.exclusive, this.exactPath); newState.contextHandlers.add(contextHandler); return newState; } public List<Handler<RoutingContext>> getFailureHandlers() { return failureHandlers; } public int getFailureHandlersLength() { return failureHandlers == null ? 0 : failureHandlers.size(); } RouteState setFailureHandlers(List<Handler<RoutingContext>> failureHandlers) { return new RouteState( this.route, this.path, this.name, this.order, this.enabled, this.methods, this.consumes, this.emptyBodyPermittedWithConsumes, this.produces, this.contextHandlers, failureHandlers, this.added, this.pattern, this.groups, this.useNormalizedPath, this.namedGroupsInRegex, this.virtualHostPattern, this.pathEndsWithSlash, this.exclusive, this.exactPath); } RouteState addFailureHandler(Handler<RoutingContext> failureHandler) { RouteState newState = new RouteState( this.route, this.path, this.name, this.order, this.enabled, this.methods, this.consumes, this.emptyBodyPermittedWithConsumes, this.produces, this.contextHandlers, this.failureHandlers == null ? new ArrayList<>() : new ArrayList<>(this.failureHandlers), this.added, this.pattern, this.groups, this.useNormalizedPath, this.namedGroupsInRegex, this.virtualHostPattern, this.pathEndsWithSlash, this.exclusive, this.exactPath); newState.failureHandlers.add(failureHandler); return newState; } public boolean isAdded() { return added; } RouteState setAdded(boolean added) { return new RouteState( this.route, this.path, this.name, this.order, this.enabled, this.methods, this.consumes, this.emptyBodyPermittedWithConsumes, this.produces, this.contextHandlers, this.failureHandlers, added, this.pattern, this.groups, this.useNormalizedPath, this.namedGroupsInRegex, this.virtualHostPattern, this.pathEndsWithSlash, this.exclusive, this.exactPath); } public Pattern getPattern() { return pattern; } RouteState setPattern(Pattern pattern) { return new RouteState( this.route, this.path, this.name, this.order, this.enabled, this.methods, this.consumes, this.emptyBodyPermittedWithConsumes, this.produces, this.contextHandlers, this.failureHandlers, this.added, pattern, this.groups, this.useNormalizedPath, this.namedGroupsInRegex, this.virtualHostPattern, this.pathEndsWithSlash, this.exclusive, this.exactPath); } public List<String> getGroups() { return groups; } RouteState setGroups(List<String> groups) { return new RouteState( this.route, this.path, this.name, this.order, this.enabled, this.methods, this.consumes, this.emptyBodyPermittedWithConsumes, this.produces, this.contextHandlers, this.failureHandlers, this.added, this.pattern, groups, this.useNormalizedPath, this.namedGroupsInRegex, this.virtualHostPattern, this.pathEndsWithSlash, this.exclusive, this.exactPath); } RouteState addGroup(String group) { RouteState newState = new RouteState( this.route, this.path, this.name, this.order, this.enabled, this.methods, this.consumes, this.emptyBodyPermittedWithConsumes, this.produces, this.contextHandlers, this.failureHandlers, this.added, this.pattern, this.groups == null ? new ArrayList<>() : new ArrayList<>(groups), this.useNormalizedPath, this.namedGroupsInRegex, this.virtualHostPattern, this.pathEndsWithSlash, this.exclusive, this.exactPath); newState.groups.add(group); return newState; } public boolean isUseNormalizedPath() { return useNormalizedPath; } RouteState setUseNormalizedPath(boolean useNormalizedPath) { return new RouteState( this.route, this.path, this.name, this.order, this.enabled, this.methods, this.consumes, this.emptyBodyPermittedWithConsumes, this.produces, this.contextHandlers, this.failureHandlers, this.added, this.pattern, this.groups, useNormalizedPath, this.namedGroupsInRegex, this.virtualHostPattern, this.pathEndsWithSlash, this.exclusive, this.exactPath); } public Set<String> getNamedGroupsInRegex() { return namedGroupsInRegex; } RouteState setNamedGroupsInRegex(Set<String> namedGroupsInRegex) { return new RouteState( this.route, this.path, this.name, this.order, this.enabled, this.methods, this.consumes, this.emptyBodyPermittedWithConsumes, this.produces, this.contextHandlers, this.failureHandlers, this.added, this.pattern, this.groups, this.useNormalizedPath, namedGroupsInRegex, this.virtualHostPattern, this.pathEndsWithSlash, this.exclusive, this.exactPath); } RouteState addNamedGroupInRegex(String namedGroupInRegex) { RouteState newState = new RouteState( this.route, this.path, this.name, this.order, this.enabled, this.methods, this.consumes, this.emptyBodyPermittedWithConsumes, this.produces, this.contextHandlers, this.failureHandlers, this.added, this.pattern, this.groups, this.useNormalizedPath, this.namedGroupsInRegex == null ? new HashSet<>() : new HashSet<>(this.namedGroupsInRegex), this.virtualHostPattern, this.pathEndsWithSlash, this.exclusive, this.exactPath); newState.namedGroupsInRegex.add(namedGroupInRegex); return newState; } public Pattern getVirtualHostPattern() { return virtualHostPattern; } RouteState setVirtualHostPattern(Pattern virtualHostPattern) { return new RouteState( this.route, this.path, this.name, this.order, this.enabled, this.methods, this.consumes, this.emptyBodyPermittedWithConsumes, this.produces, this.contextHandlers, this.failureHandlers, this.added, this.pattern, this.groups, this.useNormalizedPath, this.namedGroupsInRegex, virtualHostPattern, this.pathEndsWithSlash, this.exclusive, this.exactPath); } public boolean isPathEndsWithSlash() { return pathEndsWithSlash; } RouteState setPathEndsWithSlash(boolean pathEndsWithSlash) { return new RouteState( this.route, this.path, this.name, this.order, this.enabled, this.methods, this.consumes, this.emptyBodyPermittedWithConsumes, this.produces, this.contextHandlers, this.failureHandlers, this.added, this.pattern, this.groups, this.useNormalizedPath, this.namedGroupsInRegex, this.virtualHostPattern, pathEndsWithSlash, this.exclusive, this.exactPath); } public boolean isExclusive() { return exclusive; } RouteState setExclusive(boolean exclusive) { return new RouteState( this.route, this.path, this.name, this.order, this.enabled, this.methods, this.consumes, this.emptyBodyPermittedWithConsumes, this.produces, this.contextHandlers, this.failureHandlers, this.added, this.pattern, this.groups, this.useNormalizedPath, this.namedGroupsInRegex, this.virtualHostPattern, this.pathEndsWithSlash, exclusive, this.exactPath); } public boolean isExactPath() { return exactPath; } RouteState setExactPath(boolean exactPath) { return new RouteState( this.route, this.path, this.name, this.order, this.enabled, this.methods, this.consumes, this.emptyBodyPermittedWithConsumes, this.produces, this.contextHandlers, this.failureHandlers, this.added, this.pattern, this.groups, this.useNormalizedPath, this.namedGroupsInRegex, this.virtualHostPattern, this.pathEndsWithSlash, this.exclusive, exactPath); } RouteState setName(String name) { return new RouteState( this.route, this.path, name, this.order, this.enabled, this.methods, this.consumes, this.emptyBodyPermittedWithConsumes, this.produces, this.contextHandlers, this.failureHandlers, this.added, this.pattern, this.groups, this.useNormalizedPath, this.namedGroupsInRegex, this.virtualHostPattern, this.pathEndsWithSlash, this.exclusive, this.exactPath); } private boolean containsMethod(HttpServerRequest request) { if (!isEmpty(methods)) { return methods.contains(request.method()); } return false; } private static <T> boolean isEmpty(Collection<T> collection) { return collection == null || collection.isEmpty(); }
Returns:0 if route matches, otherwise it return the status code
/** * @return 0 if route matches, otherwise it return the status code */
public int matches(RoutingContextImplBase context, String mountPoint, boolean failure) { if (failure && !hasNextFailureHandler(context) || !failure && !hasNextContextHandler(context)) { return 404; } if (!enabled) { return 404; } HttpServerRequest request = context.request(); if (path != null && pattern == null && !pathMatches(mountPoint, context)) { return 404; } if (pattern != null) { String path = useNormalizedPath ? context.normalizedPath() : context.request().path(); if (mountPoint != null) { int strip = mountPoint.length(); // mount point can have significant slash if (mountPoint.charAt(strip - 1)== '/') { strip--; } path = path.substring(strip); } Matcher m = pattern.matcher(path); if (m.matches()) { if (!isEmpty(methods) && !containsMethod(request)) { // If I'm here path or path pattern matches, but the method is wrong return 405; } context.matchRest = -1; context.matchNormalized = useNormalizedPath; if (m.groupCount() > 0) { if (!exactPath) { context.matchRest = m.start("rest"); // always replace context.pathParams() .put("*", path.substring(context.matchRest)); } if (!isEmpty(groups)) { // Pattern - named params // decode the path as it could contain escaped chars. for (int i = 0; i < Math.min(groups.size(), m.groupCount()); i++) { final String k = groups.get(i); String undecodedValue; // We try to take value in three ways: // 1. group name of type p0, p1, pN (most frequent and used by vertx params) // 2. group name inside the regex // 3. No group name try { undecodedValue = m.group("p" + i); } catch (IllegalArgumentException e) { try { undecodedValue = m.group(k); } catch (IllegalArgumentException e1) { // Groups starts from 1 (0 group is total match) undecodedValue = m.group(i + 1); } } addPathParam(context, k, undecodedValue); } } else { // Straight regex - un-named params // decode the path as it could contain escaped chars. if (!isEmpty(namedGroupsInRegex)) { for (String namedGroup : namedGroupsInRegex) { String namedGroupValue = m.group(namedGroup); if (namedGroupValue != null) { addPathParam(context, namedGroup, namedGroupValue); } } } for (int i = 0; i < m.groupCount(); i++) { String group = m.group(i + 1); if (group != null) { final String k = "param" + i; addPathParam(context, k, group); } } } } } else { return 404; } } else { // no pattern check for wrong method if (!isEmpty(methods) && !containsMethod(request)) { // If I'm here path or path pattern matches, but the method is wrong return 405; } } if (!isEmpty(consumes)) { // Can this route consume the specified content type MIMEHeader contentType = context.parsedHeaders().contentType(); MIMEHeader consumal = contentType.findMatchedBy(consumes); if (consumal == null && !(contentType.rawValue().isEmpty() && emptyBodyPermittedWithConsumes)) { if (contentType.rawValue().isEmpty()) { return 400; } else { return 415; } } } if (!isEmpty(produces)) { List<MIMEHeader> acceptableTypes = context.parsedHeaders().accept(); if(!acceptableTypes.isEmpty()) { MIMEHeader selectedAccept = context.parsedHeaders().findBestUserAcceptedIn(acceptableTypes, produces); if (selectedAccept != null) { context.setAcceptableContentType(selectedAccept.rawValue()); } else { return 406; } } } if (!virtualHostMatches(context.request())) { return 404; } return 0; } private boolean pathMatches(String mountPoint, RoutingContext ctx) { final boolean rootRouter = mountPoint == null; final boolean pathEndsWithSlash; final String thePath; if (rootRouter) { thePath = path; pathEndsWithSlash = this.pathEndsWithSlash; } else { boolean mountPointEndsWithSlash = mountPoint.charAt(mountPoint.length() - 1) == '/'; // path is "/" if (path.length() == 1) { // mount point is always assumed to be a directory so // we must ignore the final slash thePath = mountPoint; // so this is a special case we can't consider the configured route but the mount point itself pathEndsWithSlash = mountPointEndsWithSlash; } else { // solve the double slash when mount point ends with slash if (mountPointEndsWithSlash) { thePath = mountPoint + path.substring(1); } else { thePath = mountPoint + path; } pathEndsWithSlash = this.pathEndsWithSlash; } } String requestPath; if (useNormalizedPath) { // never null requestPath = ctx.normalizedPath(); } else { requestPath = ctx.request().path(); // can be null if (requestPath == null) { requestPath = "/"; } } if (exactPath) { return pathMatchesExact(thePath, requestPath, pathEndsWithSlash); } else { if (pathEndsWithSlash) { if (requestPath.charAt(requestPath.length() - 1) == '/') { if (requestPath.equals(thePath)) { return true; } } else { if (thePath.regionMatches(0, requestPath, 0, thePath.length())) { return true; } } } if (requestPath.startsWith(thePath)) { // handle the "rest" as path param * ctx.pathParams() .put("*", requestPath.substring(thePath.length())); return true; } return false; } } private boolean virtualHostMatches(HttpServerRequest request) { if (virtualHostPattern == null) { return true; } boolean match = false; String host = request.host(); if (host == null) { return match; } for (String h : host.split(":")) { if (virtualHostPattern.matcher(h).matches()) { match = true; break; } } return match; } private static boolean pathMatchesExact(String base, String other, boolean significantSlash) { // Ignore trailing slash when matching paths int len = other.length(); if (significantSlash) { if (other.charAt(len -1) != '/') { // final slash is significant but missing return false; } } else { if (other.charAt(len -1) == '/') { // final slash is not significant, ignore it len--; } } // lengths are not the same (fail) if (base.length() != len) { return false; } // content must match return other.regionMatches(0, base, 0, len); } private void addPathParam(RoutingContext context, String name, String value) { HttpServerRequest request = context.request(); final String decodedValue = URIDecoder.decodeURIComponent(value, false); if (!request.params().contains(name)) { request.params().add(name, decodedValue); } context.pathParams().put(name, decodedValue); } boolean hasNextContextHandler(RoutingContextImplBase context) { return context.currentRouteNextHandlerIndex() < getContextHandlersLength(); } boolean hasNextFailureHandler(RoutingContextImplBase context) { return context.currentRouteNextFailureHandlerIndex() < getFailureHandlersLength(); } void handleContext(RoutingContextImplBase context) { contextHandlers .get(context.currentRouteNextHandlerIndex() - 1) .handle(context); } void handleFailure(RoutingContextImplBase context) { failureHandlers .get(context.currentRouteNextFailureHandlerIndex() - 1) .handle(context); } public String getName() { if (name != null) { return name; } if (path != null) { return path; } if (pattern != null) { return pattern.pattern(); } return null; } @Override public String toString() { return "RouteState{" + "path='" + path + '\'' + ", name=" + name + ", order=" + order + ", enabled=" + enabled + ", methods=" + methods + ", consumes=" + consumes + ", emptyBodyPermittedWithConsumes=" + emptyBodyPermittedWithConsumes + ", produces=" + produces + ", contextHandlers=" + contextHandlers + ", failureHandlers=" + failureHandlers + ", added=" + added + ", pattern=" + pattern + ", groups=" + groups + ", useNormalizedPath=" + useNormalizedPath + ", namedGroupsInRegex=" + namedGroupsInRegex + ", virtualHostPattern=" + virtualHostPattern + ", pathEndsWithSlash=" + pathEndsWithSlash + ", exclusive=" + exclusive + ", exactPath=" + exactPath + '}'; } }