org.smartboot.http.common.utils.AntPathMatcher Maven / Gradle / Ivy
/*******************************************************************************
* Copyright (c) 2017-2020, org.smartboot. All rights reserved.
* project name: smart-http
* file name: AntPathMatcher.java
* Date: 2020-01-01
* Author: sandao ([email protected])
******************************************************************************/
package org.smartboot.http.common.utils;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Part of this mapping code has been kindly borrowed from Apache Ant.
*
*
The mapping matches URLs using the following rules:
*
* - {@code ?} matches one character
* - {@code *} matches zero or more characters
* - {@code **} matches zero or more directories in a path
*
*
* Examples
*
* - {@code com/t?st.jsp} — matches {@code com/test.jsp} but also
* {@code com/tast.jsp} or {@code com/txst.jsp}
* - {@code com/*.jsp} — matches all {@code .jsp} files in the
* {@code com} directory
* com/**/test.jsp
— matches all {@code test.jsp}
* files underneath the {@code com} path
* org/springframework/**/*.jsp
— matches all
* {@code .jsp} files underneath the {@code org/springframework} path
* org/**/servlet/bla.jsp
— matches
* {@code org/springframework/servlet/bla.jsp} but also
* {@code org/springframework/testing/servlet/bla.jsp} and {@code org/servlet/bla.jsp}
*
*
* @author Alef Arendsen
* @author Juergen Hoeller
* @author Rob Harrop
* @author Arjen Poutsma
* @author Rossen Stoyanchev
* @author Sam Brannen
* @since 16.07.2003
*/
public class AntPathMatcher {
/**
* Default path separator: "/"
*/
public static final String DEFAULT_PATH_SEPARATOR = "/";
private static final int CACHE_TURNOFF_THRESHOLD = 65536;
private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\{[^/]+?\\}");
final Map stringMatcherCache = new ConcurrentHashMap(256);
private final Map tokenizedPatternCache = new ConcurrentHashMap(256);
private String pathSeparator;
private boolean trimTokens = true;
private volatile Boolean cachePatterns;
/**
* Create a new instance with the {@link #DEFAULT_PATH_SEPARATOR}.
*/
public AntPathMatcher() {
this.pathSeparator = DEFAULT_PATH_SEPARATOR;
}
private void deactivatePatternCache() {
this.cachePatterns = false;
this.tokenizedPatternCache.clear();
this.stringMatcherCache.clear();
}
public boolean match(String pattern, String path) {
return doMatch(pattern, path, true, null);
}
/**
* Actually match the given {@code path} against the given {@code pattern}.
*
* @param pattern the pattern to match against
* @param path the path String to test
* @param fullMatch whether a full pattern match is required (else a pattern match
* as far as the given base path goes is sufficient)
* @return {@code true} if the supplied {@code path} matched, {@code false} if it didn't
*/
protected boolean doMatch(String pattern, String path, boolean fullMatch, Map uriTemplateVariables) {
if (path.startsWith(this.pathSeparator) != pattern.startsWith(this.pathSeparator)) {
return false;
}
String[] pattDirs = tokenizePattern(pattern);
String[] pathDirs = tokenizePath(path);
int pattIdxStart = 0;
int pattIdxEnd = pattDirs.length - 1;
int pathIdxStart = 0;
int pathIdxEnd = pathDirs.length - 1;
// Match all elements up to the first **
while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {
String pattDir = pattDirs[pattIdxStart];
if ("**".equals(pattDir)) {
break;
}
if (!matchStrings(pattDir, pathDirs[pathIdxStart], uriTemplateVariables)) {
return false;
}
pattIdxStart++;
pathIdxStart++;
}
if (pathIdxStart > pathIdxEnd) {
// Path is exhausted, only match if rest of pattern is * or **'s
if (pattIdxStart > pattIdxEnd) {
return (pattern.endsWith(this.pathSeparator) ? path.endsWith(this.pathSeparator) :
!path.endsWith(this.pathSeparator));
}
if (!fullMatch) {
return true;
}
if (pattIdxStart == pattIdxEnd && pattDirs[pattIdxStart].equals("*") && path.endsWith(this.pathSeparator)) {
return true;
}
for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
if (!pattDirs[i].equals("**")) {
return false;
}
}
return true;
} else if (pattIdxStart > pattIdxEnd) {
// String not exhausted, but pattern is. Failure.
return false;
} else if (!fullMatch && "**".equals(pattDirs[pattIdxStart])) {
// Path start definitely matches due to "**" part in pattern.
return true;
}
// up to last '**'
while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {
String pattDir = pattDirs[pattIdxEnd];
if (pattDir.equals("**")) {
break;
}
if (!matchStrings(pattDir, pathDirs[pathIdxEnd], uriTemplateVariables)) {
return false;
}
pattIdxEnd--;
pathIdxEnd--;
}
if (pathIdxStart > pathIdxEnd) {
// String is exhausted
for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
if (!pattDirs[i].equals("**")) {
return false;
}
}
return true;
}
while (pattIdxStart != pattIdxEnd && pathIdxStart <= pathIdxEnd) {
int patIdxTmp = -1;
for (int i = pattIdxStart + 1; i <= pattIdxEnd; i++) {
if (pattDirs[i].equals("**")) {
patIdxTmp = i;
break;
}
}
if (patIdxTmp == pattIdxStart + 1) {
// '**/**' situation, so skip one
pattIdxStart++;
continue;
}
// Find the pattern between padIdxStart & padIdxTmp in str between
// strIdxStart & strIdxEnd
int patLength = (patIdxTmp - pattIdxStart - 1);
int strLength = (pathIdxEnd - pathIdxStart + 1);
int foundIdx = -1;
strLoop:
for (int i = 0; i <= strLength - patLength; i++) {
for (int j = 0; j < patLength; j++) {
String subPat = pattDirs[pattIdxStart + j + 1];
String subStr = pathDirs[pathIdxStart + i + j];
if (!matchStrings(subPat, subStr, uriTemplateVariables)) {
continue strLoop;
}
}
foundIdx = pathIdxStart + i;
break;
}
if (foundIdx == -1) {
return false;
}
pattIdxStart = patIdxTmp;
pathIdxStart = foundIdx + patLength;
}
for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
if (!pattDirs[i].equals("**")) {
return false;
}
}
return true;
}
/**
* Tokenize the given path pattern into parts, based on this matcher's settings.
* Performs caching based on {@link #setCachePatterns}, delegating to
* {@link #tokenizePath(String)} for the actual tokenization algorithm.
*
* @param pattern the pattern to tokenize
* @return the tokenized pattern parts
*/
protected String[] tokenizePattern(String pattern) {
String[] tokenized = null;
Boolean cachePatterns = this.cachePatterns;
if (cachePatterns == null || cachePatterns.booleanValue()) {
tokenized = this.tokenizedPatternCache.get(pattern);
}
if (tokenized == null) {
tokenized = tokenizePath(pattern);
if (cachePatterns == null && this.tokenizedPatternCache.size() >= CACHE_TURNOFF_THRESHOLD) {
// Try to adapt to the runtime situation that we're encountering:
// There are obviously too many different patterns coming in here...
// So let's turn off the cache since the patterns are unlikely to be reoccurring.
deactivatePatternCache();
return tokenized;
}
if (cachePatterns == null || cachePatterns.booleanValue()) {
this.tokenizedPatternCache.put(pattern, tokenized);
}
}
return tokenized;
}
/**
* Tokenize the given path String into parts, based on this matcher's settings.
*
* @param path the path to tokenize
* @return the tokenized path parts
*/
protected String[] tokenizePath(String path) {
return StringUtils.tokenizeToStringArray(path, this.pathSeparator, this.trimTokens, true);
}
/**
* Test whether or not a string matches against a pattern.
*
* @param pattern the pattern to match against (never {@code null})
* @param str the String which must be matched against the pattern (never {@code null})
* @return {@code true} if the string matches against the pattern, or {@code false} otherwise
*/
private boolean matchStrings(String pattern, String str, Map uriTemplateVariables) {
return getStringMatcher(pattern).matchStrings(str, uriTemplateVariables);
}
/**
* Build or retrieve an {@link AntPathStringMatcher} for the given pattern.
* The default implementation checks this AntPathMatcher's internal cache
* (see {@link #setCachePatterns}), creating a new AntPathStringMatcher instance
* if no cached copy is found.
*
When encountering too many patterns to cache at runtime (the threshold is 65536),
* it turns the default cache off, assuming that arbitrary permutations of patterns
* are coming in, with little chance for encountering a recurring pattern.
*
This method may be overridden to implement a custom cache strategy.
*
* @param pattern the pattern to match against (never {@code null})
* @return a corresponding AntPathStringMatcher (never {@code null})
* @see #setCachePatterns
*/
protected AntPathStringMatcher getStringMatcher(String pattern) {
AntPathStringMatcher matcher = null;
Boolean cachePatterns = this.cachePatterns;
if (cachePatterns == null || cachePatterns.booleanValue()) {
matcher = this.stringMatcherCache.get(pattern);
}
if (matcher == null) {
matcher = new AntPathStringMatcher(pattern);
if (cachePatterns == null && this.stringMatcherCache.size() >= CACHE_TURNOFF_THRESHOLD) {
// Try to adapt to the runtime situation that we're encountering:
// There are obviously too many different patterns coming in here...
// So let's turn off the cache since the patterns are unlikely to be reoccurring.
deactivatePatternCache();
return matcher;
}
if (cachePatterns == null || cachePatterns.booleanValue()) {
this.stringMatcherCache.put(pattern, matcher);
}
}
return matcher;
}
/**
* Tests whether or not a string matches against a pattern via a {@link Pattern}.
*
The pattern may contain special characters: '*' means zero or more characters; '?' means one and
* only one character; '{' and '}' indicate a URI template pattern. For example /users/{user}.
*/
protected static class AntPathStringMatcher {
private static final Pattern GLOB_PATTERN = Pattern.compile("\\?|\\*|\\{((?:\\{[^/]+?\\}|[^/{}]|\\\\[{}])+?)\\}");
private static final String DEFAULT_VARIABLE_PATTERN = "(.*)";
private final Pattern pattern;
private final List variableNames = new LinkedList();
public AntPathStringMatcher(String pattern) {
StringBuilder patternBuilder = new StringBuilder();
Matcher matcher = GLOB_PATTERN.matcher(pattern);
int end = 0;
while (matcher.find()) {
patternBuilder.append(quote(pattern, end, matcher.start()));
String match = matcher.group();
if ("?".equals(match)) {
patternBuilder.append('.');
} else if ("*".equals(match)) {
patternBuilder.append(".*");
} else if (match.startsWith("{") && match.endsWith("}")) {
int colonIdx = match.indexOf(':');
if (colonIdx == -1) {
patternBuilder.append(DEFAULT_VARIABLE_PATTERN);
this.variableNames.add(matcher.group(1));
} else {
String variablePattern = match.substring(colonIdx + 1, match.length() - 1);
patternBuilder.append('(');
patternBuilder.append(variablePattern);
patternBuilder.append(')');
String variableName = match.substring(1, colonIdx);
this.variableNames.add(variableName);
}
}
end = matcher.end();
}
patternBuilder.append(quote(pattern, end, pattern.length()));
this.pattern = Pattern.compile(patternBuilder.toString());
}
private String quote(String s, int start, int end) {
if (start == end) {
return "";
}
return Pattern.quote(s.substring(start, end));
}
/**
* Main entry point.
*
* @return {@code true} if the string matches against the pattern, or {@code false} otherwise.
*/
public boolean matchStrings(String str, Map uriTemplateVariables) {
Matcher matcher = this.pattern.matcher(str);
if (matcher.matches()) {
if (uriTemplateVariables != null) {
// SPR-8455
if (this.variableNames.size() != matcher.groupCount()) {
throw new IllegalArgumentException("The number of capturing groups in the pattern segment " +
this.pattern + " does not match the number of URI template variables it defines, " +
"which can occur if capturing groups are used in a URI template regex. " +
"Use non-capturing groups instead.");
}
for (int i = 1; i <= matcher.groupCount(); i++) {
String name = this.variableNames.get(i - 1);
String value = matcher.group(i);
uriTemplateVariables.put(name, value);
}
}
return true;
} else {
return false;
}
}
}
}