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;
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 (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 (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();
}
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();
if (mountPoint.charAt(strip - 1)== '/') {
strip--;
}
path = path.substring(strip);
}
Matcher m = pattern.matcher(path);
if (m.matches()) {
if (!isEmpty(methods) && !containsMethod(request)) {
return 405;
}
context.matchRest = -1;
context.matchNormalized = useNormalizedPath;
if (m.groupCount() > 0) {
if (!exactPath) {
context.matchRest = m.start("rest");
context.pathParams()
.put("*", path.substring(context.matchRest));
}
if (!isEmpty(groups)) {
for (int i = 0; i < Math.min(groups.size(), m.groupCount()); 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 {
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 {
if (!isEmpty(methods) && !containsMethod(request)) {
return 405;
}
}
if (!isEmpty(consumes)) {
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) == '/';
if (path.length() == 1) {
thePath = mountPoint;
pathEndsWithSlash = mountPointEndsWithSlash;
} else {
if (mountPointEndsWithSlash) {
thePath = mountPoint + path.substring(1);
} else {
thePath = mountPoint + path;
}
pathEndsWithSlash = this.pathEndsWithSlash;
}
}
String requestPath;
if (useNormalizedPath) {
requestPath = ctx.normalizedPath();
} else {
requestPath = ctx.request().path();
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)) {
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) {
int len = other.length();
if (significantSlash) {
if (other.charAt(len -1) != '/') {
return false;
}
} else {
if (other.charAt(len -1) == '/') {
len--;
}
}
if (base.length() != len) {
return false;
}
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 +
'}';
}
}