diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/ConsumesRequestCondition.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/ConsumesRequestCondition.java index 570857ea9dbf3d8da3a7d671ca4fd243198f8ab6..ef150a590de74946408cb4582c1daa5ccf780e15 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/ConsumesRequestCondition.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/ConsumesRequestCondition.java @@ -44,7 +44,7 @@ import org.springframework.web.servlet.mvc.condition.HeadersRequestCondition.Hea * @author Rossen Stoyanchev * @since 3.1 */ -public class ConsumesRequestCondition extends AbstractRequestCondition { +public final class ConsumesRequestCondition extends AbstractRequestCondition { private final List expressions; diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/HeadersRequestCondition.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/HeadersRequestCondition.java index 016e0d10570d415bc747ccd6c4d0570679029b1d..d1190ef2d8ae9a5b8d73ee37445bb2586b8ef7ef 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/HeadersRequestCondition.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/HeadersRequestCondition.java @@ -39,7 +39,7 @@ import org.springframework.web.bind.annotation.RequestMapping; * @author Rossen Stoyanchev * @since 3.1 */ -public class HeadersRequestCondition extends AbstractRequestCondition { +public final class HeadersRequestCondition extends AbstractRequestCondition { private final Set expressions; diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/ParamsRequestCondition.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/ParamsRequestCondition.java index df5108160b06224f72305c365e48d8737ae5d576..b2891656a142bbca039778d70d2295275e793174 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/ParamsRequestCondition.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/ParamsRequestCondition.java @@ -36,7 +36,7 @@ import org.springframework.web.util.WebUtils; * @author Rossen Stoyanchev * @since 3.1 */ -public class ParamsRequestCondition extends AbstractRequestCondition { +public final class ParamsRequestCondition extends AbstractRequestCondition { private final Set expressions; diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/PatternsRequestCondition.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/PatternsRequestCondition.java index 167b5ed00fca8baf7fc6c5a8b535711f53dd3b9e..1073b7b31c13a2db10a3595e6fbd8506bfaaa898 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/PatternsRequestCondition.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/PatternsRequestCondition.java @@ -41,7 +41,7 @@ import org.springframework.web.util.UrlPathHelper; * @author Rossen Stoyanchev * @since 3.1 */ -public class PatternsRequestCondition extends AbstractRequestCondition { +public final class PatternsRequestCondition extends AbstractRequestCondition { private final Set patterns; @@ -49,14 +49,7 @@ public class PatternsRequestCondition extends AbstractRequestCondition asList(String... patterns) { @@ -77,10 +83,14 @@ public class PatternsRequestCondition extends AbstractRequestCondition patterns, UrlPathHelper urlPathHelper, PathMatcher pathMatcher) { + private PatternsRequestCondition(Collection patterns, + UrlPathHelper urlPathHelper, + PathMatcher pathMatcher, + boolean useSuffixPatternMatch) { this.patterns = Collections.unmodifiableSet(prependLeadingSlash(patterns)); - this.urlPathHelper = urlPathHelper; - this.pathMatcher = pathMatcher; + this.urlPathHelper = urlPathHelper != null ? urlPathHelper : new UrlPathHelper(); + this.pathMatcher = pathMatcher != null ? pathMatcher : new AntPathMatcher(); + this.useSuffixPatternMatch = useSuffixPatternMatch; } private static Set prependLeadingSlash(Collection patterns) { @@ -139,7 +149,7 @@ public class PatternsRequestCondition extends AbstractRequestCondition { +public final class ProducesRequestCondition extends AbstractRequestCondition { private final List expressions; diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/RequestMethodsRequestCondition.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/RequestMethodsRequestCondition.java index 7d9a2eefa838b49b5f85552e134176b8227393ae..dfe9809e91be8fcc7e6e9fe94421474ebe8d12af 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/RequestMethodsRequestCondition.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/condition/RequestMethodsRequestCondition.java @@ -36,7 +36,7 @@ import org.springframework.web.bind.annotation.RequestMethod; * @author Rossen Stoyanchev * @since 3.1 */ -public class RequestMethodsRequestCondition extends AbstractRequestCondition { +public final class RequestMethodsRequestCondition extends AbstractRequestCondition { private final Set methods; diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfo.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfo.java index ff8f97f295bf8154a10bf072b9c8946b2ee443e5..448a6e3563f25253121250791867cc2b806b5265 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfo.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfo.java @@ -37,7 +37,7 @@ import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondit *
  • {@link ProducesRequestCondition}
  • * * - * Optionally a custom request condition may also be provided. + * Optionally a custom request condition may be provided. * * @author Arjen Poutsma * @author Rossen Stoyanchev @@ -80,6 +80,14 @@ public final class RequestMappingInfo { this.customCondition = custom != null ? new CustomRequestCondition(custom) : new CustomRequestCondition(); } + /** + * Re-create a {@link RequestMappingInfo} with the given custom {@link RequestCondition}. + */ + public RequestMappingInfo(RequestMappingInfo info, RequestCondition customRequestCondition) { + this(info.patternsCondition, info.methodsCondition, info.paramsCondition, info.headersCondition, + info.consumesCondition, info.producesCondition, customRequestCondition); + } + /** * Returns the URL patterns of this {@link RequestMappingInfo}; * or instance with 0 patterns, never {@code null} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java index 858b3222ccaabe5faf431d80684680ea2aa5f83a..34fc2a626e522b14225775495c4e9f880c6af71f 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java @@ -20,13 +20,13 @@ import java.lang.reflect.Method; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.stereotype.Controller; -import org.springframework.util.PathMatcher; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.servlet.mvc.condition.ConsumesRequestCondition; import org.springframework.web.servlet.mvc.condition.HeadersRequestCondition; import org.springframework.web.servlet.mvc.condition.ParamsRequestCondition; import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition; import org.springframework.web.servlet.mvc.condition.ProducesRequestCondition; +import org.springframework.web.servlet.mvc.condition.RequestCondition; import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition; import org.springframework.web.servlet.mvc.method.RequestMappingInfo; import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping; @@ -41,9 +41,28 @@ import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMappi */ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping { + private boolean useSuffixPatternMatch = true; + + /** + * Set whether to use a suffix pattern match (".*") when matching patterns to URLs. + * If enabled a method mapped to "/users" will also match to "/users.*". + *

    Default is "true". Turn this convention off if you intend to interpret path mappings strictly. + */ + public void setUseSuffixPatternMatch(boolean useSuffixPatternMatch) { + this.useSuffixPatternMatch = useSuffixPatternMatch; + } + + /** + * Returns the value of the useSuffixPatternMatch flag, see {@link #setUseSuffixPatternMatch(boolean)}. + */ + public boolean isUseSuffixPatternMatch() { + return useSuffixPatternMatch; + } + /** - * {@inheritDoc} The handler determination in this method is made based on the presence of a type-level {@link - * Controller} annotation. + * {@inheritDoc} + * The default implementation checks for the presence of a type-level {@link Controller} + * annotation via {@link AnnotationUtils#findAnnotation(Class, Class)}. */ @Override protected boolean isHandler(Class beanType) { @@ -51,40 +70,54 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi } /** - * Provides a {@link RequestMappingInfo} for the given method.

    Only {@link RequestMapping @RequestMapping}-annotated - * methods are considered. Type-level {@link RequestMapping @RequestMapping} annotations are also detected and their - * attributes combined with method-level {@link RequestMapping @RequestMapping} attributes. + * Determines if the given method is a handler method and creates a {@link RequestMappingInfo} for it. + * + *

    The default implementation expects the presence of a method-level @{@link RequestMapping} + * annotation via {@link AnnotationUtils#findAnnotation(Class, Class)}. The presence of + * type-level annotations is also checked and if present a RequestMappingInfo is created for each type- + * and method-level annotations and combined via {@link RequestMappingInfo#combine(RequestMappingInfo)}. * - * @param method the method to create a mapping for + * @param method the method to create a RequestMappingInfo for * @param handlerType the actual handler type, possibly a sub-type of {@code method.getDeclaringClass()} - * @return the mapping, or {@code null} - * @see RequestMappingInfo#combine(RequestMappingInfo, PathMatcher) + * @return the info, or {@code null} */ @Override protected RequestMappingInfo getMappingForMethod(Method method, Class handlerType) { - RequestMapping methodAnnotation = AnnotationUtils.findAnnotation(method, RequestMapping.class); - if (methodAnnotation == null) { - return null; - } - RequestMappingInfo methodInfo = createFromRequestMapping(methodAnnotation); - RequestMapping typeAnnotation = AnnotationUtils.findAnnotation(handlerType, RequestMapping.class); - if (typeAnnotation != null) { - RequestMappingInfo typeInfo = createFromRequestMapping(typeAnnotation); - return typeInfo.combine(methodInfo); - } - else { - return methodInfo; + RequestMapping methodAnnot = AnnotationUtils.findAnnotation(method, RequestMapping.class); + if (methodAnnot != null) { + RequestMapping typeAnnot = AnnotationUtils.findAnnotation(handlerType, RequestMapping.class); + RequestMappingInfo methodInfo = createRequestMappingInfo(methodAnnot, handlerType, method); + if (typeAnnot != null) { + RequestMappingInfo typeInfo = createRequestMappingInfo(typeAnnot, handlerType, method); + return typeInfo.combine(methodInfo); + } + else { + return methodInfo; + } } + return null; } - private RequestMappingInfo createFromRequestMapping(RequestMapping annotation) { + /** + * Override this method to create a {@link RequestMappingInfo} from a @{@link RequestMapping} annotation. The main + * reason for doing so is to provide a custom {@link RequestCondition} to the RequestMappingInfo constructor. + * + *

    This method is invoked both for type- and method-level @{@link RequestMapping} annotations. The resulting + * {@link RequestMappingInfo}s are combined via {@link RequestMappingInfo#combine(RequestMappingInfo)}. + * + * @param annot a type- or a method-level {@link RequestMapping} annotation + * @param handlerType the handler type + * @param method the method with which the created RequestMappingInfo will be combined + * @return a {@link RequestMappingInfo} instance; never {@code null} + */ + protected RequestMappingInfo createRequestMappingInfo(RequestMapping annot, Class handlerType, Method method) { return new RequestMappingInfo( - new PatternsRequestCondition(annotation.value(), getUrlPathHelper(), getPathMatcher()), - new RequestMethodsRequestCondition(annotation.method()), - new ParamsRequestCondition(annotation.params()), - new HeadersRequestCondition(annotation.headers()), - new ConsumesRequestCondition(annotation.consumes(), annotation.headers()), - new ProducesRequestCondition(annotation.produces(), annotation.headers()), null); + new PatternsRequestCondition(annot.value(), getUrlPathHelper(), getPathMatcher(), useSuffixPatternMatch), + new RequestMethodsRequestCondition(annot.method()), + new ParamsRequestCondition(annot.params()), + new HeadersRequestCondition(annot.headers()), + new ConsumesRequestCondition(annot.consumes(), annot.headers()), + new ProducesRequestCondition(annot.produces(), annot.headers()), null); } - + } diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/condition/PatternsRequestConditionTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/condition/PatternsRequestConditionTests.java index a0f4e29cf4307b43d3f514b5fbf5e06beddd05d5..de54f7260ef599830bb3a03706338b919ba89b88 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/condition/PatternsRequestConditionTests.java +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/condition/PatternsRequestConditionTests.java @@ -97,21 +97,39 @@ public class PatternsRequestConditionTests { } @Test - public void matchImplicitByExtension() { - PatternsRequestCondition condition = new PatternsRequestCondition("/foo"); - PatternsRequestCondition match = condition.getMatchingCondition(new MockHttpServletRequest("GET", "/foo.html")); + public void matchSuffixPattern() { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo.html"); + + PatternsRequestCondition condition = new PatternsRequestCondition("/{foo}"); + PatternsRequestCondition match = condition.getMatchingCondition(request); + + assertNotNull(match); + assertEquals("/{foo}.*", match.getPatterns().iterator().next()); + + condition = new PatternsRequestCondition(new String[] {"/{foo}"}, null, null, false); + match = condition.getMatchingCondition(request); assertNotNull(match); - assertEquals("/foo.*", match.getPatterns().iterator().next()); + assertEquals("/{foo}", match.getPatterns().iterator().next()); } @Test - public void matchImplicitTrailingSlash() { + public void matchTrailingSlash() { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo/"); + PatternsRequestCondition condition = new PatternsRequestCondition("/foo"); - PatternsRequestCondition match = condition.getMatchingCondition(new MockHttpServletRequest("GET", "/foo/")); + PatternsRequestCondition match = condition.getMatchingCondition(request); assertNotNull(match); assertEquals("/foo/", match.getPatterns().iterator().next()); + + boolean useSuffixPatternMatch = false; + condition = new PatternsRequestCondition(new String[] {"/foo"}, null, null, useSuffixPatternMatch); + match = condition.getMatchingCondition(request); + + assertNotNull(match); + assertEquals("Trailing slash should be insensitive to useSuffixPatternMatch settings (SPR-6164, SPR-5636)", + "/foo/", match.getPatterns().iterator().next()); } @Test diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMappingTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMappingTests.java index dd6e720cadf7d915d935301bc66eeadb2176bc0c..11e550d4d656e150be5670015e49ae4bdf71fed9 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMappingTests.java +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMappingTests.java @@ -268,7 +268,7 @@ public class RequestMappingInfoHandlerMappingTests { protected RequestMappingInfo getMappingForMethod(Method method, Class handlerType) { RequestMapping annotation = AnnotationUtils.findAnnotation(method, RequestMapping.class); return new RequestMappingInfo( - new PatternsRequestCondition(annotation.value(), getUrlPathHelper(), getPathMatcher()), + new PatternsRequestCondition(annotation.value(), getUrlPathHelper(), getPathMatcher(), true), new RequestMethodsRequestCondition(annotation.method()), new ParamsRequestCondition(annotation.params()), new HeadersRequestCondition(annotation.headers()),