Skip to content

Commit d062552

Browse files
committed
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
1 parent ed2cad3 commit d062552

File tree

10 files changed

+868
-644
lines changed

10 files changed

+868
-644
lines changed

framework-docs/modules/ROOT/pages/web/webflux/controller/ann-requestmapping.adoc

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,10 @@ You can map requests by using glob patterns and wildcards:
9393
|===
9494
|Pattern |Description |Example
9595

96+
| `spring`
97+
| Literal pattern
98+
| `+"/spring"+` matches `+"/spring"+`
99+
96100
| `+?+`
97101
| Matches one character
98102
| `+"/pages/t?st.html"+` matches `+"/pages/test.html"+` and `+"/pages/t3st.html"+`
@@ -104,23 +108,41 @@ You can map requests by using glob patterns and wildcards:
104108
`+"/projects/*/versions"+` matches `+"/projects/spring/versions"+` but does not match `+"/projects/spring/boot/versions"+`
105109

106110
| `+**+`
107-
| Matches zero or more path segments until the end of the path
111+
| Matches zero or more path segments
108112
| `+"/resources/**"+` matches `+"/resources/file.png"+` and `+"/resources/images/file.png"+`
109113

110-
`+"/resources/**/file.png"+` is invalid as `+**+` is only allowed at the end of the path.
114+
`+"/**/resources"+` matches `+"/spring/resources"+` and `+"/spring/framework/resources"+`
115+
116+
`+"/resources/**/file.png"+` is invalid as `+**+` is not allowed in the middle of the path.
117+
118+
`+"/**/{name}/resources"+` is invalid as only a literal pattern is allowed right after `+**+`.
119+
`+"/**/project/{project}/resources"+` is allowed.
120+
121+
`+"/**/spring/**"+` is not allowed, as only a single `+**+`/`+{*path}+` instance is allowed per pattern.
111122

112123
| `+{name}+`
113124
| Matches a path segment and captures it as a variable named "name"
114125
| `+"/projects/{project}/versions"+` matches `+"/projects/spring/versions"+` and captures `+project=spring+`
115126

127+
`+"/projects/{project}/versions"+` does not match `+"/projects/spring/framework/versions"+` as it captures a single path segment.
128+
116129
| `+{name:[a-z]+}+`
117130
| Matches the regexp `+"[a-z]+"+` as a path variable named "name"
118131
| `+"/projects/{project:[a-z]+}/versions"+` matches `+"/projects/spring/versions"+` but not `+"/projects/spring1/versions"+`
119132

120133
| `+{*path}+`
121-
| Matches zero or more path segments until the end of the path and captures it as a variable named "path"
134+
| Matches zero or more path segments and captures it as a variable named "path"
122135
| `+"/resources/{*file}"+` matches `+"/resources/images/file.png"+` and captures `+file=/images/file.png+`
123136

137+
`+"{*path}/resources"+` matches `+"/spring/framework/resources"+` and captures `+path=/spring/framework+`
138+
139+
`+"/resources/{*path}/file.png"+` is invalid as `{*path}` is not allowed in the middle of the path.
140+
141+
`+"/{*path}/{name}/resources"+` is invalid as only a literal pattern is allowed right after `{*path}`.
142+
`+"/{*path}/project/{project}/resources"+` is allowed.
143+
144+
`+"/{*path}/spring/**"+` is not allowed, as only a single `+**+`/`+{*path}+` instance is allowed per pattern.
145+
124146
|===
125147

126148
Captured URI variables can be accessed with `@PathVariable`, as the following example shows:

framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-requestmapping.adoc

Lines changed: 63 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -88,37 +88,71 @@ Kotlin::
8888
== URI patterns
8989
[.small]#xref:web/webflux/controller/ann-requestmapping.adoc#webflux-ann-requestmapping-uri-templates[See equivalent in the Reactive stack]#
9090

91-
`@RequestMapping` methods can be mapped using URL patterns. There are two alternatives:
92-
93-
* `PathPattern` -- a pre-parsed pattern matched against the URL path also pre-parsed as
94-
`PathContainer`. Designed for web use, this solution deals effectively with encoding and
95-
path parameters, and matches efficiently.
96-
* `AntPathMatcher` -- match String patterns against a String path. This is the original
97-
solution also used in Spring configuration to select resources on the classpath, on the
98-
filesystem, and other locations. It is less efficient and the String path input is a
91+
`@RequestMapping` methods can be mapped using URL patterns.
92+
Spring MVC is using `PathPattern` -- a pre-parsed pattern matched against the URL path also pre-parsed as `PathContainer`.
93+
Designed for web use, this solution deals effectively with encoding and path parameters, and matches efficiently.
94+
See xref:web/webmvc/mvc-config/path-matching.adoc[MVC config] for customizations of path matching options.
95+
96+
NOTE: the `AntPathMatcher` variant is now deprecated because it is less efficient and the String path input is a
9997
challenge for dealing effectively with encoding and other issues with URLs.
10098

