Support wildcard path elements at the start of path patterns

Prior to this commit, the `PathPattern` and `PathPatternParser` would
allow multiple-segments matching and capturing with the following:

* "/files/**" (matching 0-N segments until the end)
* "/files/{*path}" (matching 0-N segments until the end and capturing
  the value as the "path" variable)

This would be only allowed as the last path element in the pattern and
the parser would reject other combinations.

This commit expands the support and allows multiple segments matching at
the beginning of the path:

* "/**/index.html" (matching 0-N segments from the start)
* "/{*path}/index.html" (matching 0-N segments until the end and capturing
  the value as the "path" variable)

This does come with additional restrictions:

1. "/files/**/file.txt" and "/files/{*path}/file.txt" are invalid,
   as multiple segment matching is not allowed in the middle of the
   pattern.
2. "/{*path}/files/**" is not allowed, as a single "{*path}" or "/**"
   element is allowed in a pattern
3. "/{*path}/{folder}/file.txt"  "/**/{folder:[a-z]+}/file.txt" are
   invalid because only a literal pattern is allowed right after
   multiple segments path elements.

Closes gh-35213
This commit is contained in:
Brian Clozel
2025-07-25 12:34:40 +02:00
parent ed2cad3af0
commit d06255214e
10 changed files with 864 additions and 640 deletions
@@ -20,6 +20,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.http.server.PathContainer;
@@ -36,66 +37,303 @@ import static org.assertj.core.api.Assertions.fail;
*
* @author Andy Clement
* @author Sam Brannen
* @author Brian Clozel
*/
class PathPatternParserTests {
private PathPattern pathPattern;
@Test
void basicPatterns() {
checkStructure("/");
checkStructure("/foo");
checkStructure("foo");
checkStructure("foo/");
checkStructure("/foo/");
checkStructure("");
/**
* Verify that the parsed pattern matches
* the text and path elements of the original pattern.
*/
@Nested
class StructureTests {
@Test
void literalPatterns() {
checkStructure("/");
checkStructure("/foo");
checkStructure("foo");
checkStructure("foo/");
checkStructure("/foo/");
checkStructure("");
}
@Test
void singleCharWildcardPatterns() {
pathPattern = checkStructure("?");
assertPathElements(pathPattern, SingleCharWildcardedPathElement.class);
checkStructure("/?/");
checkStructure("/?abc?/");
}
@Test
void wildcardSegmentsStartOfPathPatterns() {
pathPattern = checkStructure("/**/foo");
assertPathElements(pathPattern, WildcardSegmentsPathElement.class, SeparatorPathElement.class, LiteralPathElement.class);
}
@Test
void wildcardSegmentEndOfPathPatterns() {
pathPattern = checkStructure("/**");
assertPathElements(pathPattern, WildcardSegmentsPathElement.class);
pathPattern = checkStructure("/foo/**");
assertPathElements(pathPattern, SeparatorPathElement.class, LiteralPathElement.class, WildcardSegmentsPathElement.class);
}
@Test
void regexpSegmentIsNotWildcardSegment() {
// this is not double wildcard, it's / then **acb (an odd, unnecessary use of double *)
pathPattern = checkStructure("/**acb");
assertPathElements(pathPattern, SeparatorPathElement.class, RegexPathElement.class);
}
@Test
void partialCapturingPatterns() {
pathPattern = checkStructure("{foo}abc");
assertPathElements(pathPattern, RegexPathElement.class);
checkStructure("abc{foo}");
checkStructure("/abc{foo}");
checkStructure("{foo}def/");
checkStructure("/abc{foo}def/");
checkStructure("{foo}abc{bar}");
checkStructure("{foo}abc{bar}/");
checkStructure("/{foo}abc{bar}/");
}
@Test
void completeCapturingPatterns() {
pathPattern = checkStructure("{foo}");
assertPathElements(pathPattern, CaptureVariablePathElement.class);
checkStructure("/{foo}");
checkStructure("/{f}/");
checkStructure("/{foo}/{bar}/{wibble}");
checkStructure("/{mobile-number}"); // gh-23101
}
@Test
void completeCaptureWithConstraints() {
pathPattern = checkStructure("{foo:...}");
assertPathElements(pathPattern, CaptureVariablePathElement.class);
pathPattern = checkStructure("{foo:[0-9]*}");
assertPathElements(pathPattern, CaptureVariablePathElement.class);
}
@Test
void captureSegmentsStartOfPathPatterns() {
pathPattern = checkStructure("/{*foobar}");
assertPathElements(pathPattern, CaptureSegmentsPathElement.class);
pathPattern = checkStructure("/{*foobar}/foo");
assertPathElements(pathPattern, CaptureSegmentsPathElement.class, SeparatorPathElement.class, LiteralPathElement.class);
}
@Test
void captureSegmentsEndOfPathPatterns() {
pathPattern = parse("{*foobar}");
assertThat(pathPattern.computePatternString()).isEqualTo("/{*foobar}");
assertPathElements(pathPattern, CaptureSegmentsPathElement.class);
pathPattern = checkStructure("/{*foobar}");
assertPathElements(pathPattern, CaptureSegmentsPathElement.class);
pathPattern = checkStructure("/foo/{*foobar}");
assertPathElements(pathPattern, SeparatorPathElement.class, LiteralPathElement.class, CaptureSegmentsPathElement.class);
}
@Test
void multipleSeparatorPatterns() {
pathPattern = checkStructure("///aaa");
assertPathElements(pathPattern, SeparatorPathElement.class, SeparatorPathElement.class,
SeparatorPathElement.class, LiteralPathElement.class);
pathPattern = checkStructure("///aaa////aaa/b");
assertPathElements(pathPattern, SeparatorPathElement.class, SeparatorPathElement.class,
SeparatorPathElement.class, LiteralPathElement.class, SeparatorPathElement.class,
SeparatorPathElement.class, SeparatorPathElement.class, SeparatorPathElement.class,
LiteralPathElement.class, SeparatorPathElement.class, LiteralPathElement.class);
pathPattern = checkStructure("/////**");
assertPathElements(pathPattern, SeparatorPathElement.class, SeparatorPathElement.class,
SeparatorPathElement.class, SeparatorPathElement.class, WildcardSegmentsPathElement.class);
}
@Test
void regexPathElementPatterns() {
pathPattern = checkStructure("/{var:\\\\}");
assertPathElements(pathPattern, SeparatorPathElement.class, CaptureVariablePathElement.class);
pathPattern = checkStructure("/{var:\\/}");
assertPathElements(pathPattern, SeparatorPathElement.class, CaptureVariablePathElement.class);
pathPattern = checkStructure("/{var:a{1,2}}");
assertPathElements(pathPattern, SeparatorPathElement.class, CaptureVariablePathElement.class);
pathPattern = checkStructure("/{var:[^\\/]*}");
assertPathElements(pathPattern, SeparatorPathElement.class, CaptureVariablePathElement.class);
pathPattern = checkStructure("/{var:\\[*}");
assertPathElements(pathPattern, SeparatorPathElement.class, CaptureVariablePathElement.class);
pathPattern = checkStructure("/{var:[\\{]*}");
assertPathElements(pathPattern, SeparatorPathElement.class, CaptureVariablePathElement.class);
pathPattern = checkStructure("/{var:[\\}]*}");
assertPathElements(pathPattern, SeparatorPathElement.class, CaptureVariablePathElement.class);
pathPattern = checkStructure("*");
assertPathElements(pathPattern, WildcardPathElement.class);
checkStructure("/*");
checkStructure("/*/");
checkStructure("*/");
checkStructure("/*/");
pathPattern = checkStructure("/*a*/");
assertPathElements(pathPattern, SeparatorPathElement.class, RegexPathElement.class, SeparatorPathElement.class);
pathPattern = checkStructure("*/");
assertPathElements(pathPattern, WildcardPathElement.class, SeparatorPathElement.class);
pathPattern = checkStructure("{symbolicName:[\\p{L}\\.]+}-sources-{version:[\\p{N}\\.]+}.jar");
assertPathElements(pathPattern, RegexPathElement.class);
}
private PathPattern checkStructure(String pattern) {
PathPatternParser patternParser = new PathPatternParser();
PathPattern pp = patternParser.parse(pattern);
assertThat(pp.computePatternString()).isEqualTo(pattern);
return pp;
}
@SafeVarargs
final void assertPathElements(PathPattern p, Class<? extends PathElement>... sectionClasses) {
PathElement head = p.getHeadSection();
for (Class<? extends PathElement> sectionClass : sectionClasses) {
if (head == null) {
fail("Ran out of data in parsed pattern. Pattern is: " + p.toChainString());
}
assertThat(head.getClass().getSimpleName()).as("Not expected section type. Pattern is: " + p.toChainString()).isEqualTo(sectionClass.getSimpleName());
head = head.next;
}
}
}
@Test
void singleCharWildcardPatterns() {
pathPattern = checkStructure("?");
assertPathElements(pathPattern, SingleCharWildcardedPathElement.class);
checkStructure("/?/");
checkStructure("/?abc?/");
@Nested
class ParsingErrorTests {
@Test
void captureSegmentsIllegalSyntax() {
checkError("/{*foobar}abc", 1, PatternMessage.CAPTURE_ALL_IS_STANDALONE_CONSTRUCT);
checkError("/{*f%obar}", 4, PatternMessage.ILLEGAL_CHARACTER_IN_CAPTURE_DESCRIPTOR);
checkError("/{*foobar}abc", 1, PatternMessage.CAPTURE_ALL_IS_STANDALONE_CONSTRUCT);
checkError("/{f*oobar}", 3, PatternMessage.ILLEGAL_CHARACTER_IN_CAPTURE_DESCRIPTOR);
checkError("/{*foobar:.*}/abc", 9, PatternMessage.ILLEGAL_CHARACTER_IN_CAPTURE_DESCRIPTOR);
checkError("/{abc}{*foobar}", 1, PatternMessage.CAPTURE_ALL_IS_STANDALONE_CONSTRUCT);
checkError("/{abc}{*foobar}{foo}", 1, PatternMessage.CAPTURE_ALL_IS_STANDALONE_CONSTRUCT);
checkError("/{*foo}/foo/{*bar}", 18, PatternMessage.CANNOT_HAVE_MANY_MULTISEGMENT_PATHELEMENTS);
checkError("/{*foo}/{bar}", 8, PatternMessage.MULTISEGMENT_PATHELEMENT_NOT_FOLLOWED_BY_LITERAL);
checkError("{foo:}", 5, PatternMessage.MISSING_REGEX_CONSTRAINT);
checkError("{foo}_{foo}", 0, PatternMessage.ILLEGAL_DOUBLE_CAPTURE, "foo");
checkError("/{bar}/{bar}", 7, PatternMessage.ILLEGAL_DOUBLE_CAPTURE, "bar");
checkError("/{bar}/{bar}_{foo}", 7, PatternMessage.ILLEGAL_DOUBLE_CAPTURE, "bar");
}
@Test
void regexpSegmentsIllegalSyntax() {
checkError("/{var:[^/]*}", 8, PatternMessage.MISSING_CLOSE_CAPTURE);
checkError("/{var:abc", 8, PatternMessage.MISSING_CLOSE_CAPTURE);
// Do not check the expected position due a change in RegEx parsing in JDK 13.
// See https://github.com/spring-projects/spring-framework/issues/23669
checkError("/{var:a{{1,2}}}", PatternMessage.REGEX_PATTERN_SYNTAX_EXCEPTION);
}
@Test
void illegalCapturePatterns() {
checkError("{abc/", 4, PatternMessage.MISSING_CLOSE_CAPTURE);
checkError("{abc:}/", 5, PatternMessage.MISSING_REGEX_CONSTRAINT);
checkError("{", 1, PatternMessage.MISSING_CLOSE_CAPTURE);
checkError("{abc", 4, PatternMessage.MISSING_CLOSE_CAPTURE);
checkError("{/}", 1, 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("/{/}", 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);
checkError("/foobar/{abc}_{abc}", 8, PatternMessage.ILLEGAL_DOUBLE_CAPTURE);
checkError("/foobar/{abc:..}_{abc:..}", 8, PatternMessage.ILLEGAL_DOUBLE_CAPTURE);
}
@Test
void captureGroupInRegexpNotAllowed() {
PathPattern pp = parse("/{abc:foo(bar)}");
assertThatIllegalArgumentException().isThrownBy(() ->
pp.matchAndExtract(PathContainer.parsePath("/foo")))
.withMessage("No capture groups allowed in the constraint regex: foo(bar)");
assertThatIllegalArgumentException().isThrownBy(() ->
pp.matchAndExtract(PathContainer.parsePath("/foobar")))
.withMessage("No capture groups allowed in the constraint regex: foo(bar)");
}
@Test
void badPatterns() {
//checkError("/{foo}{bar}/",6,PatternMessage.CANNOT_HAVE_ADJACENT_CAPTURES);
checkError("/{?}/", 2, PatternMessage.ILLEGAL_CHARACTER_AT_START_OF_CAPTURE_DESCRIPTOR, "?");
checkError("/{a?b}/", 3, PatternMessage.ILLEGAL_CHARACTER_IN_CAPTURE_DESCRIPTOR, "?");
checkError("/{%%$}", 2, PatternMessage.ILLEGAL_CHARACTER_AT_START_OF_CAPTURE_DESCRIPTOR, "%");
checkError("/{ }", 2, PatternMessage.ILLEGAL_CHARACTER_AT_START_OF_CAPTURE_DESCRIPTOR, " ");
checkError("/{%:[0-9]*}", 2, PatternMessage.ILLEGAL_CHARACTER_AT_START_OF_CAPTURE_DESCRIPTOR, "%");
}
@Test
void captureTheRestWithinPatternNotSupported() {
PathPatternParser parser = new PathPatternParser();
assertThatThrownBy(() -> parser.parse("/resources/**/details"))
.isInstanceOf(PatternParseException.class)
.extracting("messageType").isEqualTo(PatternMessage.INVALID_LOCATION_FOR_MULTISEGMENT_PATHELEMENT);
}
/**
* Delegates to {@link #checkError(String, int, PatternMessage, String...)},
* passing {@code -1} as the {@code expectedPos}.
* @since 5.2
*/
private void checkError(String pattern, PatternMessage expectedMessage, String... expectedInserts) {
checkError(pattern, -1, expectedMessage, expectedInserts);
}
/**
* @param expectedPos the expected position, or {@code -1} if the position should not be checked
*/
private void checkError(String pattern, int expectedPos, PatternMessage expectedMessage,
String... expectedInserts) {
assertThatExceptionOfType(PatternParseException.class)
.isThrownBy(() -> pathPattern = parse(pattern))
.satisfies(ex -> {
if (expectedPos >= 0) {
assertThat(ex.getPosition()).as(ex.toDetailedString()).isEqualTo(expectedPos);
}
assertThat(ex.getMessageType()).as(ex.toDetailedString()).isEqualTo(expectedMessage);
if (expectedInserts.length != 0) {
assertThat(ex.getInserts()).isEqualTo(expectedInserts);
}
});
}
}
@Test
void multiwildcardPattern() {
pathPattern = checkStructure("/**");
assertPathElements(pathPattern, WildcardTheRestPathElement.class);
// this is not double wildcard, it's / then **acb (an odd, unnecessary use of double *)
pathPattern = checkStructure("/**acb");
assertPathElements(pathPattern, SeparatorPathElement.class, RegexPathElement.class);
}
@Test
void toStringTests() {
assertThat(checkStructure("/{*foobar}").toChainString()).isEqualTo("CaptureTheRest(/{*foobar})");
assertThat(checkStructure("{foobar}").toChainString()).isEqualTo("CaptureVariable({foobar})");
assertThat(checkStructure("abc").toChainString()).isEqualTo("Literal(abc)");
assertThat(checkStructure("{a}_*_{b}").toChainString()).isEqualTo("Regex({a}_*_{b})");
assertThat(checkStructure("/").toChainString()).isEqualTo("Separator(/)");
assertThat(checkStructure("?a?b?c").toChainString()).isEqualTo("SingleCharWildcarded(?a?b?c)");
assertThat(checkStructure("*").toChainString()).isEqualTo("Wildcard(*)");
assertThat(checkStructure("/**").toChainString()).isEqualTo("WildcardTheRest(/**)");
}
@Test
void captureTheRestPatterns() {
pathPattern = parse("{*foobar}");
assertThat(pathPattern.computePatternString()).isEqualTo("/{*foobar}");
assertPathElements(pathPattern, CaptureTheRestPathElement.class);
pathPattern = checkStructure("/{*foobar}");
assertPathElements(pathPattern, CaptureTheRestPathElement.class);
checkError("/{*foobar}/", 10, PatternMessage.NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST);
checkError("/{*foobar}abc", 10, PatternMessage.NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST);
checkError("/{*f%obar}", 4, PatternMessage.ILLEGAL_CHARACTER_IN_CAPTURE_DESCRIPTOR);
checkError("/{*foobar}abc", 10, PatternMessage.NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST);
checkError("/{f*oobar}", 3, PatternMessage.ILLEGAL_CHARACTER_IN_CAPTURE_DESCRIPTOR);
checkError("/{*foobar}/abc", 10, PatternMessage.NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST);
checkError("/{*foobar:.*}/abc", 9, PatternMessage.ILLEGAL_CHARACTER_IN_CAPTURE_DESCRIPTOR);
checkError("/{abc}{*foobar}", 1, PatternMessage.CAPTURE_ALL_IS_STANDALONE_CONSTRUCT);
checkError("/{abc}{*foobar}{foo}", 15, PatternMessage.NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST);
assertThat(parse("/{*foobar}").toChainString()).isEqualTo("CaptureSegments(/{*foobar})");
assertThat(parse("{foobar}").toChainString()).isEqualTo("CaptureVariable({foobar})");
assertThat(parse("abc").toChainString()).isEqualTo("Literal(abc)");
assertThat(parse("{a}_*_{b}").toChainString()).isEqualTo("Regex({a}_*_{b})");
assertThat(parse("/").toChainString()).isEqualTo("Separator(/)");
assertThat(parse("?a?b?c").toChainString()).isEqualTo("SingleCharWildcarded(?a?b?c)");
assertThat(parse("*").toChainString()).isEqualTo("Wildcard(*)");
assertThat(parse("/**").toChainString()).isEqualTo("WildcardSegments(/**)");
}
@Test
@@ -116,82 +354,6 @@ class PathPatternParserTests {
assertThat(pp2.hashCode()).isNotEqualTo(pp1.hashCode());
}
@Test
void regexPathElementPatterns() {
checkError("/{var:[^/]*}", 8, PatternMessage.MISSING_CLOSE_CAPTURE);
checkError("/{var:abc", 8, PatternMessage.MISSING_CLOSE_CAPTURE);
// Do not check the expected position due a change in RegEx parsing in JDK 13.
// See https://github.com/spring-projects/spring-framework/issues/23669
checkError("/{var:a{{1,2}}}", PatternMessage.REGEX_PATTERN_SYNTAX_EXCEPTION);
pathPattern = checkStructure("/{var:\\\\}");
PathElement next = pathPattern.getHeadSection().next;
assertThat(next.getClass().getName()).isEqualTo(CaptureVariablePathElement.class.getName());
assertMatches(pathPattern, "/\\");
pathPattern = checkStructure("/{var:\\/}");
next = pathPattern.getHeadSection().next;
assertThat(next.getClass().getName()).isEqualTo(CaptureVariablePathElement.class.getName());
assertNoMatch(pathPattern, "/aaa");
pathPattern = checkStructure("/{var:a{1,2}}");
next = pathPattern.getHeadSection().next;
assertThat(next.getClass().getName()).isEqualTo(CaptureVariablePathElement.class.getName());
pathPattern = checkStructure("/{var:[^\\/]*}");
next = pathPattern.getHeadSection().next;
assertThat(next.getClass().getName()).isEqualTo(CaptureVariablePathElement.class.getName());
PathPattern.PathMatchInfo result = matchAndExtract(pathPattern, "/foo");
assertThat(result.getUriVariables().get("var")).isEqualTo("foo");
pathPattern = checkStructure("/{var:\\[*}");
next = pathPattern.getHeadSection().next;
assertThat(next.getClass().getName()).isEqualTo(CaptureVariablePathElement.class.getName());
result = matchAndExtract(pathPattern, "/[[[");
assertThat(result.getUriVariables().get("var")).isEqualTo("[[[");
pathPattern = checkStructure("/{var:[\\{]*}");
next = pathPattern.getHeadSection().next;
assertThat(next.getClass().getName()).isEqualTo(CaptureVariablePathElement.class.getName());
result = matchAndExtract(pathPattern, "/{{{");
assertThat(result.getUriVariables().get("var")).isEqualTo("{{{");
pathPattern = checkStructure("/{var:[\\}]*}");
next = pathPattern.getHeadSection().next;
assertThat(next.getClass().getName()).isEqualTo(CaptureVariablePathElement.class.getName());
result = matchAndExtract(pathPattern, "/}}}");
assertThat(result.getUriVariables().get("var")).isEqualTo("}}}");
pathPattern = checkStructure("*");
assertThat(pathPattern.getHeadSection().getClass().getName()).isEqualTo(WildcardPathElement.class.getName());
checkStructure("/*");
checkStructure("/*/");
checkStructure("*/");
checkStructure("/*/");
pathPattern = checkStructure("/*a*/");
next = pathPattern.getHeadSection().next;
assertThat(next.getClass().getName()).isEqualTo(RegexPathElement.class.getName());
pathPattern = checkStructure("*/");
assertThat(pathPattern.getHeadSection().getClass().getName()).isEqualTo(WildcardPathElement.class.getName());
checkError("{foo}_{foo}", 0, PatternMessage.ILLEGAL_DOUBLE_CAPTURE, "foo");
checkError("/{bar}/{bar}", 7, PatternMessage.ILLEGAL_DOUBLE_CAPTURE, "bar");
checkError("/{bar}/{bar}_{foo}", 7, PatternMessage.ILLEGAL_DOUBLE_CAPTURE, "bar");
pathPattern = checkStructure("{symbolicName:[\\p{L}\\.]+}-sources-{version:[\\p{N}\\.]+}.jar");
assertThat(pathPattern.getHeadSection().getClass().getName()).isEqualTo(RegexPathElement.class.getName());
}
@Test
void completeCapturingPatterns() {
pathPattern = checkStructure("{foo}");
assertThat(pathPattern.getHeadSection().getClass().getName()).isEqualTo(CaptureVariablePathElement.class.getName());
checkStructure("/{foo}");
checkStructure("/{f}/");
checkStructure("/{foo}/{bar}/{wibble}");
checkStructure("/{mobile-number}"); // gh-23101
}
@Test
void noEncoding() {
// Check no encoding of expressions or constraints
@@ -205,66 +367,6 @@ class PathPatternParserTests {
assertThat(pp.toChainString()).isEqualTo("Regex({foo:f o}_ _{bar:b\\|o})");
}
@Test
void completeCaptureWithConstraints() {
pathPattern = checkStructure("{foo:...}");
assertPathElements(pathPattern, CaptureVariablePathElement.class);
pathPattern = checkStructure("{foo:[0-9]*}");
assertPathElements(pathPattern, CaptureVariablePathElement.class);
checkError("{foo:}", 5, PatternMessage.MISSING_REGEX_CONSTRAINT);
}
@Test
void partialCapturingPatterns() {
pathPattern = checkStructure("{foo}abc");
assertThat(pathPattern.getHeadSection().getClass().getName()).isEqualTo(RegexPathElement.class.getName());
checkStructure("abc{foo}");
checkStructure("/abc{foo}");
checkStructure("{foo}def/");
checkStructure("/abc{foo}def/");
checkStructure("{foo}abc{bar}");
checkStructure("{foo}abc{bar}/");
checkStructure("/{foo}abc{bar}/");
}
@Test
void illegalCapturePatterns() {
checkError("{abc/", 4, PatternMessage.MISSING_CLOSE_CAPTURE);
checkError("{abc:}/", 5, PatternMessage.MISSING_REGEX_CONSTRAINT);
checkError("{", 1, PatternMessage.MISSING_CLOSE_CAPTURE);
checkError("{abc", 4, PatternMessage.MISSING_CLOSE_CAPTURE);
checkError("{/}", 1, 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("/{/}", 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);
checkError("/foobar/{abc}_{abc}", 8, PatternMessage.ILLEGAL_DOUBLE_CAPTURE);
checkError("/foobar/{abc:..}_{abc:..}", 8, PatternMessage.ILLEGAL_DOUBLE_CAPTURE);
PathPattern pp = parse("/{abc:foo(bar)}");
assertThatIllegalArgumentException().isThrownBy(() ->
pp.matchAndExtract(toPSC("/foo")))
.withMessage("No capture groups allowed in the constraint regex: foo(bar)");
assertThatIllegalArgumentException().isThrownBy(() ->
pp.matchAndExtract(toPSC("/foobar")))
.withMessage("No capture groups allowed in the constraint regex: foo(bar)");
}
@Test
void badPatterns() {
// checkError("/{foo}{bar}/",6,PatternMessage.CANNOT_HAVE_ADJACENT_CAPTURES);
checkError("/{?}/", 2, PatternMessage.ILLEGAL_CHARACTER_AT_START_OF_CAPTURE_DESCRIPTOR, "?");
checkError("/{a?b}/", 3, PatternMessage.ILLEGAL_CHARACTER_IN_CAPTURE_DESCRIPTOR, "?");
checkError("/{%%$}", 2, PatternMessage.ILLEGAL_CHARACTER_AT_START_OF_CAPTURE_DESCRIPTOR, "%");
checkError("/{ }", 2, PatternMessage.ILLEGAL_CHARACTER_AT_START_OF_CAPTURE_DESCRIPTOR, " ");
checkError("/{%:[0-9]*}", 2, PatternMessage.ILLEGAL_CHARACTER_AT_START_OF_CAPTURE_DESCRIPTOR, "%");
}
@Test
void patternPropertyGetCaptureCountTests() {
// Test all basic section types
@@ -311,30 +413,23 @@ class PathPatternParserTests {
}
@Test
void multipleSeparatorPatterns() {
pathPattern = checkStructure("///aaa");
void normalizedLengthWhenMultipleSeparator() {
pathPattern = parse("///aaa");
assertThat(pathPattern.getNormalizedLength()).isEqualTo(6);
assertPathElements(pathPattern, SeparatorPathElement.class, SeparatorPathElement.class,
SeparatorPathElement.class, LiteralPathElement.class);
pathPattern = checkStructure("///aaa////aaa/b");
pathPattern = parse("///aaa////aaa/b");
assertThat(pathPattern.getNormalizedLength()).isEqualTo(15);
assertPathElements(pathPattern, SeparatorPathElement.class, SeparatorPathElement.class,
SeparatorPathElement.class, LiteralPathElement.class, SeparatorPathElement.class,
SeparatorPathElement.class, SeparatorPathElement.class, SeparatorPathElement.class,
LiteralPathElement.class, SeparatorPathElement.class, LiteralPathElement.class);
pathPattern = checkStructure("/////**");
pathPattern = parse("/////**");
assertThat(pathPattern.getNormalizedLength()).isEqualTo(5);
assertPathElements(pathPattern, SeparatorPathElement.class, SeparatorPathElement.class,
SeparatorPathElement.class, SeparatorPathElement.class, WildcardTheRestPathElement.class);
}
@Test
void patternPropertyGetLengthTests() {
void normalizedLengthWhenVariable() {
// Test all basic section types
assertThat(parse("{foo}").getNormalizedLength()).isEqualTo(1);
assertThat(parse("foo").getNormalizedLength()).isEqualTo(3);
assertThat(parse("{*foobar}").getNormalizedLength()).isEqualTo(1);
assertThat(parse("/{*foobar}").getNormalizedLength()).isEqualTo(1);
assertThat(parse("**").getNormalizedLength()).isEqualTo(1);
assertThat(parse("/**").getNormalizedLength()).isEqualTo(1);
assertThat(parse("{abc}asdf").getNormalizedLength()).isEqualTo(5);
assertThat(parse("{abc}_*").getNormalizedLength()).isEqualTo(3);
@@ -350,6 +445,15 @@ class PathPatternParserTests {
assertThat(parse("/{foo}/{bar}_{goo}_{wibble}/abc/bar").getNormalizedLength()).isEqualTo(16);
}
@Test
void separatorTests() {
PathPatternParser parser = new PathPatternParser();
parser.setPathOptions(PathContainer.Options.create('.', false));
String rawPattern = "first.second.{last}";
PathPattern pattern = parser.parse(rawPattern);
assertThat(pattern.computePatternString()).isEqualTo(rawPattern);
}
@Test
void compareTests() {
PathPattern p1, p2, p3;
@@ -414,96 +518,14 @@ class PathPatternParserTests {
assertThat(patterns).element(1).isEqualTo(p2);
}
@Test
void captureTheRestWithinPatternNotSupported() {
PathPatternParser parser = new PathPatternParser();
assertThatThrownBy(() -> parser.parse("/resources/**/details"))
.isInstanceOf(PatternParseException.class)
.extracting("messageType").isEqualTo(PatternMessage.NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST);
}
@Test
void separatorTests() {
PathPatternParser parser = new PathPatternParser();
parser.setPathOptions(PathContainer.Options.create('.', false));
String rawPattern = "first.second.{last}";
PathPattern pattern = parser.parse(rawPattern);
assertThat(pattern.computePatternString()).isEqualTo(rawPattern);
}
private PathPattern parse(String pattern) {
PathPatternParser patternParser = new PathPatternParser();
return patternParser.parse(pattern);
}
/**
* Verify the pattern string computed for a parsed pattern matches the original pattern text
*/
private PathPattern checkStructure(String pattern) {
PathPattern pp = parse(pattern);
assertThat(pp.computePatternString()).isEqualTo(pattern);
return pp;
}
/**
* Delegates to {@link #checkError(String, int, PatternMessage, String...)},
* passing {@code -1} as the {@code expectedPos}.
* @since 5.2
*/
private void checkError(String pattern, PatternMessage expectedMessage, String... expectedInserts) {
checkError(pattern, -1, expectedMessage, expectedInserts);
}
/**
* @param expectedPos the expected position, or {@code -1} if the position should not be checked
*/
private void checkError(String pattern, int expectedPos, PatternMessage expectedMessage,
String... expectedInserts) {
assertThatExceptionOfType(PatternParseException.class)
.isThrownBy(() -> pathPattern = parse(pattern))
.satisfies(ex -> {
if (expectedPos >= 0) {
assertThat(ex.getPosition()).as(ex.toDetailedString()).isEqualTo(expectedPos);
}
assertThat(ex.getMessageType()).as(ex.toDetailedString()).isEqualTo(expectedMessage);
if (expectedInserts.length != 0) {
assertThat(ex.getInserts()).isEqualTo(expectedInserts);
}
});
}
@SafeVarargs
private void assertPathElements(PathPattern p, Class<? extends PathElement>... sectionClasses) {
PathElement head = p.getHeadSection();
for (Class<? extends PathElement> sectionClass : sectionClasses) {
if (head == null) {
fail("Ran out of data in parsed pattern. Pattern is: " + p.toChainString());
}
assertThat(head.getClass().getSimpleName()).as("Not expected section type. Pattern is: " + p.toChainString()).isEqualTo(sectionClass.getSimpleName());
head = head.next;
}
}
// Mirrors the score computation logic in PathPattern
private int computeScore(int capturedVariableCount, int wildcardCount) {
return capturedVariableCount + wildcardCount * 100;
}
private void assertMatches(PathPattern pp, String path) {
assertThat(pp.matches(PathPatternTests.toPathContainer(path))).isTrue();
}
private void assertNoMatch(PathPattern pp, String path) {
assertThat(pp.matches(PathPatternTests.toPathContainer(path))).isFalse();
}
private PathPattern.PathMatchInfo matchAndExtract(PathPattern pp, String path) {
return pp.matchAndExtract(PathPatternTests.toPathContainer(path));
}
private PathContainer toPSC(String path) {
return PathPatternTests.toPathContainer(path);
}
}
@@ -23,6 +23,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.http.server.PathContainer;
@@ -34,12 +35,303 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Exercise matching of {@link PathPattern} objects.
* Tests for {@link PathPattern}.
*
* @author Andy Clement
* @author Brian Clozel
*/
class PathPatternTests {
@Nested
class MatchingTests {
@Test
void basicMatching() {
checkMatches("", "");
checkMatches("", null);
checkNoMatch("/abc", "/");
checkMatches("/", "/");
checkNoMatch("/", "/a");
checkMatches("foo/bar/", "foo/bar/");
checkNoMatch("foo", "foobar");
checkMatches("/foo/bar", "/foo/bar");
checkNoMatch("/foo/bar", "/foo/baz");
}
@Test
void literalPathElements() {
checkMatches("foo", "foo");
checkNoMatch("foo", "bar");
checkNoMatch("foo", "/foo");
checkNoMatch("/foo", "foo");
checkMatches("/f", "/f");
checkMatches("/foo", "/foo");
checkNoMatch("/foo", "/food");
checkNoMatch("/food", "/foo");
checkMatches("/foo/", "/foo/");
checkMatches("/foo/bar/woo", "/foo/bar/woo");
checkMatches("foo/bar/woo", "foo/bar/woo");
}
@Test
void questionMarks() {
checkNoMatch("a", "ab");
checkMatches("/f?o/bar", "/foo/bar");
checkNoMatch("/foo/b2r", "/foo/bar");
checkNoMatch("?", "te");
checkMatches("?", "a");
checkMatches("???", "abc");
checkNoMatch("tes?", "te");
checkNoMatch("tes?", "tes");
checkNoMatch("tes?", "testt");
checkNoMatch("tes?", "tsst");
checkMatches(".?.a", ".a.a");
checkNoMatch(".?.a", ".aba");
checkMatches("/f?o/bar","/f%20o/bar");
}
@Test
void multipleSeparatorsInPattern() {
PathPattern pp = parse("a//b//c");
assertThat(pp.toChainString()).isEqualTo("Literal(a) Separator(/) Separator(/) Literal(b) Separator(/) Separator(/) Literal(c)");
assertMatches(pp,"a//b//c");
assertThat(parse("a//**").toChainString()).isEqualTo("Literal(a) Separator(/) WildcardSegments(/**)");
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
void multipleSelectorsInPath() {
checkNoMatch("/abc", "////abc");
checkNoMatch("/", "//");
checkNoMatch("/abc/def/ghi", "/abc//def///ghi");
checkNoMatch("/abc", "////abc");
checkMatches("////abc", "////abc");
checkNoMatch("/", "//");
checkNoMatch("/abc//def", "/abc/def");
checkNoMatch("/abc//def///ghi", "/abc/def/ghi");
checkMatches("/abc//def///ghi", "/abc//def///ghi");
}
@Test
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
void captureSegmentsAtStart() {
checkMatches("/{*foobar}/resource", "/resource");
checkNoMatch("/{*foobar}/resource", "/resourceX");
checkNoMatch("/{*foobar}/resource", "/foobar/resourceX");
checkMatches("/{*foobar}/resource", "/foobar/resource");
}
@Test
void captureSegmentsAtEnd() {
checkMatches("/resource/{*foobar}", "/resource");
checkNoMatch("/resource/{*foobar}", "/resourceX");
checkNoMatch("/resource/{*foobar}", "/resourceX/foobar");
checkMatches("/resource/{*foobar}", "/resource/foobar");
}
@Test
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");
checkMatches("/*/bar", "/foo/bar");
checkMatches("/a*b*c*d/bar", "/abcd/bar");
checkMatches("*a*", "testa");
checkMatches("a/*", "a/");
checkNoMatch("a/*", "a//"); // no data for *
PathPatternParser ppp = new PathPatternParser();
assertThat(ppp.parse("a/*").matches(toPathContainer("a//"))).isFalse();
checkMatches("a/*", "a/a");
}
@Test
void wildcardSegmentsStart() {
checkMatches("/**/resource", "/resource");
checkNoMatch("/**/resource", "/Xresource");
checkNoMatch("/**/resource", "/foobar/resourceX");
checkMatches("/**/resource", "/foobar/resource");
checkMatches("/**/resource/test", "/foo/bar/resource/test");
checkNoMatch("/**/resource/test", "/foo/bar/resource/t");
}
@Test
void wildcardSegmentsEnd() {
checkMatches("/resource/**", "/resource");
checkNoMatch("/resource/**", "/resourceX");
checkNoMatch("/resource/**", "/resourceX/foobar");
checkMatches("/resource/**", "/resource/foobar");
}
@Test
void antPathMatcherTests() {
// test exact matching
checkMatches("test", "test");
checkMatches("/test", "/test");
checkMatches("https://example.org", "https://example.org");
checkNoMatch("/test.jpg", "test.jpg");
checkNoMatch("test", "/test");
checkNoMatch("/test", "test");
// test matching with ?'s
checkMatches("t?st", "test");
checkMatches("??st", "test");
checkMatches("tes?", "test");
checkMatches("te??", "test");
checkMatches("?es?", "test");
checkNoMatch("tes?", "tes");
checkNoMatch("tes?", "testt");
checkNoMatch("tes?", "tsst");
// test matching with *'s
checkMatches("*", "test");
checkMatches("test*", "test");
checkMatches("test*", "testTest");
checkMatches("test/*", "test/Test");
checkMatches("test/*", "test/t");
checkMatches("test/*", "test/");
checkMatches("*test*", "AnothertestTest");
checkMatches("*test", "Anothertest");
checkMatches("*.*", "test.");
checkMatches("*.*", "test.test");
checkMatches("*.*", "test.test.test");
checkMatches("test*aaa", "testblaaaa");
checkNoMatch("test*", "tst");
checkNoMatch("test*", "tsttest");
checkMatches("test*", "test"); // trailing slash is optional
checkNoMatch("test*", "test/t");
checkNoMatch("test/*", "test");
checkNoMatch("*test*", "tsttst");
checkNoMatch("*test", "tsttst");
checkNoMatch("*.*", "tsttst");
checkNoMatch("test*aaa", "test");
checkNoMatch("test*aaa", "testblaaab");
// test matching with ?'s and /'s
checkMatches("/?", "/a");
checkMatches("/?/a", "/a/a");
checkMatches("/a/?", "/a/b");
checkMatches("/??/a", "/aa/a");
checkMatches("/a/??", "/a/bb");
checkMatches("/?", "/a");
checkMatches("/**", "");
checkMatches("/books/**", "/books");
checkMatches("/**", "/testing/testing");
checkMatches("/*/**", "/testing/testing");
checkMatches("/bla*bla/test", "/blaXXXbla/test");
checkMatches("/*bla/test", "/XXXbla/test");
checkNoMatch("/bla*bla/test", "/blaXXXbl/test");
checkNoMatch("/*bla/test", "XXXblab/test");
checkNoMatch("/*bla/test", "XXXbl/test");
checkNoMatch("/????", "/bala/bla");
checkMatches("/foo/bar/**", "/foo/bar/");
checkMatches("/{bla}.html", "/testing.html");
checkCapture("/{bla}.*", "/testing.html", "bla", "testing");
}
}
@Nested
class VariableCaptureTests {
@Test
void constrainedMatches() {
checkCapture("{foo:[0-9]*}", "123", "foo", "123");
checkNoMatch("{foo:[0-9]*}", "abc");
checkNoMatch("/{foo:[0-9]*}", "abc");
checkCapture("/*/{foo:....}/**", "/foo/barg/foo", "foo", "barg");
checkCapture("/*/{foo:....}/**", "/foo/barg/abc/def/ghi", "foo", "barg");
checkNoMatch("{foo:....}", "99");
checkMatches("{foo:..}", "99");
checkCapture("/{abc:\\{\\}}", "/{}", "abc", "{}");
checkCapture("/{abc:\\[\\]}", "/[]", "abc", "[]");
checkCapture("/{abc:\\\\\\\\}", "/\\\\"); // this is fun...
}
@Test
void captureSegmentsAtStart() {
checkCapture("/{*foobar}/resource", "/foobar/resource", "foobar", "/foobar");
checkCapture("/{*something}/customer", "/99/customer", "something", "/99");
checkCapture("/{*something}/customer", "/aa/bb/cc/customer", "something", "/aa/bb/cc");
checkCapture("/{*something}/customer", "/customer", "something", "");
checkCapture("/{*something}/customer", "//////99/customer", "something", "//////99");
}
@Test
void captureSegmentsAtEnd() {
checkCapture("/resource/{*foobar}", "/resource/foobar", "foobar", "/foobar");
checkCapture("/customer/{*something}", "/customer/99", "something", "/99");
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//////", "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
void encodingAndBoundVariablesCapturePathElement() {
checkCapture("{var}","f%20o","var","f o");
checkCapture("{var1}/{var2}","f%20o/f%7Co","var1","f o","var2","f|o");
checkCapture("{var1}/{var2}","f%20o/f%7co","var1","f o","var2","f|o"); // lower case encoding
checkCapture("{var:foo}","foo","var","foo");
checkCapture("{var:f o}","f%20o","var","f o"); // constraint is expressed in non encoded form
checkCapture("{var:f.o}","f%20o","var","f o");
checkCapture("{var:f\\|o}","f%7co","var","f|o");
checkCapture("{var:.*}","x\ny","var","x\ny");
}
@Test
void encodingAndBoundVariablesCaptureTheRestPathElement() {
checkCapture("/{*var}","/f%20o","var","/f o");
checkCapture("{var1}/{*var2}","f%20o/f%7Co","var1","f o","var2","/f|o");
checkCapture("/{*var}","/foo","var","/foo");
checkCapture("/{*var}","/f%20o","var","/f o");
checkCapture("/{*var}","/f%20o","var","/f o");
checkCapture("/{*var}","/f%7co","var","/f|o");
}
@Test
void encodingAndBoundVariablesRegexPathElement() {
checkCapture("/{var1:f o}_ _{var2}","/f%20o_%20_f%7co","var1","f o","var2","f|o");
checkCapture("/{var1}_{var2}","/f%20o_foo","var1","f o","var2","foo");
checkCapture("/{var1}_ _{var2}","/f%20o_%20_f%7co","var1","f o","var2","f|o");
checkCapture("/{var1}_ _{var2:f\\|o}","/f%20o_%20_f%7co","var1","f o","var2","f|o");
checkCapture("/{var1:f o}_ _{var2:f\\|o}","/f%20o_%20_f%7co","var1","f o","var2","f|o");
checkCapture("/{var1:f o}_ _{var2:f\\|o}","/f%20o_%20_f%7co","var1","f o","var2","f|o");
checkCapture("/{var1}_{var2}","/f\noo_foo","var1","f\noo","var2","foo");
}
}
@Test
void pathContainer() {
assertThat(elementsToString(toPathContainer("/abc/def").elements())).isEqualTo("[/][abc][/][def]");
@@ -62,44 +354,8 @@ class PathPatternTests {
assertThat(parser.parse("/foo/bar").hasPatternSyntax()).isFalse();
}
@Test
void matching_LiteralPathElement() {
checkMatches("foo", "foo");
checkNoMatch("foo", "bar");
checkNoMatch("foo", "/foo");
checkNoMatch("/foo", "foo");
checkMatches("/f", "/f");
checkMatches("/foo", "/foo");
checkNoMatch("/foo", "/food");
checkNoMatch("/food", "/foo");
checkMatches("/foo/", "/foo/");
checkMatches("/foo/bar/woo", "/foo/bar/woo");
checkMatches("foo/bar/woo", "foo/bar/woo");
}
@Test
void basicMatching() {
checkMatches("", "");
checkMatches("", null);
checkNoMatch("/abc", "/");
checkMatches("/", "/");
checkNoMatch("/", "/a");
checkMatches("foo/bar/", "foo/bar/");
checkNoMatch("foo", "foobar");
checkMatches("/foo/bar", "/foo/bar");
checkNoMatch("/foo/bar", "/foo/baz");
}
private void assertMatches(PathPattern pp, String path) {
assertThat(pp.matches(toPathContainer(path))).isTrue();
}
private void assertNoMatch(PathPattern pp, String path) {
assertThat(pp.matches(toPathContainer(path))).isFalse();
}
@Test
void pathRemainderBasicCases_spr15336() {
@Test // SPR-15336
void pathRemainderBasicCases() {
// Cover all PathElement kinds
assertThat(getPathRemaining("/foo", "/foo/bar").getPathRemaining().value()).isEqualTo("/bar");
assertThat(getPathRemaining("/foo", "/foo/").getPathRemaining().value()).isEqualTo("/");
@@ -119,41 +375,8 @@ class PathPatternTests {
assertThat(getPathRemaining("/foo//", "/foo///bar").getPathRemaining().value()).isEqualTo("/bar");
}
@Test
void encodingAndBoundVariablesCapturePathElement() {
checkCapture("{var}","f%20o","var","f o");
checkCapture("{var1}/{var2}","f%20o/f%7Co","var1","f o","var2","f|o");
checkCapture("{var1}/{var2}","f%20o/f%7co","var1","f o","var2","f|o"); // lower case encoding
checkCapture("{var:foo}","foo","var","foo");
checkCapture("{var:f o}","f%20o","var","f o"); // constraint is expressed in non encoded form
checkCapture("{var:f.o}","f%20o","var","f o");
checkCapture("{var:f\\|o}","f%7co","var","f|o");
checkCapture("{var:.*}","x\ny","var","x\ny");
}
@Test
void encodingAndBoundVariablesCaptureTheRestPathElement() {
checkCapture("/{*var}","/f%20o","var","/f o");
checkCapture("{var1}/{*var2}","f%20o/f%7Co","var1","f o","var2","/f|o");
checkCapture("/{*var}","/foo","var","/foo");
checkCapture("/{*var}","/f%20o","var","/f o");
checkCapture("/{*var}","/f%20o","var","/f o");
checkCapture("/{*var}","/f%7co","var","/f|o");
}
@Test
void encodingAndBoundVariablesRegexPathElement() {
checkCapture("/{var1:f o}_ _{var2}","/f%20o_%20_f%7co","var1","f o","var2","f|o");
checkCapture("/{var1}_{var2}","/f%20o_foo","var1","f o","var2","foo");
checkCapture("/{var1}_ _{var2}","/f%20o_%20_f%7co","var1","f o","var2","f|o");
checkCapture("/{var1}_ _{var2:f\\|o}","/f%20o_%20_f%7co","var1","f o","var2","f|o");
checkCapture("/{var1:f o}_ _{var2:f\\|o}","/f%20o_%20_f%7co","var1","f o","var2","f|o");
checkCapture("/{var1:f o}_ _{var2:f\\|o}","/f%20o_%20_f%7co","var1","f o","var2","f|o");
checkCapture("/{var1}_{var2}","/f\noo_foo","var1","f\noo","var2","foo");
}
@Test
void pathRemainingCornerCases_spr15336() {
@Test // SPR-15336
void pathRemainingCornerCases() {
// No match when the literal path element is a longer form of the segment in the pattern
assertThat(parse("/foo").matchStartOfPath(toPathContainer("/footastic/bar"))).isNull();
assertThat(parse("/f?o").matchStartOfPath(toPathContainer("/footastic/bar"))).isNull();
@@ -166,6 +389,11 @@ class PathPatternTests {
assertThat(parse("/resource/**")
.matchStartOfPath(toPathContainer("/resource")).getPathRemaining().value()).isEmpty();
assertThat(parse("/**/resource")
.matchStartOfPath(toPathContainer("/test/resource")).getPathRemaining().value()).isEmpty();
assertThat(parse("/**/resource")
.matchStartOfPath(toPathContainer("/test/resource/other")).getPathRemaining().value()).isEqualTo("/other");
// Similar to above for the capture-the-rest variant
assertThat(parse("/resource/{*foo}").matchStartOfPath(toPathContainer("/resourceX"))).isNull();
assertThat(parse("/resource/{*foo}")
@@ -194,191 +422,8 @@ class PathPatternTests {
assertThat(parse("").matchStartOfPath(toPathContainer("")).getPathRemaining().value()).isEmpty();
}
@Test
void questionMarks() {
checkNoMatch("a", "ab");
checkMatches("/f?o/bar", "/foo/bar");
checkNoMatch("/foo/b2r", "/foo/bar");
checkNoMatch("?", "te");
checkMatches("?", "a");
checkMatches("???", "abc");
checkNoMatch("tes?", "te");
checkNoMatch("tes?", "tes");
checkNoMatch("tes?", "testt");
checkNoMatch("tes?", "tsst");
checkMatches(".?.a", ".a.a");
checkNoMatch(".?.a", ".aba");
checkMatches("/f?o/bar","/f%20o/bar");
}
@Test
void captureTheRest() {
checkMatches("/resource/{*foobar}", "/resource");
checkNoMatch("/resource/{*foobar}", "/resourceX");
checkNoMatch("/resource/{*foobar}", "/resourceX/foobar");
checkMatches("/resource/{*foobar}", "/resource/foobar");
checkCapture("/resource/{*foobar}", "/resource/foobar", "foobar", "/foobar");
checkCapture("/customer/{*something}", "/customer/99", "something", "/99");
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//////", "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
void multipleSeparatorsInPattern() {
PathPattern pp = parse("a//b//c");
assertThat(pp.toChainString()).isEqualTo("Literal(a) Separator(/) Separator(/) Literal(b) Separator(/) Separator(/) Literal(c)");
assertMatches(pp,"a//b//c");
assertThat(parse("a//**").toChainString()).isEqualTo("Literal(a) Separator(/) WildcardTheRest(/**)");
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
void multipleSelectorsInPath() {
checkNoMatch("/abc", "////abc");
checkNoMatch("/", "//");
checkNoMatch("/abc/def/ghi", "/abc//def///ghi");
checkNoMatch("/abc", "////abc");
checkMatches("////abc", "////abc");
checkNoMatch("/", "//");
checkNoMatch("/abc//def", "/abc/def");
checkNoMatch("/abc//def///ghi", "/abc/def/ghi");
checkMatches("/abc//def///ghi", "/abc//def///ghi");
}
@Test
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");
}
@SuppressWarnings("deprecation")
@Test
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");
checkMatches("/*/bar", "/foo/bar");
checkMatches("/a*b*c*d/bar", "/abcd/bar");
checkMatches("*a*", "testa");
checkMatches("a/*", "a/");
checkNoMatch("a/*", "a//"); // no data for *
PathPatternParser ppp = new PathPatternParser();
assertThat(ppp.parse("a/*").matches(toPathContainer("a//"))).isFalse();
checkMatches("a/*", "a/a");
checkMatches("/resource/**", "/resource");
checkNoMatch("/resource/**", "/resourceX");
checkNoMatch("/resource/**", "/resourceX/foobar");
checkMatches("/resource/**", "/resource/foobar");
}
@Test
void constrainedMatches() {
checkCapture("{foo:[0-9]*}", "123", "foo", "123");
checkNoMatch("{foo:[0-9]*}", "abc");
checkNoMatch("/{foo:[0-9]*}", "abc");
checkCapture("/*/{foo:....}/**", "/foo/barg/foo", "foo", "barg");
checkCapture("/*/{foo:....}/**", "/foo/barg/abc/def/ghi", "foo", "barg");
checkNoMatch("{foo:....}", "99");
checkMatches("{foo:..}", "99");
checkCapture("/{abc:\\{\\}}", "/{}", "abc", "{}");
checkCapture("/{abc:\\[\\]}", "/[]", "abc", "[]");
checkCapture("/{abc:\\\\\\\\}", "/\\\\"); // this is fun...
}
@Test
void antPathMatcherTests() {
// test exact matching
checkMatches("test", "test");
checkMatches("/test", "/test");
checkMatches("https://example.org", "https://example.org");
checkNoMatch("/test.jpg", "test.jpg");
checkNoMatch("test", "/test");
checkNoMatch("/test", "test");
// test matching with ?'s
checkMatches("t?st", "test");
checkMatches("??st", "test");
checkMatches("tes?", "test");
checkMatches("te??", "test");
checkMatches("?es?", "test");
checkNoMatch("tes?", "tes");
checkNoMatch("tes?", "testt");
checkNoMatch("tes?", "tsst");
// test matching with *'s
checkMatches("*", "test");
checkMatches("test*", "test");
checkMatches("test*", "testTest");
checkMatches("test/*", "test/Test");
checkMatches("test/*", "test/t");
checkMatches("test/*", "test/");
checkMatches("*test*", "AnothertestTest");
checkMatches("*test", "Anothertest");
checkMatches("*.*", "test.");
checkMatches("*.*", "test.test");
checkMatches("*.*", "test.test.test");
checkMatches("test*aaa", "testblaaaa");
checkNoMatch("test*", "tst");
checkNoMatch("test*", "tsttest");
checkMatches("test*", "test"); // trailing slash is optional
checkNoMatch("test*", "test/t");
checkNoMatch("test/*", "test");
checkNoMatch("*test*", "tsttst");
checkNoMatch("*test", "tsttst");
checkNoMatch("*.*", "tsttst");
checkNoMatch("test*aaa", "test");
checkNoMatch("test*aaa", "testblaaab");
// test matching with ?'s and /'s
checkMatches("/?", "/a");
checkMatches("/?/a", "/a/a");
checkMatches("/a/?", "/a/b");
checkMatches("/??/a", "/aa/a");
checkMatches("/a/??", "/a/bb");
checkMatches("/?", "/a");
checkMatches("/**", "");
checkMatches("/books/**", "/books");
checkMatches("/**", "/testing/testing");
checkMatches("/*/**", "/testing/testing");
checkMatches("/bla*bla/test", "/blaXXXbla/test");
checkMatches("/*bla/test", "/XXXbla/test");
checkNoMatch("/bla*bla/test", "/blaXXXbl/test");
checkNoMatch("/*bla/test", "XXXblab/test");
checkNoMatch("/*bla/test", "XXXbl/test");
checkNoMatch("/????", "/bala/bla");
checkMatches("/foo/bar/**", "/foo/bar/");
checkMatches("/{bla}.html", "/testing.html");
checkCapture("/{bla}.*", "/testing.html", "bla", "testing");
}
@Test
void pathRemainingEnhancements_spr15419() {
@Test // SPR-15149
void pathRemainingEnhancements() {
PathPattern pp;
PathPattern.PathRemainingMatchInfo pri;
// It would be nice to partially match a path and get any bound variables in one step
@@ -495,8 +540,8 @@ class PathPatternTests {
assertMatches(p,"bAb");
}
@Test
void extractPathWithinPattern_spr15259() {
@Test // SPR-15259
void extractPathWithinPatternWildards() {
checkExtractPathWithinPattern("/**","//","");
checkExtractPathWithinPattern("/**","/","");
checkExtractPathWithinPattern("/**","","");
@@ -553,9 +598,8 @@ class PathPatternTests {
assertThat(result.elements()).hasSize(3);
}
@Test
@SuppressWarnings("deprecation")
public void extractUriTemplateVariables_spr15264() {
@Test // SPR-15264
public void extractUriTemplateVariables() {
PathPattern pp;
pp = new PathPatternParser().parse("/{foo}");
assertMatches(pp,"/abc");
@@ -611,10 +655,7 @@ class PathPatternTests {
Map<String, String> vars = new AntPathMatcher().extractUriTemplateVariables("/{foo}{bar}", "/a");
assertThat(vars).containsEntry("foo", "a");
assertThat(vars.get("bar")).isEmpty();
}
@Test
void extractUriTemplateVariables() {
assertMatches(parse("{hotel}"),"1");
assertMatches(parse("/hotels/{hotel}"),"/hotels/1");
checkCapture("/hotels/{hotel}", "/hotels/1", "hotel", "1");
@@ -1003,6 +1044,43 @@ class PathPatternTests {
assertThat(result).isNotNull();
}
@Test
void regexPathElementPatterns() {
PathPatternParser pp = new PathPatternParser();
PathPattern pattern = pp.parse("/{var:\\\\}");
assertMatches(pattern, "/\\");
pattern = pp.parse("/{var:\\/}");
assertNoMatch(pattern, "/aaa");
pattern = pp.parse("/{var:[^\\/]*}");
PathPattern.PathMatchInfo result = matchAndExtract(pattern, "/foo");
assertThat(result.getUriVariables().get("var")).isEqualTo("foo");
pattern = pp.parse("/{var:\\[*}");
result = matchAndExtract(pattern, "/[[[");
assertThat(result.getUriVariables().get("var")).isEqualTo("[[[");
pattern = pp.parse("/{var:[\\{]*}");
result = matchAndExtract(pattern, "/{{{");
assertThat(result.getUriVariables().get("var")).isEqualTo("{{{");
pattern = pp.parse("/{var:[\\}]*}");
result = matchAndExtract(pattern, "/}}}");
assertThat(result.getUriVariables().get("var")).isEqualTo("}}}");
}
private void assertMatches(PathPattern pp, String path) {
assertThat(pp.matches(toPathContainer(path))).isTrue();
}
private void assertNoMatch(PathPattern pp, String path) {
assertThat(pp.matches(toPathContainer(path))).isFalse();
}
private PathPattern.PathMatchInfo matchAndExtract(String pattern, String path) {
return parse(pattern).matchAndExtract(PathPatternTests.toPathContainer(path));
}