Skip to content

Commit 763a0ea

Browse files
committed
Add PathPatternRequestMatcher
Closes spring-projectsgh-16429
1 parent 7b8ff72 commit 763a0ea

File tree

3 files changed

+348
-0
lines changed

3 files changed

+348
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
/*
2+
* Copyright 2002-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.web.servlet.util.matcher;
18+
19+
import java.util.Objects;
20+
21+
import jakarta.servlet.http.HttpServletRequest;
22+
23+
import org.springframework.http.HttpMethod;
24+
import org.springframework.http.server.PathContainer;
25+
import org.springframework.http.server.RequestPath;
26+
import org.springframework.security.web.util.matcher.RequestMatcher;
27+
import org.springframework.security.web.util.matcher.RequestMatcherBuilder;
28+
import org.springframework.util.Assert;
29+
import org.springframework.web.util.ServletRequestPathUtils;
30+
import org.springframework.web.util.pattern.PathPattern;
31+
import org.springframework.web.util.pattern.PathPatternParser;
32+
33+
/**
34+
* A {@link RequestMatcher} that uses {@link PathPattern}s to match against each
35+
* {@link HttpServletRequest}. Specifically, this means that the class anticipates that
36+
* the provided pattern does not include the servlet path in order to align with Spring
37+
* MVC.
38+
*
39+
* <p>
40+
* Note that the {@link org.springframework.web.servlet.HandlerMapping} that contains the
41+
* related URI patterns must be using the same
42+
* {@link org.springframework.web.util.pattern.PathPatternParser} configured in this
43+
* class.
44+
* </p>
45+
*
46+
* @author Josh Cummings
47+
* @since 6.5
48+
*/
49+
public final class PathPatternRequestMatcher implements RequestMatcher {
50+
51+
private static final String PATH_ATTRIBUTE = PathPatternRequestMatcher.class + ".PATH";
52+
53+
static final String ANY_SERVLET = new String();
54+
55+
private final PathPattern pattern;
56+
57+
private String servletPath;
58+
59+
private HttpMethod method;
60+
61+
PathPatternRequestMatcher(PathPattern pattern) {
62+
this.pattern = pattern;
63+
}
64+
65+
/**
66+
* Create a {@link Builder} for creating {@link PathPattern}-based request matchers.
67+
* That is, matchers that anticipate patterns do not specify the servlet path.
68+
* @return the {@link Builder}
69+
*/
70+
public static Builder builder() {
71+
return new Builder(PathPatternParser.defaultInstance);
72+
}
73+
74+
/**
75+
* Create a {@link Builder} for creating {@link PathPattern}-based request matchers.
76+
* That is, matchers that anticipate patterns do not specify the servlet path.
77+
* @param parser the {@link PathPatternParser}; only needed when different from
78+
* {@link PathPatternParser#defaultInstance}
79+
* @return the {@link Builder}
80+
*/
81+
public static Builder withPathPatternParser(PathPatternParser parser) {
82+
return new Builder(parser);
83+
}
84+
85+
@Override
86+
public boolean matches(HttpServletRequest request) {
87+
return matcher(request).isMatch();
88+
}
89+
90+
@Override
91+
public MatchResult matcher(HttpServletRequest request) {
92+
if (this.method != null && !this.method.name().equals(request.getMethod())) {
93+
return MatchResult.notMatch();
94+
}
95+
if (this.servletPath != null && !this.servletPath.equals(request.getServletPath())
96+
&& !ANY_SERVLET.equals(this.servletPath)) {
97+
return MatchResult.notMatch();
98+
}
99+
PathContainer path = getPathContainer(request);
100+
PathPattern.PathMatchInfo info = this.pattern.matchAndExtract(path);
101+
return (info != null) ? MatchResult.match(info.getUriVariables()) : MatchResult.notMatch();
102+
}
103+
104+
PathContainer getPathContainer(HttpServletRequest request) {
105+
if (this.servletPath != null) {
106+
return ServletRequestPathUtils.parseAndCache(request).pathWithinApplication();
107+
}
108+
else {
109+
return parseAndCache(request);
110+
}
111+
}
112+
113+
PathContainer parseAndCache(HttpServletRequest request) {
114+
PathContainer path = (PathContainer) request.getAttribute(PATH_ATTRIBUTE);
115+
if (path != null) {
116+
return path;
117+
}
118+
path = RequestPath.parse(request.getRequestURI(), request.getContextPath()).pathWithinApplication();
119+
request.setAttribute(PATH_ATTRIBUTE, path);
120+
return path;
121+
}
122+
123+
void setServletPath(String servletPath) {
124+
this.servletPath = servletPath;
125+
}
126+
127+
void setMethod(HttpMethod method) {
128+
this.method = method;
129+
}
130+
131+
@Override
132+
public boolean equals(Object o) {
133+
if (!(o instanceof PathPatternRequestMatcher that)) {
134+
return false;
135+
}
136+
return Objects.equals(this.pattern, that.pattern) && Objects.equals(this.servletPath, that.servletPath)
137+
&& Objects.equals(this.method, that.method);
138+
}
139+
140+
@Override
141+
public int hashCode() {
142+
return Objects.hash(this.pattern, this.servletPath, this.method);
143+
}
144+
145+
@Override
146+
public String toString() {
147+
return "PathPatternRequestMatcher [pattern=" + this.pattern + ", servletPath=" + this.servletPath + ", method="
148+
+ this.method + ']';
149+
}
150+
151+
/**
152+
* A builder for {@link MvcRequestMatcher}
153+
*
154+
* @author Marcus Da Coregio
155+
* @since 6.5
156+
*/
157+
public static final class Builder implements RequestMatcherBuilder {
158+
159+
private final PathPatternParser parser;
160+
161+
private HttpMethod method;
162+
163+
private String servletPath;
164+
165+
/**
166+
* Construct a new instance of this builder
167+
*/
168+
public Builder(PathPatternParser parser) {
169+
Assert.notNull(parser, "pathPatternParser cannot be null");
170+
this.parser = parser;
171+
}
172+
173+
public Builder method(HttpMethod method) {
174+
this.method = method;
175+
return this;
176+
}
177+
178+
/**
179+
* Sets the servlet path to be used by the {@link MvcRequestMatcher} generated by
180+
* this builder
181+
* @param servletPath the servlet path to use
182+
* @return the {@link MvcRequestMatcher.Builder} for further configuration
183+
*/
184+
public Builder servletPath(String servletPath) {
185+
this.servletPath = servletPath;
186+
return this;
187+
}
188+
189+
/**
190+
* Creates an {@link MvcRequestMatcher} that uses the provided pattern and HTTP
191+
* method to match
192+
* @param method the {@link HttpMethod}, can be null
193+
* @param pattern the patterns used to match
194+
* @return the generated {@link MvcRequestMatcher}
195+
*/
196+
public PathPatternRequestMatcher pattern(HttpMethod method, String pattern) {
197+
String parsed = this.parser.initFullPathPattern(pattern);
198+
PathPattern pathPattern = this.parser.parse(parsed);
199+
PathPatternRequestMatcher requestMatcher = new PathPatternRequestMatcher(pathPattern);
200+
requestMatcher.setServletPath(this.servletPath);
201+
requestMatcher.setMethod(method);
202+
return requestMatcher;
203+
}
204+
205+
}
206+
207+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2002-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.web.util.matcher;
18+
19+
import java.util.ArrayList;
20+
import java.util.List;
21+
22+
import org.springframework.http.HttpMethod;
23+
24+
public interface RequestMatcherBuilder {
25+
26+
default RequestMatcher[] pattern(HttpMethod method, String... patterns) {
27+
List<RequestMatcher> requestMatchers = new ArrayList<>();
28+
for (String pattern : patterns) {
29+
requestMatchers.add(pattern(method, pattern));
30+
}
31+
return requestMatchers.toArray(RequestMatcher[]::new);
32+
}
33+
34+
default RequestMatcher[] pattern(String... patterns) {
35+
return pattern(null, patterns);
36+
}
37+
38+
default RequestMatcher pattern(String pattern) {
39+
return pattern(null, pattern);
40+
}
41+
42+
default RequestMatcher anyRequest() {
43+
return pattern(null, "/**");
44+
}
45+
46+
RequestMatcher pattern(HttpMethod method, String pattern);
47+
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Copyright 2002-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.web.servlet.util.matcher;
18+
19+
import jakarta.servlet.http.MappingMatch;
20+
import org.junit.jupiter.api.Test;
21+
22+
import org.springframework.http.HttpMethod;
23+
import org.springframework.mock.web.MockHttpServletMapping;
24+
import org.springframework.mock.web.MockHttpServletRequest;
25+
import org.springframework.security.web.util.matcher.RequestMatcher;
26+
27+
import static org.assertj.core.api.Assertions.assertThat;
28+
29+
/**
30+
* Tests for {@link PathPatternRequestMatcher}
31+
*/
32+
public class PathPatternRequestMatcherTests {
33+
34+
@Test
35+
void matcherWhenPatternMatchesRequestThenMatchResult() {
36+
RequestMatcher matcher = PathPatternRequestMatcher.builder().pattern("/uri");
37+
assertThat(matcher.matches(request("GET", "/uri"))).isTrue();
38+
}
39+
40+
@Test
41+
void matcherWhenPatternContainsPlaceholdersThenMatchResult() {
42+
RequestMatcher matcher = PathPatternRequestMatcher.builder().pattern("/uri/{username}");
43+
assertThat(matcher.matcher(request("GET", "/uri/bob")).getVariables()).containsEntry("username", "bob");
44+
}
45+
46+
@Test
47+
void matcherWhenSameServletPathThenMatchResult() {
48+
RequestMatcher matcher = PathPatternRequestMatcher.builder().servletPath("/mvc").pattern("/uri");
49+
assertThat(matcher.matches(request("GET", "/mvc/uri", "/mvc"))).isTrue();
50+
}
51+
52+
@Test
53+
void matcherWhenSameMethodThenMatchResult() {
54+
RequestMatcher matcher = PathPatternRequestMatcher.builder().pattern(HttpMethod.GET, "/uri");
55+
assertThat(matcher.matches(request("GET", "/uri"))).isTrue();
56+
}
57+
58+
@Test
59+
void matcherWhenDifferentPathThenNotMatchResult() {
60+
RequestMatcher matcher = PathPatternRequestMatcher.builder()
61+
.servletPath("/mvc")
62+
.pattern(HttpMethod.GET, "/uri");
63+
assertThat(matcher.matches(request("GET", "/uri", ""))).isFalse();
64+
}
65+
66+
@Test
67+
void matcherWhenDifferentMethodThenNotMatchResult() {
68+
RequestMatcher matcher = PathPatternRequestMatcher.builder()
69+
.servletPath("/mvc")
70+
.pattern(HttpMethod.GET, "/uri");
71+
assertThat(matcher.matches(request("POST", "/mvc/uri", "/mvc"))).isFalse();
72+
}
73+
74+
@Test
75+
void matcherWhenNoServletPathThenMatchAbsolute() {
76+
RequestMatcher matcher = PathPatternRequestMatcher.builder().pattern(HttpMethod.GET, "/uri");
77+
assertThat(matcher.matches(request("GET", "/mvc/uri", "/mvc"))).isFalse();
78+
assertThat(matcher.matches(request("GET", "/uri", ""))).isTrue();
79+
}
80+
81+
MockHttpServletRequest request(String method, String uri) {
82+
return new MockHttpServletRequest(method, uri);
83+
}
84+
85+
MockHttpServletRequest request(String method, String uri, String servletPath) {
86+
MockHttpServletRequest request = new MockHttpServletRequest(method, uri);
87+
request.setServletPath(servletPath);
88+
request
89+
.setHttpServletMapping(new MockHttpServletMapping(uri, servletPath + "/*", "servlet", MappingMatch.PATH));
90+
return request;
91+
}
92+
93+
}

0 commit comments

Comments
 (0)