101-
`PathPattern` is the recommended solution for web applications and it is the only choice in
102-
Spring WebFlux. It was enabled for use in Spring MVC from version 5.3 and is enabled by
103-
default from version 6.0. See xref:web/webmvc/mvc-config/path-matching.adoc[MVC config] for
104-
customizations of path matching options.
105-
106-
`PathPattern` supports the same pattern syntax as `AntPathMatcher`. In addition, it also
107-
supports the capturing pattern, for example, `+{*spring}+`, for matching 0 or more path segments
108-
at the end of a path. `PathPattern` also restricts the use of `+**+` for matching multiple
109-
path segments such that it's only allowed at the end of a pattern. This eliminates many
110-
cases of ambiguity when choosing the best matching pattern for a given request.
111-
For full pattern syntax please refer to
112-
{spring-framework-api}/web/util/pattern/PathPattern.html[PathPattern] and
113-
{spring-framework-api}/util/AntPathMatcher.html[AntPathMatcher].
114-
115-
Some example patterns:
116-
117-
* `+"/resources/ima?e.png"+` - match one character in a path segment
118-
* `+"/resources/*.png"+` - match zero or more characters in a path segment
119-
* `+"/resources/**"+` - match multiple path segments
120-
* `+"/projects/{project}/versions"+` - match a path segment and capture it as a variable
121-
* `++"/projects/{project:[a-z]+}/versions"++` - match and capture a variable with a regex
99+
You can map requests by using glob patterns and wildcards:
100+
101+
[cols="2,3,5"]
102+
|===
103+
|Pattern |Description |Example
104+
105+
| `spring`
106+
| Literal pattern
107+
| `+"/spring"+` matches `+"/spring"+`
108+
109+
| `+?+`
110+
| Matches one character
111+
| `+"/pages/t?st.html"+` matches `+"/pages/test.html"+` and `+"/pages/t3st.html"+`
112+
113+
| `+*+`
114+
| Matches zero or more characters within a path segment
115+
| `+"/resources/*.png"+` matches `+"/resources/file.png"+`
116+
117+
`+"/projects/*/versions"+` matches `+"/projects/spring/versions"+` but does not match `+"/projects/spring/boot/versions"+`
118+
119+
| `+**+`
120+
| Matches zero or more path segments
121+
| `+"/resources/**"+` matches `+"/resources/file.png"+` and `+"/resources/images/file.png"+`
122+
123+
`+"/**/resources"+` matches `+"/spring/resources"+` and `+"/spring/framework/resources"+`
124+
125+
`+"/resources/**/file.png"+` is invalid as `+**+` is not allowed in the middle of the path.
126+
127+
`+"/**/{name}/resources"+` is invalid as only a literal pattern is allowed right after `+**+`.
128+
`+"/**/project/{project}/resources"+` is allowed.
129+
130+
`+"/**/spring/**"+` is not allowed, as only a single `+**+`/`+{*path}+` instance is allowed per pattern.
131+
132+
| `+{name}+`
133+
| Matches a path segment and captures it as a variable named "name"
134+
| `+"/projects/{project}/versions"+` matches `+"/projects/spring/versions"+` and captures `+project=spring+`
135+
136+
`+"/projects/{project}/versions"+` does not match `+"/projects/spring/framework/versions"+` as it captures a single path segment.
137+
138+
| `+{name:[a-z]+}+`
139+
| Matches the regexp `+"[a-z]+"+` as a path variable named "name"
140+
| `+"/projects/{project:[a-z]+}/versions"+` matches `+"/projects/spring/versions"+` but not `+"/projects/spring1/versions"+`
141+
142+
| `+{*path}+`
143+
| Matches zero or more path segments and captures it as a variable named "path"
144+
| `+"/resources/{*file}"+` matches `+"/resources/images/file.png"+` and captures `+file=/images/file.png+`
145+
146+
`+"{*path}/resources"+` matches `+"/spring/framework/resources"+` and captures `+path=/spring/framework+`
147+
148+
`+"/resources/{*path}/file.png"+` is invalid as `{*path}` is not allowed in the middle of the path.
149+
150+
`+"/{*path}/{name}/resources"+` is invalid as only a literal pattern is allowed right after `{*path}`.
151+
`+"/{*path}/project/{project}/resources"+` is allowed.
152+
153+
`+"/{*path}/spring/**"+` is not allowed, as only a single `+**+`/`+{*path}+` instance is allowed per pattern.
154+
155+
|===
122156

123157
Captured URI variables can be accessed with `@PathVariable`. For example:
124158

spring-web/src/main/java/org/springframework/web/util/pattern/CaptureTheRestPathElement.java renamed to spring-web/src/main/java/org/springframework/web/util/pattern/CaptureSegmentsPathElement.java

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -25,66 +25,85 @@
2525
import org.springframework.web.util.pattern.PathPattern.MatchingContext;
2626

