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.logging.Logger;
import io.vertx.core.logging.LoggerFactory;
import io.vertx.core.net.impl.URIDecoder;
import io.vertx.ext.web.MIMEHeader;
import io.vertx.ext.web.Route;
import io.vertx.ext.web.RoutingContext;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RouteImpl implements Route {
private static final Logger log = LoggerFactory.getLogger(RouteImpl.class);
private final RouterImpl router;
private final Set<HttpMethod> methods = new HashSet<>();
private final Set<MIMEHeader> consumes = new LinkedHashSet<>();
private boolean emptyBodyPermittedWithConsumes = false;
private final Set<MIMEHeader> produces = new LinkedHashSet<>();
private String path;
private int order;
private boolean enabled = true;
private List<Handler<RoutingContext>> contextHandlers;
private List<Handler<RoutingContext>> failureHandlers;
private boolean added;
private Pattern pattern;
private List<String> groups;
private boolean useNormalisedPath = true;
private Set<String> namedGroupsInRegex = new TreeSet<>();
RouteImpl(RouterImpl router, int order) {
this.router = router;
this.order = order;
this.contextHandlers = new ArrayList<>();
this.failureHandlers = new ArrayList<>();
}
RouteImpl(RouterImpl router, int order, HttpMethod method, String path) {
this(router, order);
methods.add(method);
checkPath(path);
setPath(path);
}
RouteImpl(RouterImpl router, int order, String path) {
this(router, order);
checkPath(path);
setPath(path);
}
RouteImpl(RouterImpl router, int order, HttpMethod method, String regex, boolean bregex) {
this(router, order);
methods.add(method);
setRegex(regex);
}
RouteImpl(RouterImpl router, int order, String regex, boolean bregex) {
this(router, order);
setRegex(regex);
}
@Override
public synchronized Route method(HttpMethod method) {
methods.add(method);
return this;
}
@Override
public synchronized Route path(String path) {
checkPath(path);
setPath(path);
return this;
}
@Override
public synchronized Route pathRegex(String regex) {
setRegex(regex);
return this;
}
@Override
public synchronized Route produces(String contentType) {
ParsableMIMEValue value = new ParsableMIMEValue(contentType).forceParse();
produces.add(value);
return this;
}
@Override
public synchronized Route consumes(String contentType) {
ParsableMIMEValue value = new ParsableMIMEValue(contentType).forceParse();
consumes.add(value);
return this;
}
@Override
public synchronized Route order(int order) {
if (added) {
throw new IllegalStateException("Can't change order after route is active");
}
this.order = order;
return this;
}
@Override
public synchronized Route last() {
return order(Integer.MAX_VALUE);
}
@Override
public synchronized Route handler(Handler<RoutingContext> contextHandler) {
this.contextHandlers.add(contextHandler);
checkAdd();
return this;
}
@Override
public Route blockingHandler(Handler<RoutingContext> contextHandler) {
return blockingHandler(contextHandler, true);
}
@Override
public synchronized Route blockingHandler(Handler<RoutingContext> contextHandler, boolean ordered) {
return handler(new BlockingHandlerDecorator(contextHandler, ordered));
}
@Override
public synchronized Route failureHandler(Handler<RoutingContext> exceptionHandler) {
this.failureHandlers.add(exceptionHandler);
checkAdd();
return this;
}
@Override
public synchronized Route remove() {
router.remove(this);
return this;
}
@Override
public synchronized Route disable() {
enabled = false;
return this;
}
@Override
public synchronized Route enable() {
enabled = true;
return this;
}
@Override
public Route useNormalisedPath(boolean useNormalisedPath) {
this.useNormalisedPath = useNormalisedPath;
return this;
}
@Override
public String getPath() {
return path;
}
@Override
public Set<HttpMethod> methods() {
return this.methods;
}
@Override
public Route setRegexGroupsNames(List<String> groups) {
this.groups = groups;
return this;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("Route[ ");
sb.append("path:").append(path);
sb.append(" pattern:").append(pattern);
sb.append(" handlers:").append(contextHandlers);
sb.append(" failureHandlers:").append(failureHandlers);
sb.append(" order:").append(order);
sb.append(" methods:[");
int cnt = 0;
for (HttpMethod method : methods) {
sb.append(method);
cnt++;
if (cnt < methods.size()) {
sb.append(",");
}
}
sb.append("]]@").append(System.identityHashCode(this));
return sb.toString();
}
void handleContext(RoutingContextImplBase context) {
Handler<RoutingContext> contextHandler;
synchronized (this) {
contextHandler = contextHandlers.get(context.currentRouteNextHandlerIndex() - 1);
}
contextHandler.handle(context);
}
void handleFailure(RoutingContextImplBase context) {
Handler<RoutingContext> failureHandler;
synchronized (this) {
failureHandler = failureHandlers.get(context.currentRouteNextFailureHandlerIndex() - 1);
}
failureHandler.handle(context);
}
synchronized 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 = useNormalisedPath ? context.normalisedPath() : context.request().path();
if (mountPoint != null) {
path = path.substring(mountPoint.length());
}
Matcher m = pattern.matcher(path);
if (m.matches()) {
if (m.groupCount() > 0) {
if (groups != null) {
for (int i = 0; i < groups.size(); i++) {
final String k = groups.get(i);
String undecodedValue;
try {
undecodedValue = m.group("p" + i);
} catch (IllegalArgumentException e) {
try {
undecodedValue = m.group(k);
} catch (IllegalArgumentException e1) {
undecodedValue = m.group(i + 1);
}
}
addPathParam(context, k, undecodedValue);
}
} else {
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;
}
}
if (!methods.isEmpty() && !methods.contains(request.method())) {
return 405;
}
if (!consumes.isEmpty()) {
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;
}
}
}
List<MIMEHeader> acceptableTypes = context.parsedHeaders().accept();
if (!produces.isEmpty() && !acceptableTypes.isEmpty()) {
MIMEHeader selectedAccept = context.parsedHeaders().findBestUserAcceptedIn(acceptableTypes, produces);
if (selectedAccept != null) {
context.setAcceptableContentType(selectedAccept.rawValue());
return 0;
}
return 406;
}
return 0;
}
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);
}
RouterImpl router() {
return router;
}
private boolean pathMatches(String mountPoint, RoutingContext ctx) {
String thePath = mountPoint == null ? path : mountPoint + path;
String requestPath;
if (useNormalisedPath) {
requestPath = ctx.normalisedPath();
} else {
requestPath = ctx.request().path();
if (requestPath == null) {
requestPath = "/";
}
}
if (exactPath) {
return pathMatchesExact(requestPath, thePath);
} else {
if (thePath.endsWith("/") && requestPath.equals(removeTrailing(thePath))) {
return true;
}
return requestPath.startsWith(thePath);
}
}
private boolean pathMatchesExact(String path1, String path2) {
return removeTrailing(path1).equals(removeTrailing(path2));
}
private String removeTrailing(String path) {
int i = path.length();
if (path.charAt(i - 1) == '/') {
path = path.substring(0, i - 1);
}
return path;
}
private void setPath(String path) {
if (path.indexOf(':') != -1) {
createPatternRegex(path);
this.path = path;
} else {
if (path.charAt(path.length() - 1) != '*') {
exactPath = true;
this.path = path;
} else {
exactPath = false;
this.path = path.substring(0, path.length() - 1);
}
}
}
private void setRegex(String regex) {
pattern = Pattern.compile(regex);
Set<String> namedGroups = findNamedGroups(pattern.pattern());
if (!namedGroups.isEmpty()) {
namedGroupsInRegex.addAll(namedGroups);
}
}
private Set<String> findNamedGroups(String path) {
Set<String> namedGroups = new TreeSet<>();
Matcher m = Pattern.compile("\\(\\?<([a-zA-Z][a-zA-Z0-9]*)>").matcher(path);
while (m.find()) {
namedGroups.add(m.group(1));
}
return namedGroups;
}
private static final Pattern RE_OPERATORS_NO_STAR = Pattern.compile("([\\(\\)\\$\\+\\.])");
private static final Pattern RE_TOKEN_SEARCH = Pattern.compile(":([A-Za-z][A-Za-z0-9_]*)");
private void createPatternRegex(String path) {
path = RE_OPERATORS_NO_STAR.matcher(path).replaceAll("\\\\$1");
if (path.charAt(path.length() - 1) == '*') {
path = path.substring(0, path.length() - 1) + ".*";
}
Matcher m = RE_TOKEN_SEARCH.matcher(path);
StringBuffer sb = new StringBuffer();
groups = new ArrayList<>();
int index = 0;
while (m.find()) {
String param = "p" + index;
String group = m.group().substring(1);
if (groups.contains(group)) {
throw new IllegalArgumentException("Cannot use identifier " + group + " more than once in pattern string");
}
m.appendReplacement(sb, "(?<" + param + ">[^/]+)");
groups.add(group);
index++;
}
m.appendTail(sb);
path = sb.toString();
pattern = Pattern.compile(path);
}
private void checkPath(String path) {
if ("".equals(path) || path.charAt(0) != '/') {
throw new IllegalArgumentException("Path must start with /");
}
}
private boolean exactPath;
int order() {
return order;
}
private void checkAdd() {
if (!added) {
router.add(this);
added = true;
}
}
synchronized protected boolean hasNextContextHandler(RoutingContextImplBase context) {
return context.currentRouteNextHandlerIndex() < contextHandlers.size();
}
synchronized protected boolean hasNextFailureHandler(RoutingContextImplBase context) {
return context.currentRouteNextFailureHandlerIndex() < failureHandlers.size();
}
public void setEmptyBodyPermittedWithConsumes(boolean emptyBodyPermittedWithConsumes) {
this.emptyBodyPermittedWithConsumes = emptyBodyPermittedWithConsumes;
}
}