Skip to content

Commit 8292491

Browse files
committed
SPR-6164 Add option to disable '.*' pattern matching in RequestMappingHandlerMapping and PatternsRequestCondition
1 parent 2b5d2e5 commit 8292491

File tree

10 files changed

+133
-61
lines changed

10 files changed

+133
-61
lines changed

org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/ConsumesRequestCondition.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
* @author Rossen Stoyanchev
4545
* @since 3.1
4646
*/
47-
public class ConsumesRequestCondition extends AbstractRequestCondition<ConsumesRequestCondition> {
47+
public final class ConsumesRequestCondition extends AbstractRequestCondition<ConsumesRequestCondition> {
4848

4949
private final List<ConsumeMediaTypeExpression> expressions;
5050

org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/HeadersRequestCondition.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
* @author Rossen Stoyanchev
4040
* @since 3.1
4141
*/
42-
public class HeadersRequestCondition extends AbstractRequestCondition<HeadersRequestCondition> {
42+
public final class HeadersRequestCondition extends AbstractRequestCondition<HeadersRequestCondition> {
4343

4444
private final Set<HeaderExpression> expressions;
4545

org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/ParamsRequestCondition.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
* @author Rossen Stoyanchev
3737
* @since 3.1
3838
*/
39-
public class ParamsRequestCondition extends AbstractRequestCondition<ParamsRequestCondition> {
39+
public final class ParamsRequestCondition extends AbstractRequestCondition<ParamsRequestCondition> {
4040

4141
private final Set<ParamExpression> expressions;
4242

org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/PatternsRequestCondition.java

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -41,22 +41,15 @@
4141
* @author Rossen Stoyanchev
4242
* @since 3.1
4343
*/
44-
public class PatternsRequestCondition extends AbstractRequestCondition<PatternsRequestCondition> {
44+
public final class PatternsRequestCondition extends AbstractRequestCondition<PatternsRequestCondition> {
4545

4646
private final Set<String> patterns;
4747

4848
private final UrlPathHelper urlPathHelper;
4949

5050
private final PathMatcher pathMatcher;
5151

52-
/**
53-
* Creates a new {@link PatternsRequestCondition} with the given URL patterns.
54-
* Each pattern that is not empty and does not start with "/" is prepended with "/".
55-
* @param patterns 0 or more URL patterns; if 0 the condition will match to every request.
56-
*/
57-
public PatternsRequestCondition(String... patterns) {
58-
this(patterns, new UrlPathHelper(), new AntPathMatcher());
59-
}
52+
private final boolean useSuffixPatternMatch;
6053

6154
/**
6255
* Creates a new {@link PatternsRequestCondition} with the given URL patterns.
@@ -65,9 +58,22 @@ public PatternsRequestCondition(String... patterns) {
6558
* @param patterns the URL patterns to use; if 0, the condition will match to every request.
6659
* @param urlPathHelper a {@link UrlPathHelper} for determining the lookup path for a request
6760
* @param pathMatcher a {@link PathMatcher} for pattern path matching
61+
* @param useSuffixPatternMatch whether to enable matching by suffix (".*")
62+
*/
63+
public PatternsRequestCondition(String[] patterns,
64+
UrlPathHelper urlPathHelper,
65+
PathMatcher pathMatcher,
66+
boolean useSuffixPatternMatch) {
67+
this(asList(patterns), urlPathHelper, pathMatcher, useSuffixPatternMatch);
68+
}
69+
70+
/**
71+
* Creates a new {@link PatternsRequestCondition} with the given URL patterns.
72+
* Each pattern that is not empty and does not start with "/" is prepended with "/".
73+
* @param patterns 0 or more URL patterns; if 0 the condition will match to every request.
6874
*/
69-
public PatternsRequestCondition(String[] patterns, UrlPathHelper urlPathHelper, PathMatcher pathMatcher) {
70-
this(asList(patterns), urlPathHelper, pathMatcher);
75+
public PatternsRequestCondition(String... patterns) {
76+
this(patterns, null, null, true);
7177
}
7278

7379
private static List<String> asList(String... patterns) {
@@ -77,10 +83,14 @@ private static List<String> asList(String... patterns) {
7783
/**
7884
* Private constructor.
7985
*/
80-
private PatternsRequestCondition(Collection<String> patterns, UrlPathHelper urlPathHelper, PathMatcher pathMatcher) {
86+
private PatternsRequestCondition(Collection<String> patterns,
87+
UrlPathHelper urlPathHelper,
88+
PathMatcher pathMatcher,
89+
boolean useSuffixPatternMatch) {
8190
this.patterns = Collections.unmodifiableSet(prependLeadingSlash(patterns));
82-
this.urlPathHelper = urlPathHelper;
83-
this.pathMatcher = pathMatcher;
91+
this.urlPathHelper = urlPathHelper != null ? urlPathHelper : new UrlPathHelper();
92+
this.pathMatcher = pathMatcher != null ? pathMatcher : new AntPathMatcher();
93+
this.useSuffixPatternMatch = useSuffixPatternMatch;
8494
}
8595

8696
private static Set<String> prependLeadingSlash(Collection<String> patterns) {
@@ -139,7 +149,7 @@ else if (!other.patterns.isEmpty()) {
139149
else {
140150
result.add("");
141151
}
142-
return new PatternsRequestCondition(result, urlPathHelper, pathMatcher);
152+
return new PatternsRequestCondition(result, urlPathHelper, pathMatcher, useSuffixPatternMatch);
143153
}
144154

145155
/**
@@ -172,16 +182,19 @@ public PatternsRequestCondition getMatchingCondition(HttpServletRequest request)
172182
}
173183
}
174184
Collections.sort(matches, pathMatcher.getPatternComparator(lookupPath));
175-
return matches.isEmpty() ? null : new PatternsRequestCondition(matches, urlPathHelper, pathMatcher);
185+
return matches.isEmpty() ? null :
186+
new PatternsRequestCondition(matches, urlPathHelper, pathMatcher, useSuffixPatternMatch);
176187
}
177188

178189
private String getMatchingPattern(String pattern, String lookupPath) {
179190
if (pattern.equals(lookupPath)) {
180191
return pattern;
181192
}
182-
boolean hasSuffix = pattern.indexOf('.') != -1;
183-
if (!hasSuffix && pathMatcher.match(pattern + ".*", lookupPath)) {
184-
return pattern + ".*";
193+
if (useSuffixPatternMatch) {
194+
boolean hasSuffix = pattern.indexOf('.') != -1;
195+
if (!hasSuffix && pathMatcher.match(pattern + ".*", lookupPath)) {
196+
return pattern + ".*";
197+
}
185198
}
186199
if (pathMatcher.match(pattern, lookupPath)) {
187200
return pattern;

org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/ProducesRequestCondition.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
* @author Rossen Stoyanchev
4545
* @since 3.1
4646
*/
47-
public class ProducesRequestCondition extends AbstractRequestCondition<ProducesRequestCondition> {
47+
public final class ProducesRequestCondition extends AbstractRequestCondition<ProducesRequestCondition> {
4848

4949
private final List<ProduceMediaTypeExpression> expressions;
5050

org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/RequestMethodsRequestCondition.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
* @author Rossen Stoyanchev
3737
* @since 3.1
3838
*/
39-
public class RequestMethodsRequestCondition extends AbstractRequestCondition<RequestMethodsRequestCondition> {
39+
public final class RequestMethodsRequestCondition extends AbstractRequestCondition<RequestMethodsRequestCondition> {
4040

4141
private final Set<RequestMethod> methods;
4242

org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfo.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
* <li>{@link ProducesRequestCondition}</li>
3838
* </ul>
3939
*
40-
* Optionally a custom request condition may also be provided.
40+
* Optionally a custom request condition may be provided.
4141
*
4242
* @author Arjen Poutsma
4343
* @author Rossen Stoyanchev
@@ -80,6 +80,14 @@ public RequestMappingInfo(PatternsRequestCondition patterns,
8080
this.customCondition = custom != null ? new CustomRequestCondition(custom) : new CustomRequestCondition();
8181
}
8282

83+
/**
84+
* Re-create a {@link RequestMappingInfo} with the given custom {@link RequestCondition}.
85+
*/
86+
public RequestMappingInfo(RequestMappingInfo info, RequestCondition<?> customRequestCondition) {
87+
this(info.patternsCondition, info.methodsCondition, info.paramsCondition, info.headersCondition,
88+
info.consumesCondition, info.producesCondition, customRequestCondition);
89+
}
90+
8391
/**
8492
* Returns the URL patterns of this {@link RequestMappingInfo};
8593
* or instance with 0 patterns, never {@code null}

org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java

Lines changed: 62 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@
2020

2121
import org.springframework.core.annotation.AnnotationUtils;
2222
import org.springframework.stereotype.Controller;
23-
import org.springframework.util.PathMatcher;
2423
import org.springframework.web.bind.annotation.RequestMapping;
2524
import org.springframework.web.servlet.mvc.condition.ConsumesRequestCondition;
2625
import org.springframework.web.servlet.mvc.condition.HeadersRequestCondition;
2726
import org.springframework.web.servlet.mvc.condition.ParamsRequestCondition;
2827
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
2928
import org.springframework.web.servlet.mvc.condition.ProducesRequestCondition;
29+
import org.springframework.web.servlet.mvc.condition.RequestCondition;
3030
import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
3131
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
3232
import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
@@ -41,50 +41,83 @@
4141
*/
4242
public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping {
4343

44+
private boolean useSuffixPatternMatch = true;
45+
46+
/**
47+
* Set whether to use a suffix pattern match (".*") when matching patterns to URLs.
48+
* If enabled a method mapped to "/users" will also match to "/users.*".
49+
* <p>Default is "true". Turn this convention off if you intend to interpret path mappings strictly.
50+
*/
51+
public void setUseSuffixPatternMatch(boolean useSuffixPatternMatch) {
52+
this.useSuffixPatternMatch = useSuffixPatternMatch;
53+
}
54+
55+
/**
56+
* Returns the value of the useSuffixPatternMatch flag, see {@link #setUseSuffixPatternMatch(boolean)}.
57+
*/
58+
public boolean isUseSuffixPatternMatch() {
59+
return useSuffixPatternMatch;
60+
}
61+
4462
/**
45-
* {@inheritDoc} The handler determination in this method is made based on the presence of a type-level {@link
46-
* Controller} annotation.
63+
* {@inheritDoc}
64+
* The default implementation checks for the presence of a type-level {@link Controller}
65+
* annotation via {@link AnnotationUtils#findAnnotation(Class, Class)}.
4766
*/
4867
@Override
4968
protected boolean isHandler(Class<?> beanType) {
5069
return AnnotationUtils.findAnnotation(beanType, Controller.class) != null;
5170
}
5271

5372
/**
54-
* Provides a {@link RequestMappingInfo} for the given method. <p>Only {@link RequestMapping @RequestMapping}-annotated
55-
* methods are considered. Type-level {@link RequestMapping @RequestMapping} annotations are also detected and their
56-
* attributes combined with method-level {@link RequestMapping @RequestMapping} attributes.
73+
* Determines if the given method is a handler method and creates a {@link RequestMappingInfo} for it.
74+
*
75+
* <p>The default implementation expects the presence of a method-level @{@link RequestMapping}
76+
* annotation via {@link AnnotationUtils#findAnnotation(Class, Class)}. The presence of
77+
* type-level annotations is also checked and if present a RequestMappingInfo is created for each type-
78+
* and method-level annotations and combined via {@link RequestMappingInfo#combine(RequestMappingInfo)}.
5779
*
58-
* @param method the method to create a mapping for
80+
* @param method the method to create a RequestMappingInfo for
5981
* @param handlerType the actual handler type, possibly a sub-type of {@code method.getDeclaringClass()}
60-
* @return the mapping, or {@code null}
61-
* @see RequestMappingInfo#combine(RequestMappingInfo, PathMatcher)
82+
* @return the info, or {@code null}
6283
*/
6384
@Override
6485
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
65-
RequestMapping methodAnnotation = AnnotationUtils.findAnnotation(method, RequestMapping.class);
66-
if (methodAnnotation == null) {
67-
return null;
68-
}
69-
RequestMappingInfo methodInfo = createFromRequestMapping(methodAnnotation);
70-
RequestMapping typeAnnotation = AnnotationUtils.findAnnotation(handlerType, RequestMapping.class);
71-
if (typeAnnotation != null) {
72-
RequestMappingInfo typeInfo = createFromRequestMapping(typeAnnotation);
73-
return typeInfo.combine(methodInfo);
74-
}
75-
else {
76-
return methodInfo;
86+
RequestMapping methodAnnot = AnnotationUtils.findAnnotation(method, RequestMapping.class);
87+
if (methodAnnot != null) {
88+
RequestMapping typeAnnot = AnnotationUtils.findAnnotation(handlerType, RequestMapping.class);
89+
RequestMappingInfo methodInfo = createRequestMappingInfo(methodAnnot, handlerType, method);
90+
if (typeAnnot != null) {
91+
RequestMappingInfo typeInfo = createRequestMappingInfo(typeAnnot, handlerType, method);
92+
return typeInfo.combine(methodInfo);
93+
}
94+
else {
95+
return methodInfo;
96+
}
7797
}
98+
return null;
7899
}
79100

80-
private RequestMappingInfo createFromRequestMapping(RequestMapping annotation) {
101+
/**
102+
* Override this method to create a {@link RequestMappingInfo} from a @{@link RequestMapping} annotation. The main
103+
* reason for doing so is to provide a custom {@link RequestCondition} to the RequestMappingInfo constructor.
104+
*
105+
* <p>This method is invoked both for type- and method-level @{@link RequestMapping} annotations. The resulting
106+
* {@link RequestMappingInfo}s are combined via {@link RequestMappingInfo#combine(RequestMappingInfo)}.
107+
*
108+
* @param annot a type- or a method-level {@link RequestMapping} annotation
109+
* @param handlerType the handler type
110+
* @param method the method with which the created RequestMappingInfo will be combined
111+
* @return a {@link RequestMappingInfo} instance; never {@code null}
112+
*/
113+
protected RequestMappingInfo createRequestMappingInfo(RequestMapping annot, Class<?> handlerType, Method method) {
81114
return new RequestMappingInfo(
82-
new PatternsRequestCondition(annotation.value(), getUrlPathHelper(), getPathMatcher()),
83-
new RequestMethodsRequestCondition(annotation.method()),
84-
new ParamsRequestCondition(annotation.params()),
85-
new HeadersRequestCondition(annotation.headers()),
86-
new ConsumesRequestCondition(annotation.consumes(), annotation.headers()),
87-
new ProducesRequestCondition(annotation.produces(), annotation.headers()), null);
115+
new PatternsRequestCondition(annot.value(), getUrlPathHelper(), getPathMatcher(), useSuffixPatternMatch),
116+
new RequestMethodsRequestCondition(annot.method()),
117+
new ParamsRequestCondition(annot.params()),
118+
new HeadersRequestCondition(annot.headers()),
119+
new ConsumesRequestCondition(annot.consumes(), annot.headers()),
120+
new ProducesRequestCondition(annot.produces(), annot.headers()), null);
88121
}
89-
122+
90123
}

org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/condition/PatternsRequestConditionTests.java

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -97,21 +97,39 @@ public void matchSortPatterns() {
9797
}
9898

9999
@Test
100-
public void matchImplicitByExtension() {
101-
PatternsRequestCondition condition = new PatternsRequestCondition("/foo");
102-
PatternsRequestCondition match = condition.getMatchingCondition(new MockHttpServletRequest("GET", "/foo.html"));
100+
public void matchSuffixPattern() {
101+
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo.html");
102+
103+
PatternsRequestCondition condition = new PatternsRequestCondition("/{foo}");
104+
PatternsRequestCondition match = condition.getMatchingCondition(request);
105+
106+
assertNotNull(match);
107+
assertEquals("/{foo}.*", match.getPatterns().iterator().next());
108+
109+
condition = new PatternsRequestCondition(new String[] {"/{foo}"}, null, null, false);
110+
match = condition.getMatchingCondition(request);
103111

104112
assertNotNull(match);
105-
assertEquals("/foo.*", match.getPatterns().iterator().next());
113+
assertEquals("/{foo}", match.getPatterns().iterator().next());
106114
}
107115

108116
@Test
109-
public void matchImplicitTrailingSlash() {
117+
public void matchTrailingSlash() {
118+
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo/");
119+
110120
PatternsRequestCondition condition = new PatternsRequestCondition("/foo");
111-
PatternsRequestCondition match = condition.getMatchingCondition(new MockHttpServletRequest("GET", "/foo/"));
121+
PatternsRequestCondition match = condition.getMatchingCondition(request);
112122

113123
assertNotNull(match);
114124
assertEquals("/foo/", match.getPatterns().iterator().next());
125+
126+
boolean useSuffixPatternMatch = false;
127+
condition = new PatternsRequestCondition(new String[] {"/foo"}, null, null, useSuffixPatternMatch);
128+
match = condition.getMatchingCondition(request);
129+
130+
assertNotNull(match);
131+
assertEquals("Trailing slash should be insensitive to useSuffixPatternMatch settings (SPR-6164, SPR-5636)",
132+
"/foo/", match.getPatterns().iterator().next());
115133
}
116134

117135
@Test

org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMappingTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ protected boolean isHandler(Class<?> beanType) {
268268
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
269269
RequestMapping annotation = AnnotationUtils.findAnnotation(method, RequestMapping.class);
270270
return new RequestMappingInfo(
271-
new PatternsRequestCondition(annotation.value(), getUrlPathHelper(), getPathMatcher()),
271+
new PatternsRequestCondition(annotation.value(), getUrlPathHelper(), getPathMatcher(), true),
272272
new RequestMethodsRequestCondition(annotation.method()),
273273
new ParamsRequestCondition(annotation.params()),
274274
new HeadersRequestCondition(annotation.headers()),

0 commit comments

Comments
 (0)