2727
/**
28-
* A path element representing capturing the rest of a path. In the pattern
29-
* '/foo/{*foobar}' the /{*foobar} is represented as a {@link CaptureTheRestPathElement}.
28+
* A path element that captures multiple path segments.
29+
* This element is only allowed in two situations:
30+
* <ol>
31+
* <li>At the start of a path, immediately followed by a {@link LiteralPathElement} like '/{*foobar}/foo/{bar}'
32+
* <li>At the end of a path, like '/foo/{*foobar}'
33+
* </ol>
34+
* <p>Only a single {@link WildcardSegmentsPathElement} or {@link CaptureSegmentsPathElement} element is allowed
35+
* * in a pattern. In the pattern '/foo/{*foobar}' the /{*foobar} is represented as a {@link CaptureSegmentsPathElement}.
3036
*
3137
* @author Andy Clement
38+
* @author Brian Clozel
3239
* @since 5.0
3340
*/
34-
class CaptureTheRestPathElement extends PathElement {
41+
class CaptureSegmentsPathElement extends PathElement {
3542

3643
private final String variableName;
3744

3845

3946
/**
40-
* Create a new {@link CaptureTheRestPathElement} instance.
47+
* Create a new {@link CaptureSegmentsPathElement} instance.
4148
* @param pos position of the path element within the path pattern text
4249
* @param captureDescriptor a character array containing contents like '{' '*' 'a' 'b' '}'
4350
* @param separator the separator used in the path pattern
4451
*/
45-
CaptureTheRestPathElement(int pos, char[] captureDescriptor, char separator) {
52+
CaptureSegmentsPathElement(int pos, char[] captureDescriptor, char separator) {
4653
super(pos, separator);
4754
this.variableName = new String(captureDescriptor, 2, captureDescriptor.length - 3);
4855
}
4956

5057

5158
@Override
5259
public boolean matches(int pathIndex, MatchingContext matchingContext) {
53-
// No need to handle 'match start' checking as this captures everything
54-
// anyway and cannot be followed by anything else
55-
// assert next == null
56-
57-
// If there is more data, it must start with the separator
58-
if (pathIndex < matchingContext.pathLength && !matchingContext.isSeparator(pathIndex)) {
60+
// wildcard segments at the start of the pattern
61+
if (pathIndex == 0 && this.next != null) {
62+
int endPathIndex = pathIndex;
63+
while (endPathIndex < matchingContext.pathLength) {
64+
if (this.next.matches(endPathIndex, matchingContext)) {
65+
collectParameters(matchingContext, pathIndex, endPathIndex);
66+
return true;
67+
}
68+
endPathIndex++;
69+
}
70+
return false;
71+
}
72+
// match until the end of the path
73+
else if (pathIndex < matchingContext.pathLength && !matchingContext.isSeparator(pathIndex)) {
5974
return false;
6075
}
6176
if (matchingContext.determineRemainingPath) {
6277
matchingContext.remainingPathIndex = matchingContext.pathLength;
6378
}
79+
collectParameters(matchingContext, pathIndex, matchingContext.pathLength);
80+
return true;
81+
}
82+
83+
private void collectParameters(MatchingContext matchingContext, int pathIndex, int endPathIndex) {
6484
if (matchingContext.extractingVariables) {
6585
// Collect the parameters from all the remaining segments
66-
MultiValueMap<String,String> parametersCollector = null;
67-
for (int i = pathIndex; i < matchingContext.pathLength; i++) {
86+
MultiValueMap<String, String> parametersCollector = NO_PARAMETERS;
87+
for (int i = pathIndex; i < endPathIndex; i++) {
6888
Element element = matchingContext.pathElements.get(i);
6989
if (element instanceof PathSegment pathSegment) {
7090
MultiValueMap<String, String> parameters = pathSegment.parameters();
7191
if (!parameters.isEmpty()) {
72-
if (parametersCollector == null) {
92+
if (parametersCollector == NO_PARAMETERS) {
7393
parametersCollector = new LinkedMultiValueMap<>();
7494
}
7595
parametersCollector.addAll(parameters);
7696
}
7797
}
7898
}
79-
matchingContext.set(this.variableName, pathToString(pathIndex, matchingContext.pathElements),
80-
parametersCollector == null?NO_PARAMETERS:parametersCollector);
99+
matchingContext.set(this.variableName, pathToString(pathIndex, endPathIndex, matchingContext.pathElements),
100+
parametersCollector);
81101
}
82-
return true;
83102
}
84103

85-
private String pathToString(int fromSegment, List<Element> pathElements) {
104+
private String pathToString(int fromSegment, int toSegment, List<Element> pathElements) {
86105
StringBuilder sb = new StringBuilder();
87-
for (int i = fromSegment, max = pathElements.size(); i < max; i++) {
106+
for (int i = fromSegment, max = toSegment; i < max; i++) {
88107
Element element = pathElements.get(i);
89108
if (element instanceof PathSegment pathSegment) {
90109
sb.append(pathSegment.valueToMatch());
@@ -119,7 +138,7 @@ public int getCaptureCount() {
119138

120139
@Override
121140
public String toString() {
122-
return "CaptureTheRest(/{*" + this.variableName + "})";
141+
return "CaptureSegments(/{*" + this.variableName + "})";
123142
}
124143

125144
}

0 commit comments

Comments
 (0)