Support optional trailing separator on path pattern matching

This commit adds the ability for path patterns to automatically
match a trailing separator (so there is no need to add two
variants of a pattern, one with and one without). This behaviour
is currently turned off but a simple tweak in PathPatternParser
can make it the default. If made default other parts of Spring
may need altering (simplifying hopefully) to cope with this.

Issue: SPR-15260
This commit is contained in:
Andy Clement 2017-04-19 11:59:21 -07:00
parent e93e49f268
commit cd86558811
13 changed files with 404 additions and 139 deletions

View File

@ -49,13 +49,8 @@ class CaptureTheRestPathElement extends PathElement {
matchingContext.candidate[candidateIndex] != separator) {
return false;
}
while ((candidateIndex + 1) < matchingContext.candidateLength &&
matchingContext.candidate[candidateIndex + 1] == separator) {
candidateIndex++;
}
if (matchingContext.determineRemaining) {
if (matchingContext.determineRemainingPath) {
matchingContext.remainingPathIndex = matchingContext.candidateLength;
return true;
}
if (matchingContext.extractingVariables) {
matchingContext.set(variableName, new String(matchingContext.candidate, candidateIndex,

View File

@ -22,7 +22,8 @@ import org.springframework.web.util.patterns.PathPattern.MatchingContext;
/**
* A path element representing capturing a piece of the path as a variable. In the pattern
* '/foo/{bar}/goo' the {bar} is represented as a {@link CaptureVariablePathElement}.
* '/foo/{bar}/goo' the {bar} is represented as a {@link CaptureVariablePathElement}. There
* must be at least one character to bind to the variable.
*
* @author Andy Clement
*/
@ -66,6 +67,10 @@ class CaptureVariablePathElement extends PathElement {
@Override
public boolean matches(int candidateIndex, MatchingContext matchingContext) {
int nextPos = matchingContext.scanAhead(candidateIndex);
// There must be at least one character to capture:
if (nextPos == candidateIndex) {
return false;
}
CharSequence candidateCapture = null;
if (constraintPattern != null) {
// TODO possible optimization - only regex match if rest of pattern matches? Benefit likely to vary pattern to pattern
@ -80,13 +85,18 @@ class CaptureVariablePathElement extends PathElement {
}
boolean match = false;
if (next == null) {
if (matchingContext.determineRemaining && nextPos > candidateIndex) {
if (matchingContext.determineRemainingPath && nextPos > candidateIndex) {
matchingContext.remainingPathIndex = nextPos;
match = true;
}
else {
// Needs to be at least one character #SPR15264
match = (nextPos == matchingContext.candidateLength && nextPos > candidateIndex);
if (!match && matchingContext.isAllowOptionalTrailingSlash()) {
match = (nextPos > candidateIndex) &&
(nextPos + 1) == matchingContext.candidateLength &&
matchingContext.candidate[nextPos] == separator;
}
}
}
else {

View File

@ -35,6 +35,11 @@ public class InternalPathPatternParser {
// Is the parser producing case sensitive PathPattern matchers
boolean caseSensitive = true;
// If true the PathPatterns produced by the parser will allow patterns
// that don't have a trailing slash to match paths that may or may not
// have a trailing slash
private boolean matchOptionalTrailingSlash = false;
// The input data for parsing
private char[] pathPatternData;
@ -79,11 +84,14 @@ public class InternalPathPatternParser {
* Create a PatternParser that will use the specified separator instead of
* the default.
*
* @param separator the path separator to look for when parsing.
* @param separator the path separator to look for when parsing
* @param caseSensitive true if PathPatterns should be sensitive to case
* @param matchOptionalTrailingSlash true if patterns without a trailing slash can match paths that do have a trailing slash
*/
public InternalPathPatternParser(char separator, boolean caseSensitive) {
public InternalPathPatternParser(char separator, boolean caseSensitive, boolean matchOptionalTrailingSlash) {
this.separator = separator;
this.caseSensitive = caseSensitive;
this.matchOptionalTrailingSlash = matchOptionalTrailingSlash;
}
/**
@ -99,10 +107,6 @@ public class InternalPathPatternParser {
if (pathPattern == null) {
pathPattern = "";
}
// int starstar = pathPattern.indexOf("**");
// if (starstar!=-1 && starstar!=pathPattern.length()-2) {
// throw new IllegalStateException("Not allowed ** unless at end of pattern: "+pathPattern);
// }
pathPatternData = pathPattern.toCharArray();
pathPatternLength = pathPatternData.length;
headPE = null;
@ -117,10 +121,6 @@ public class InternalPathPatternParser {
if (pathElementStart != -1) {
pushPathElement(createPathElement());
}
// Skip over multiple separators
while ((pos + 1) < pathPatternLength && pathPatternData[pos + 1] == separator) {
pos++;
}
if (peekDoubleWildcard()) {
pushPathElement(new WildcardTheRestPathElement(pos, separator));
pos += 2;
@ -195,7 +195,7 @@ public class InternalPathPatternParser {
if (pathElementStart != -1) {
pushPathElement(createPathElement());
}
return new PathPattern(pathPattern, headPE, separator, caseSensitive);
return new PathPattern(pathPattern, headPE, separator, caseSensitive, matchOptionalTrailingSlash);
}
/**

View File

@ -69,12 +69,19 @@ class LiteralPathElement extends PathElement {
}
}
if (next == null) {
if (matchingContext.determineRemaining && nextIfExistsIsSeparator(candidateIndex, matchingContext)) {
if (matchingContext.determineRemainingPath && nextIfExistsIsSeparator(candidateIndex, matchingContext)) {
matchingContext.remainingPathIndex = candidateIndex;
return true;
}
else {
return candidateIndex == matchingContext.candidateLength;
if (candidateIndex == matchingContext.candidateLength) {
return true;
}
else {
return matchingContext.isAllowOptionalTrailingSlash() &&
(candidateIndex + 1) == matchingContext.candidateLength &&
matchingContext.candidate[candidateIndex] == separator;
}
}
}
else {

View File

@ -76,6 +76,9 @@ public class PathPattern implements Comparable<PathPattern> {
/** Will this match candidates in a case sensitive way? (case sensitivity at parse time) */
private boolean caseSensitive;
/** If this pattern has no trailing slash, allow candidates to include one and still match successfully */
boolean allowOptionalTrailingSlash;
/** How many variables are captured in this pattern */
private int capturedVariableCount;
@ -105,11 +108,12 @@ public class PathPattern implements Comparable<PathPattern> {
/** Does the pattern end with {*...} */
private boolean isCatchAll = false;
public PathPattern(String patternText, PathElement head, char separator, boolean caseSensitive) {
public PathPattern(String patternText, PathElement head, char separator, boolean caseSensitive, boolean allowOptionalTrailingSlash) {
this.head = head;
this.patternString = patternText;
this.separator = separator;
this.caseSensitive = caseSensitive;
this.allowOptionalTrailingSlash = allowOptionalTrailingSlash;
// Compute fields for fast comparison
PathElement s = head;
while (s != null) {
@ -251,11 +255,15 @@ public class PathPattern implements Comparable<PathPattern> {
// assert this.matches(path)
PathElement s = head;
int separatorCount = 0;
boolean matchTheRest = false;
// Find first path element that is pattern based
while (s != null) {
if (s instanceof SeparatorPathElement || s instanceof CaptureTheRestPathElement
|| s instanceof WildcardTheRestPathElement) {
separatorCount++;
if (s instanceof WildcardTheRestPathElement || s instanceof CaptureTheRestPathElement) {
matchTheRest = true;
}
}
if (s.getWildcardCount() != 0 || s.getCaptureCount() != 0) {
break;
@ -271,17 +279,15 @@ public class PathPattern implements Comparable<PathPattern> {
int pos = 0;
while (separatorCount > 0 && pos < len) {
if (path.charAt(pos++) == separator) {
// Skip any adjacent separators
while (pos < len && path.charAt(pos) == separator) {
pos++;
}
separatorCount--;
}
}
int end = len;
// Trim trailing separators
while (end > 0 && path.charAt(end - 1) == separator) {
end--;
if (!matchTheRest) {
while (end > 0 && path.charAt(end - 1) == separator) {
end--;
}
}
// Check if multiple separators embedded in the resulting path, if so trim them out.
// Example: aaa////bbb//ccc/d -> aaa/bbb/ccc/d
@ -425,7 +431,7 @@ public class PathPattern implements Comparable<PathPattern> {
boolean extractingVariables;
boolean determineRemaining = false;
boolean determineRemainingPath = false;
// if determineRemaining is true, this is set to the position in
// the candidate where the pattern finished matching - i.e. it
@ -438,11 +444,12 @@ public class PathPattern implements Comparable<PathPattern> {
this.extractingVariables = extractVariables;
}
/**
*
*/
public void setMatchAllowExtraPath() {
determineRemaining = true;
determineRemainingPath = true;
}
public boolean isAllowOptionalTrailingSlash() {
return allowOptionalTrailingSlash;
}
public void setMatchStartMatching(boolean b) {

View File

@ -32,6 +32,11 @@ public class PathPatternParser {
// The expected path separator to split path elements during parsing, default '/'
private char separator = DEFAULT_SEPARATOR;
// If true the PathPatterns produced by the parser will allow patterns
// that don't have a trailing slash to match paths that may or may not
// have a trailing slash
private boolean matchOptionalTrailingSlash = false;
/**
* Create a path pattern parser that will use the default separator '/' when
@ -40,6 +45,18 @@ public class PathPatternParser {
public PathPatternParser() {
}
/**
* Control behavior of the path patterns produced by this parser. The default
* value for matchOptionalTrailingSlash is true but here it can be set to false.
* If true then PathPatterns without a trailing slash will match paths with or
* without a trailing slash.
*
* @param matchOptionalTrailingSlash boolean value to override the default value of true
*/
public void setMatchOptionalTrailingSlash(boolean matchOptionalTrailingSlash) {
this.matchOptionalTrailingSlash = matchOptionalTrailingSlash;
}
/**
* Create a path pattern parser that will use the supplied separator when
* parsing patterns.
@ -64,7 +81,7 @@ public class PathPatternParser {
* @return a PathPattern for quickly matching paths against the specified path pattern
*/
public PathPattern parse(String pathPattern) {
InternalPathPatternParser ippp = new InternalPathPatternParser(separator, caseSensitive);
InternalPathPatternParser ippp = new InternalPathPatternParser(separator, caseSensitive, matchOptionalTrailingSlash);
return ippp.parse(pathPattern);
}

View File

@ -124,7 +124,7 @@ class RegexPathElement extends PathElement {
boolean matches = m.matches();
if (matches) {
if (next == null) {
if (matchingContext.determineRemaining &&
if (matchingContext.determineRemainingPath &&
((this.variableNames.size() == 0) ? true : p > candidateIndex)) {
matchingContext.remainingPathIndex = p;
matches = true;
@ -134,6 +134,11 @@ class RegexPathElement extends PathElement {
// If pattern is capturing variables there must be some actual data to bind to them
matches = (p == matchingContext.candidateLength &&
((this.variableNames.size() == 0) ? true : p > candidateIndex));
if (!matches && matchingContext.isAllowOptionalTrailingSlash()) {
matches = ((this.variableNames.size() == 0) ? true : p > candidateIndex) &&
(p + 1) == matchingContext.candidateLength &&
matchingContext.candidate[p] == separator;
}
}
}
else {

View File

@ -39,31 +39,24 @@ class SeparatorPathElement extends PathElement {
@Override
public boolean matches(int candidateIndex, MatchingContext matchingContext) {
boolean matched = false;
if (candidateIndex < matchingContext.candidateLength) {
if (matchingContext.candidate[candidateIndex] == separator) {
// Skip further separators in the path (they are all 'matched'
// by a single SeparatorPathElement)
while ((candidateIndex + 1) < matchingContext.candidateLength &&
matchingContext.candidate[candidateIndex + 1] == separator) {
candidateIndex++;
}
if (next == null) {
if (matchingContext.determineRemaining) {
matchingContext.remainingPathIndex = candidateIndex + 1;
matched = true;
}
else {
matched = ((candidateIndex + 1) == matchingContext.candidateLength);
}
if (candidateIndex < matchingContext.candidateLength &&
matchingContext.candidate[candidateIndex] == separator) {
if (next == null) {
if (matchingContext.determineRemainingPath) {
matchingContext.remainingPathIndex = candidateIndex + 1;
matched = true;
}
else {
candidateIndex++;
if (matchingContext.isMatchStartMatching && candidateIndex == matchingContext.candidateLength) {
return true; // no more data but matches up to this point
}
matched = next.matches(candidateIndex, matchingContext);
matched = ((candidateIndex + 1) == matchingContext.candidateLength);
}
}
else {
candidateIndex++;
if (matchingContext.isMatchStartMatching && candidateIndex == matchingContext.candidateLength) {
return true; // no more data but matches up to this point
}
matched = next.matches(candidateIndex, matchingContext);
}
}
return matched;
}

View File

@ -76,12 +76,19 @@ class SingleCharWildcardedPathElement extends PathElement {
}
}
if (next == null) {
if (matchingContext.determineRemaining && nextIfExistsIsSeparator(candidateIndex, matchingContext)) {
if (matchingContext.determineRemainingPath && nextIfExistsIsSeparator(candidateIndex, matchingContext)) {
matchingContext.remainingPathIndex = candidateIndex;
return true;
}
else {
return candidateIndex == matchingContext.candidateLength;
if (candidateIndex == matchingContext.candidateLength) {
return true;
}
else {
return matchingContext.isAllowOptionalTrailingSlash() &&
(candidateIndex + 1) == matchingContext.candidateLength &&
matchingContext.candidate[candidateIndex] == separator;
}
}
}
else {

View File

@ -20,7 +20,8 @@ import org.springframework.web.util.patterns.PathPattern.MatchingContext;
/**
* A wildcard path element. In the pattern '/foo/&ast;/goo' the * is
* represented by a WildcardPathElement.
* represented by a WildcardPathElement. Within a path it matches at least
* one character but at the end of a path it can match zero characters.
*
* @author Andy Clement
* @since 5.0
@ -32,26 +33,38 @@ class WildcardPathElement extends PathElement {
}
/**
* Matching on a WildcardPathElement is quite straight forward. Just scan the
* candidate from the candidateIndex for the next separator or the end of the
* Matching on a WildcardPathElement is quite straight forward. Scan the
* candidate from the candidateIndex onwards for the next separator or the end of the
* candidate.
*/
@Override
public boolean matches(int candidateIndex, MatchingContext matchingContext) {
int nextPos = matchingContext.scanAhead(candidateIndex);
if (next == null) {
if (matchingContext.determineRemaining) {
if (matchingContext.determineRemainingPath) {
matchingContext.remainingPathIndex = nextPos;
return true;
}
else {
return (nextPos == matchingContext.candidateLength);
if (nextPos == matchingContext.candidateLength) {
return true;
}
else {
return matchingContext.isAllowOptionalTrailingSlash() && // if optional slash is on...
nextPos > candidateIndex && // and there is at least one character to match the *...
(nextPos + 1) == matchingContext.candidateLength && // and the nextPos is the end of the candidate...
matchingContext.candidate[nextPos] == separator; // and the final character is a separator
}
}
}
else {
else {
if (matchingContext.isMatchStartMatching && nextPos == matchingContext.candidateLength) {
return true; // no more data but matches up to this point
}
// Within a path (e.g. /aa/*/bb) there must be at least one character to match the wildcard
if (nextPos == candidateIndex) {
return false;
}
return next.matches(nextPos, matchingContext);
}
}

View File

@ -38,7 +38,7 @@ class WildcardTheRestPathElement extends PathElement {
matchingContext.candidate[candidateIndex] != separator) {
return false;
}
if (matchingContext.determineRemaining) {
if (matchingContext.determineRemainingPath) {
matchingContext.remainingPathIndex = matchingContext.candidateLength;
}
return true;

View File

@ -38,6 +38,198 @@ import static org.junit.Assert.*;
*/
public class PathPatternMatcherTests {
@Test
public void basicMatching() {
checkMatches(null, null);
checkMatches("", "");
checkMatches("", null);
checkNoMatch("/abc", null);
checkMatches(null, "");
checkNoMatch(null, "/abc");
checkMatches("/", "/");
checkNoMatch("/", "/a");
checkMatches("f", "f");
checkMatches("/foo", "/foo");
checkMatches("/foo/", "/foo/");
checkMatches("/foo/bar", "/foo/bar");
checkMatches("foo/bar", "foo/bar");
checkMatches("/foo/bar/", "/foo/bar/");
checkMatches("foo/bar/", "foo/bar/");
checkMatches("/foo/bar/woo", "/foo/bar/woo");
checkNoMatch("foo", "foobar");
checkMatches("/foo/bar", "/foo/bar");
checkNoMatch("/foo/bar", "/foo/baz");
// TODO Need more tests for escaped separators in path patterns and paths?
checkMatches("/foo\\/bar", "/foo\\/bar"); // chain string is Separator(/) Literal(foo\) Separator(/) Literal(bar)
}
@Test
public void optionalTrailingSeparators() {
// LiteralPathElement
PathPattern pp = parse("/resource");
assertTrue(pp.matches("/resource"));
assertTrue(pp.matches("/resource/"));
assertFalse(pp.matches("/resource//"));
pp = parse("/resource/");
assertFalse(pp.matches("/resource"));
assertTrue(pp.matches("/resource/"));
assertFalse(pp.matches("/resource//"));
// SingleCharWildcardPathElement
pp = parse("/res?urce");
assertTrue(pp.matches("/resource"));
assertTrue(pp.matches("/resource/"));
assertFalse(pp.matches("/resource//"));
pp = parse("/res?urce/");
assertFalse(pp.matches("/resource"));
assertTrue(pp.matches("/resource/"));
assertFalse(pp.matches("/resource//"));
// CaptureVariablePathElement
pp = parse("/{var}");
assertTrue(pp.matches("/resource"));
assertEquals("resource",pp.matchAndExtract("/resource").get("var"));
assertTrue(pp.matches("/resource/"));
assertEquals("resource",pp.matchAndExtract("/resource/").get("var"));
assertFalse(pp.matches("/resource//"));
pp = parse("/{var}/");
assertFalse(pp.matches("/resource"));
assertTrue(pp.matches("/resource/"));
assertEquals("resource",pp.matchAndExtract("/resource/").get("var"));
assertFalse(pp.matches("/resource//"));
// CaptureTheRestPathElement
pp = parse("/{*var}");
assertTrue(pp.matches("/resource"));
assertEquals("/resource",pp.matchAndExtract("/resource").get("var"));
assertTrue(pp.matches("/resource/"));
assertEquals("/resource/",pp.matchAndExtract("/resource/").get("var"));
assertTrue(pp.matches("/resource//"));
assertEquals("/resource//",pp.matchAndExtract("/resource//").get("var"));
assertTrue(pp.matches("//resource//"));
assertEquals("//resource//",pp.matchAndExtract("//resource//").get("var"));
// WildcardTheRestPathElement
pp = parse("/**");
assertTrue(pp.matches("/resource"));
assertTrue(pp.matches("/resource/"));
assertTrue(pp.matches("/resource//"));
assertTrue(pp.matches("//resource//"));
// WildcardPathElement
pp = parse("/*");
assertTrue(pp.matches("/resource"));
assertTrue(pp.matches("/resource/"));
assertFalse(pp.matches("/resource//"));
pp = parse("/*/");
assertFalse(pp.matches("/resource"));
assertTrue(pp.matches("/resource/"));
assertFalse(pp.matches("/resource//"));
// RegexPathElement
pp = parse("/{var1}_{var2}");
assertTrue(pp.matches("/res1_res2"));
assertEquals("res1",pp.matchAndExtract("/res1_res2").get("var1"));
assertEquals("res2",pp.matchAndExtract("/res1_res2").get("var2"));
assertTrue(pp.matches("/res1_res2/"));
assertEquals("res1",pp.matchAndExtract("/res1_res2/").get("var1"));
assertEquals("res2",pp.matchAndExtract("/res1_res2/").get("var2"));
assertFalse(pp.matches("/res1_res2//"));
pp = parse("/{var1}_{var2}/");
assertFalse(pp.matches("/res1_res2"));
assertTrue(pp.matches("/res1_res2/"));
assertEquals("res1",pp.matchAndExtract("/res1_res2/").get("var1"));
assertEquals("res2",pp.matchAndExtract("/res1_res2/").get("var2"));
assertFalse(pp.matches("/res1_res2//"));
pp = parse("/{var1}*");
assertTrue(pp.matches("/a"));
assertTrue(pp.matches("/a/"));
assertFalse(pp.matches("/")); // no characters for var1
assertFalse(pp.matches("//")); // no characters for var1
// Now with trailing matching turned OFF
PathPatternParser parser = new PathPatternParser();
parser.setMatchOptionalTrailingSlash(false);
// LiteralPathElement
pp = parser.parse("/resource");
assertTrue(pp.matches("/resource"));
assertFalse(pp.matches("/resource/"));
assertFalse(pp.matches("/resource//"));
pp = parser.parse("/resource/");
assertFalse(pp.matches("/resource"));
assertTrue(pp.matches("/resource/"));
assertFalse(pp.matches("/resource//"));
// SingleCharWildcardPathElement
pp = parser.parse("/res?urce");
assertTrue(pp.matches("/resource"));
assertFalse(pp.matches("/resource/"));
assertFalse(pp.matches("/resource//"));
pp = parser.parse("/res?urce/");
assertFalse(pp.matches("/resource"));
assertTrue(pp.matches("/resource/"));
assertFalse(pp.matches("/resource//"));
// CaptureVariablePathElement
pp = parser.parse("/{var}");
assertTrue(pp.matches("/resource"));
assertEquals("resource",pp.matchAndExtract("/resource").get("var"));
assertFalse(pp.matches("/resource/"));
assertFalse(pp.matches("/resource//"));
pp = parser.parse("/{var}/");
assertFalse(pp.matches("/resource"));
assertTrue(pp.matches("/resource/"));
assertEquals("resource",pp.matchAndExtract("/resource/").get("var"));
assertFalse(pp.matches("/resource//"));
// CaptureTheRestPathElement
pp = parser.parse("/{*var}");
assertTrue(pp.matches("/resource"));
assertEquals("/resource",pp.matchAndExtract("/resource").get("var"));
assertTrue(pp.matches("/resource/"));
assertEquals("/resource/",pp.matchAndExtract("/resource/").get("var"));
assertTrue(pp.matches("/resource//"));
assertEquals("/resource//",pp.matchAndExtract("/resource//").get("var"));
assertTrue(pp.matches("//resource//"));
assertEquals("//resource//",pp.matchAndExtract("//resource//").get("var"));
// WildcardTheRestPathElement
pp = parser.parse("/**");
assertTrue(pp.matches("/resource"));
assertTrue(pp.matches("/resource/"));
assertTrue(pp.matches("/resource//"));
assertTrue(pp.matches("//resource//"));
// WildcardPathElement
pp = parser.parse("/*");
assertTrue(pp.matches("/resource"));
assertFalse(pp.matches("/resource/"));
assertFalse(pp.matches("/resource//"));
pp = parser.parse("/*/");
assertFalse(pp.matches("/resource"));
assertTrue(pp.matches("/resource/"));
assertFalse(pp.matches("/resource//"));
// RegexPathElement
pp = parser.parse("/{var1}_{var2}");
assertTrue(pp.matches("/res1_res2"));
assertEquals("res1",pp.matchAndExtract("/res1_res2").get("var1"));
assertEquals("res2",pp.matchAndExtract("/res1_res2").get("var2"));
assertFalse(pp.matches("/res1_res2/"));
assertFalse(pp.matches("/res1_res2//"));
pp = parser.parse("/{var1}_{var2}/");
assertFalse(pp.matches("/res1_res2"));
assertTrue(pp.matches("/res1_res2/"));
assertEquals("res1",pp.matchAndExtract("/res1_res2/").get("var1"));
assertEquals("res2",pp.matchAndExtract("/res1_res2/").get("var2"));
assertFalse(pp.matches("/res1_res2//"));
pp = parser.parse("/{var1}*");
assertTrue(pp.matches("/a"));
assertFalse(pp.matches("/a/"));
assertFalse(pp.matches("/")); // no characters for var1
assertFalse(pp.matches("//")); // no characters for var1
}
@Test
public void pathRemainderBasicCases_spr15336() {
// Cover all PathElement kinds
@ -92,31 +284,6 @@ public class PathPatternMatcherTests {
assertNull(parse("").getPathRemaining(null).getPathRemaining());
}
@Test
public void basicMatching() {
checkMatches(null, null);
checkMatches("", "");
checkMatches("", null);
checkNoMatch("/abc", null);
checkMatches(null, "");
checkNoMatch(null, "/abc");
checkMatches("/", "/");
checkNoMatch("/", "/a");
checkMatches("f", "f");
checkMatches("/foo", "/foo");
checkMatches("/foo/", "/foo/");
checkMatches("/foo/bar", "/foo/bar");
checkMatches("foo/bar", "foo/bar");
checkMatches("/foo/bar/", "/foo/bar/");
checkMatches("foo/bar/", "foo/bar/");
checkMatches("/foo/bar/woo", "/foo/bar/woo");
checkNoMatch("foo", "foobar");
checkMatches("/foo/bar", "/foo/bar");
checkNoMatch("/foo/bar", "/foo/baz");
// TODO Need more tests for escaped separators in path patterns and paths?
checkMatches("/foo\\/bar", "/foo\\/bar"); // chain string is Separator(/) Literal(foo\) Separator(/) Literal(bar)
}
@Test
public void questionMarks() {
checkNoMatch("a", "ab");
@ -144,38 +311,62 @@ public class PathPatternMatcherTests {
checkCapture("/customer/{*something}", "/customer/aa/bb/cc", "something",
"/aa/bb/cc");
checkCapture("/customer/{*something}", "/customer/", "something", "/");
checkCapture("/customer/////{*something}", "/customer/", "something", "/");
checkCapture("/customer/{*something}", "/customer//////99", "something", "/99");
checkCapture("/customer///{*something}", "/customer//////99", "something", "/99");
checkCapture("/customer/////{*something}", "/customer/////", "something", "/");
checkCapture("/customer/////{*something}", "/customer//////", "something", "//");
checkCapture("/customer//////{*something}", "/customer//////99", "something", "/99");
checkCapture("/customer//////{*something}", "/customer//////99", "something", "/99");
checkCapture("/customer/{*something}", "/customer", "something", "");
checkCapture("/{*something}", "", "something", "");
checkCapture("/customer/{*something}", "/customer//////99", "something", "//////99");
}
@Test
public void multipleSelectorsInPattern() {
checkMatches("///abc", "/abc");
checkMatches("//", "/");
checkMatches("abc", "abc");
checkMatches("///abc//d/e", "/abc/d/e");
checkMatches("///abc//{def}//////xyz", "/abc/foo/xyz");
public void multipleSeparatorsInPattern() {
PathPattern pp = parse("a//b//c");
assertEquals("Literal(a) Separator(/) Separator(/) Literal(b) Separator(/) Separator(/) Literal(c)",pp.toChainString());
assertTrue(pp.matches("a//b//c"));
assertEquals("Literal(a) Separator(/) WildcardTheRest(/**)",parse("a//**").toChainString());
checkMatches("///abc", "///abc");
checkNoMatch("///abc", "/abc");
checkNoMatch("//", "/");
checkMatches("//", "//");
checkNoMatch("///abc//d/e", "/abc/d/e");
checkMatches("///abc//d/e", "///abc//d/e");
checkNoMatch("///abc//{def}//////xyz", "/abc/foo/xyz");
checkMatches("///abc//{def}//////xyz", "///abc//p//////xyz");
}
@Test
public void multipleSelectorsInPath() {
checkMatches("/abc", "////abc");
checkMatches("/", "//");
checkMatches("/abc//def///ghi", "/abc/def/ghi");
checkNoMatch("/abc", "////abc");
checkNoMatch("/", "//");
checkNoMatch("/abc/def/ghi", "/abc//def///ghi");
checkNoMatch("/abc", "////abc");
checkMatches("////abc", "////abc");
checkNoMatch("/", "//");
checkNoMatch("/abc//def///ghi", "/abc/def/ghi");
checkMatches("/abc//def///ghi", "/abc//def///ghi");
}
@Test
public void multipleSelectorsInPatternAndPath() {
checkMatches("///one///two///three", "//one/////two///////three");
checkMatches("//one//two//three", "/one/////two/three");
checkCapture("///{foo}///bar", "/one/bar", "foo", "one");
public void multipleSeparatorsInPatternAndPath() {
checkNoMatch("///one///two///three", "//one/////two///////three");
checkMatches("//one/////two///////three", "//one/////two///////three");
checkNoMatch("//one//two//three", "/one/////two/three");
checkMatches("/one/////two/three", "/one/////two/three");
checkCapture("///{foo}///bar", "///one///bar", "foo", "one");
}
@Test
public void wildcards() {
checkMatches("/*/bar", "/foo/bar");
checkNoMatch("/*/bar", "/foo/baz");
checkNoMatch("/*/bar", "//bar");
checkMatches("/f*/bar", "/foo/bar");
checkMatches("/*/bar", "/foo/bar");
checkMatches("a/*","a/");
checkMatches("/*","/");
checkMatches("/*/bar", "/foo/bar");
checkNoMatch("/*/bar", "/foo/baz");
checkMatches("/f*/bar", "/foo/bar");
@ -183,20 +374,19 @@ public class PathPatternMatcherTests {
checkMatches("/a*b*c*d/bar", "/abcd/bar");
checkMatches("*a*", "testa");
checkMatches("a/*", "a/");
checkNoMatch("a/*", "a//"); // trailing slash, so is allowed
checkMatches("a/*", "a/a/"); // trailing slash, so is allowed
PathPatternParser ppp = new PathPatternParser();
ppp.setMatchOptionalTrailingSlash(false);
assertFalse(ppp.parse("a/*").matches("a//"));
checkMatches("a/*", "a/a");
checkNoMatch("a/*", "a/a/");
checkMatches("a/*", "a/a/"); // trailing slash is optional
checkMatches("/resource/**", "/resource");
checkNoMatch("/resource/**", "/resourceX");
checkNoMatch("/resource/**", "/resourceX/foobar");
checkMatches("/resource/**", "/resource/foobar");
}
@Test
public void trailingSeparators() {
checkNoMatch("aaa/", "aaa");
}
@Test
public void constrainedMatches() {
checkCapture("{foo:[0-9]*}", "123", "foo", "123");
@ -246,7 +436,8 @@ public class PathPatternMatcherTests {
checkMatches("test*aaa", "testblaaaa");
checkNoMatch("test*", "tst");
checkNoMatch("test*", "tsttest");
checkNoMatch("test*", "test/");
checkMatches("test*", "test/"); // trailing slash is optional
checkMatches("test*", "test"); // trailing slash is optional
checkNoMatch("test*", "test/t");
checkNoMatch("test/*", "test");
checkNoMatch("*test*", "tsttst");
@ -265,8 +456,6 @@ public class PathPatternMatcherTests {
checkMatches("/**", "");
checkMatches("/books/**", "/books");
checkMatches("/books////**", "/books");
checkMatches("/books////**", "/books////");
checkMatches("/**", "/testing/testing");
checkMatches("/*/**", "/testing/testing");
checkMatches("/bla*bla/test", "/blaXXXbla/test");
@ -314,6 +503,12 @@ public class PathPatternMatcherTests {
@Test
public void matchStart() {
checkStartNoMatch("test/*/","test//");
checkStartMatches("test/*","test/abc");
checkStartMatches("test/*/def","test/abc/def");
checkStartNoMatch("test/*/def","test//");
checkStartNoMatch("test/*/def","test//def");
checkStartMatches("test/{a}_{b}/foo", "test/a_b");
checkStartMatches("test/?/abc", "test/a");
checkStartMatches("test/{*foobar}", "test/");
@ -357,7 +552,8 @@ public class PathPatternMatcherTests {
checkStartMatches("*.*", "test.test.test");
checkStartMatches("test*aaa", "testblaaaa");
checkStartNoMatch("test*", "tst");
checkStartNoMatch("test*", "test/");
checkStartMatches("test*", "test/"); // trailing slash is optional
checkStartMatches("test*", "test");
checkStartNoMatch("test*", "tsttest");
checkStartNoMatch("test*", "test/t");
checkStartMatches("test/*", "test");
@ -398,7 +594,7 @@ public class PathPatternMatcherTests {
"/XXXblaXXXX/testing/bla/testing/testing.jpg");
checkStartMatches("/abc/{foo}", "/abc/def");
checkStartNoMatch("/abc/{foo}", "/abc/def/");
checkStartMatches("/abc/{foo}", "/abc/def/"); // trailing slash is optional
checkStartMatches("/abc/{foo}/", "/abc/def/");
checkStartNoMatch("/abc/{foo}/", "/abc/def/ghi");
checkStartMatches("/abc/{foo}/", "/abc/def");
@ -556,8 +752,8 @@ public class PathPatternMatcherTests {
@Test
public void extractPathWithinPattern_spr15259() {
checkExtractPathWithinPattern("/**","//","/");
checkExtractPathWithinPattern("/**","/","");
checkExtractPathWithinPattern("/**","//","");
checkExtractPathWithinPattern("/**","","");
checkExtractPathWithinPattern("/**","/foobar","foobar");
}
@ -579,10 +775,8 @@ public class PathPatternMatcherTests {
checkExtractPathWithinPattern("/a/b/c*d*/*.html", "/a/b/cod/foo.html", "cod/foo.html");
checkExtractPathWithinPattern("a/{foo}/b/{bar}", "a/c/b/d", "c/b/d");
checkExtractPathWithinPattern("a/{foo}_{bar}/d/e", "a/b_c/d/e", "b_c/d/e");
checkExtractPathWithinPattern("aaa//*///ccc///ddd", "aaa/bbb/ccc/ddd", "bbb/ccc/ddd");
checkExtractPathWithinPattern("aaa/*/ccc/ddd", "aaa//bbb//ccc/ddd", "bbb/ccc/ddd");
checkExtractPathWithinPattern("aaa//*///ccc///ddd", "aaa//bbb///ccc///ddd", "bbb/ccc/ddd");
checkExtractPathWithinPattern("aaa//*///ccc///ddd", "aaa//bbb//ccc/ddd", "bbb/ccc/ddd");
checkExtractPathWithinPattern("aaa//*///ccc///ddd", "aaa/////bbb//ccc/ddd", "bbb/ccc/ddd");
checkExtractPathWithinPattern("aaa/c*/ddd/", "aaa/ccc///ddd///", "ccc/ddd");
checkExtractPathWithinPattern("", "", "");
checkExtractPathWithinPattern("/", "", "");
@ -604,13 +798,18 @@ public class PathPatternMatcherTests {
pp = new PathPatternParser().parse("/{foo}/{bar}");
assertTrue(pp.matches("/abc/def"));
assertFalse(pp.matches("/def"));
assertFalse(pp.matches("/"));
assertFalse(pp.matches("//def"));
assertFalse(pp.matches("//"));
pp = parse("/{foo}/boo");
assertTrue(pp.matches("/abc/boo"));
assertTrue(pp.matches("/a/boo"));
assertFalse(pp.matches("/boo"));
assertFalse(pp.matches("//boo"));
pp = parse("/{foo}*");
assertTrue(pp.matches("/abc"));
@ -630,11 +829,14 @@ public class PathPatternMatcherTests {
checkCapture("/{foo:[a-z][a-z]}{bar:[a-z]}", "/abc", "foo", "ab", "bar", "c");
// Only patterns not capturing variables cannot match against just /
pp = new PathPatternParser().parse("/****");
PathPatternParser ppp = new PathPatternParser();
ppp.setMatchOptionalTrailingSlash(true);
pp = ppp.parse("/****");
assertTrue(pp.matches("/abcdef"));
assertTrue(pp.matches("/"));
assertTrue(pp.matches("/"));
assertTrue(pp.matches("//"));
// Confirming AntPathMatcher behaviour:
assertFalse(new AntPathMatcher().match("/{foo}", "/"));
assertTrue(new AntPathMatcher().match("/{foo}", "/a"));
@ -682,6 +884,7 @@ public class PathPatternMatcherTests {
checkCapture("/{bla}.*", "/testing.html", "bla", "testing");
Map<String, String> extracted = checkCapture("/abc", "/abc");
assertEquals(0, extracted.size());
checkCapture("/{bla}/foo","/a/foo");
}
@Test
@ -990,6 +1193,7 @@ public class PathPatternMatcherTests {
private PathPattern parse(String path) {
PathPatternParser pp = new PathPatternParser();
pp.setMatchOptionalTrailingSlash(true);
return pp.parse(path);
}
@ -998,18 +1202,21 @@ public class PathPatternMatcherTests {
private void checkMatches(String uriTemplate, String path) {
PathPatternParser parser = (separator == PathPatternParser.DEFAULT_SEPARATOR
? new PathPatternParser() : new PathPatternParser(separator));
parser.setMatchOptionalTrailingSlash(true);
PathPattern p = parser.parse(uriTemplate);
assertTrue(p.matches(path));
}
private void checkStartNoMatch(String uriTemplate, String path) {
PathPatternParser p = new PathPatternParser();
p.setMatchOptionalTrailingSlash(true);
PathPattern pattern = p.parse(uriTemplate);
assertFalse(pattern.matchStart(path));
}
private void checkStartMatches(String uriTemplate, String path) {
PathPatternParser p = new PathPatternParser();
p.setMatchOptionalTrailingSlash(true);
PathPattern pattern = p.parse(uriTemplate);
assertTrue(pattern.matchStart(path));
}

View File

@ -41,7 +41,7 @@ public class PathPatternParserTests {
checkStructure("foo");
checkStructure("foo/");
checkStructure("/foo/");
checkStructure("//");
checkStructure("");
}
@Test
@ -49,7 +49,7 @@ public class PathPatternParserTests {
p = checkStructure("?");
assertPathElements(p, SingleCharWildcardedPathElement.class);
checkStructure("/?/");
checkStructure("//?abc?/");
checkStructure("/?abc?/");
}
@Test
@ -175,7 +175,7 @@ public class PathPatternParserTests {
p = checkStructure("{foo}");
assertEquals(CaptureVariablePathElement.class.getName(), p.getHeadSection().getClass().getName());
checkStructure("/{foo}");
checkStructure("//{f}/");
checkStructure("/{f}/");
checkStructure("/{foo}/{bar}/{wibble}");
}
@ -208,13 +208,13 @@ public class PathPatternParserTests {
checkError("{", 1, PatternMessage.MISSING_CLOSE_CAPTURE);
checkError("{abc", 4, PatternMessage.MISSING_CLOSE_CAPTURE);
checkError("{/}", 1, PatternMessage.MISSING_CLOSE_CAPTURE);
checkError("//{", 3, PatternMessage.MISSING_CLOSE_CAPTURE);
checkError("/{", 2, PatternMessage.MISSING_CLOSE_CAPTURE);
checkError("}", 0, PatternMessage.MISSING_OPEN_CAPTURE);
checkError("/}", 1, PatternMessage.MISSING_OPEN_CAPTURE);
checkError("def}", 3, PatternMessage.MISSING_OPEN_CAPTURE);
checkError("//{/}", 3, PatternMessage.MISSING_CLOSE_CAPTURE);
checkError("//{{/}", 3, PatternMessage.ILLEGAL_NESTED_CAPTURE);
checkError("//{abc{/}", 6, PatternMessage.ILLEGAL_NESTED_CAPTURE);
checkError("/{/}", 2, PatternMessage.MISSING_CLOSE_CAPTURE);
checkError("/{{/}", 2, PatternMessage.ILLEGAL_NESTED_CAPTURE);
checkError("/{abc{/}", 5, PatternMessage.ILLEGAL_NESTED_CAPTURE);
checkError("/{0abc}/abc", 2, PatternMessage.ILLEGAL_CHARACTER_AT_START_OF_CAPTURE_DESCRIPTOR);
checkError("/{a?bc}/abc", 3, PatternMessage.ILLEGAL_CHARACTER_IN_CAPTURE_DESCRIPTOR);
checkError("/{abc}_{abc}", 1, PatternMessage.ILLEGAL_DOUBLE_CAPTURE);
@ -294,15 +294,19 @@ public class PathPatternParserTests {
@Test
public void multipleSeparatorPatterns() {
p = checkStructure("///aaa");
assertEquals(4, p.getNormalizedLength());
assertPathElements(p, SeparatorPathElement.class, LiteralPathElement.class);
assertEquals(6, p.getNormalizedLength());
assertPathElements(p, SeparatorPathElement.class, SeparatorPathElement.class,
SeparatorPathElement.class, LiteralPathElement.class);
p = checkStructure("///aaa////aaa/b");
assertEquals(10, p.getNormalizedLength());
assertPathElements(p, SeparatorPathElement.class, LiteralPathElement.class,
SeparatorPathElement.class, LiteralPathElement.class, SeparatorPathElement.class, LiteralPathElement.class);
assertEquals(15, p.getNormalizedLength());
assertPathElements(p, SeparatorPathElement.class, SeparatorPathElement.class,
SeparatorPathElement.class, LiteralPathElement.class, SeparatorPathElement.class,
SeparatorPathElement.class, SeparatorPathElement.class, SeparatorPathElement.class,
LiteralPathElement.class, SeparatorPathElement.class, LiteralPathElement.class);
p = checkStructure("/////**");
assertEquals(1, p.getNormalizedLength());
assertPathElements(p, WildcardTheRestPathElement.class);
assertEquals(5, p.getNormalizedLength());
assertPathElements(p, SeparatorPathElement.class, SeparatorPathElement.class,
SeparatorPathElement.class, SeparatorPathElement.class, WildcardTheRestPathElement.class);
}
@Test