diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java index ed756dfc8c..42c4960fbd 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -163,6 +163,8 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { handlerMappingDef.getPropertyValues().add("removeSemicolonContent", !enableMatrixVariables); } + configurePathMatchingProperties(handlerMappingDef, element, parserContext); + RuntimeBeanReference conversionService = getConversionService(element, source, parserContext); RuntimeBeanReference validator = getValidator(element, source, parserContext); RuntimeBeanReference messageCodesResolver = getMessageCodesResolver(element); @@ -309,6 +311,40 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { return contentNegotiationManagerRef; } + private void configurePathMatchingProperties(RootBeanDefinition handlerMappingDef, Element element, + ParserContext parserContext) { + + Element pathMatchingElement = DomUtils.getChildElementByTagName(element, "path-matching"); + if (pathMatchingElement != null) { + Object source = parserContext.extractSource(element); + if (pathMatchingElement.hasAttribute("suffix-pattern")) { + Boolean useSuffixPatternMatch = Boolean.valueOf(pathMatchingElement.getAttribute("suffix-pattern")); + handlerMappingDef.getPropertyValues().add("useSuffixPatternMatch", useSuffixPatternMatch); + } + if (pathMatchingElement.hasAttribute("trailing-slash")) { + Boolean useTrailingSlashMatch = Boolean.valueOf(pathMatchingElement.getAttribute("trailing-slash")); + handlerMappingDef.getPropertyValues().add("useTrailingSlashMatch", useTrailingSlashMatch); + } + if (pathMatchingElement.hasAttribute("registered-suffixes-only")) { + Boolean useRegisteredSuffixPatternMatch = Boolean.valueOf(pathMatchingElement.getAttribute("registered-suffixes-only")); + handlerMappingDef.getPropertyValues().add("useRegisteredSuffixPatternMatch", useRegisteredSuffixPatternMatch); + } + RuntimeBeanReference pathHelperRef = null; + if (pathMatchingElement.hasAttribute("path-helper")) { + pathHelperRef = new RuntimeBeanReference(pathMatchingElement.getAttribute("path-helper")); + } + pathHelperRef = MvcNamespaceUtils.registerUrlPathHelper(pathHelperRef, parserContext, source); + handlerMappingDef.getPropertyValues().add("urlPathHelper", pathHelperRef); + + RuntimeBeanReference pathMatcherRef = null; + if (pathMatchingElement.hasAttribute("path-matcher")) { + pathMatcherRef = new RuntimeBeanReference(pathMatchingElement.getAttribute("path-matcher")); + } + pathMatcherRef = MvcNamespaceUtils.registerPathMatcher(pathMatcherRef, parserContext, source); + handlerMappingDef.getPropertyValues().add("pathMatcher", pathMatcherRef); + } + } + private Properties getDefaultMediaTypes() { Properties props = new Properties(); if (romePresent) { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/MvcNamespaceUtils.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/MvcNamespaceUtils.java index abf53b4e2c..4ce612ff79 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/MvcNamespaceUtils.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/MvcNamespaceUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,12 +17,16 @@ package org.springframework.web.servlet.config; import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.parsing.BeanComponentDefinition; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.PathMatcher; import org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping; import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter; import org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter; +import org.springframework.web.util.UrlPathHelper; /** * Convenience methods for use in MVC namespace BeanDefinitionParsers. @@ -41,12 +45,70 @@ abstract class MvcNamespaceUtils { private static final String HTTP_REQUEST_HANDLER_ADAPTER_BEAN_NAME = HttpRequestHandlerAdapter.class.getName(); + private static final String URL_PATH_HELPER_BEAN_NAME = "mvcUrlPathHelper"; + + private static final String PATH_MATCHER_BEAN_NAME = "mvcPathMatcher"; + + public static void registerDefaultComponents(ParserContext parserContext, Object source) { registerBeanNameUrlHandlerMapping(parserContext, source); registerHttpRequestHandlerAdapter(parserContext, source); registerSimpleControllerHandlerAdapter(parserContext, source); } + + /** + * Adds an alias to an existing well-known name or registers a new instance of a {@link UrlPathHelper} + * under that well-known name, unless already registered. + * @return a RuntimeBeanReference to this {@link UrlPathHelper} instance + * @since 3.2.17 + */ + public static RuntimeBeanReference registerUrlPathHelper(RuntimeBeanReference urlPathHelperRef, + ParserContext parserContext, Object source) { + + if (urlPathHelperRef != null) { + if (parserContext.getRegistry().isAlias(URL_PATH_HELPER_BEAN_NAME)) { + parserContext.getRegistry().removeAlias(URL_PATH_HELPER_BEAN_NAME); + } + parserContext.getRegistry().registerAlias(urlPathHelperRef.getBeanName(), URL_PATH_HELPER_BEAN_NAME); + } + else if (!parserContext.getRegistry().isAlias(URL_PATH_HELPER_BEAN_NAME) + && !parserContext.getRegistry().containsBeanDefinition(URL_PATH_HELPER_BEAN_NAME)) { + RootBeanDefinition urlPathHelperDef = new RootBeanDefinition(UrlPathHelper.class); + urlPathHelperDef.setSource(source); + urlPathHelperDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + parserContext.getRegistry().registerBeanDefinition(URL_PATH_HELPER_BEAN_NAME, urlPathHelperDef); + parserContext.registerComponent(new BeanComponentDefinition(urlPathHelperDef, URL_PATH_HELPER_BEAN_NAME)); + } + return new RuntimeBeanReference(URL_PATH_HELPER_BEAN_NAME); + } + + /** + * Adds an alias to an existing well-known name or registers a new instance of a {@link PathMatcher} + * under that well-known name, unless already registered. + * @return a RuntimeBeanReference to this {@link PathMatcher} instance + * @since 3.2.17 + */ + public static RuntimeBeanReference registerPathMatcher(RuntimeBeanReference pathMatcherRef, + ParserContext parserContext, Object source) { + + if (pathMatcherRef != null) { + if (parserContext.getRegistry().isAlias(PATH_MATCHER_BEAN_NAME)) { + parserContext.getRegistry().removeAlias(PATH_MATCHER_BEAN_NAME); + } + parserContext.getRegistry().registerAlias(pathMatcherRef.getBeanName(), PATH_MATCHER_BEAN_NAME); + } + else if (!parserContext.getRegistry().isAlias(PATH_MATCHER_BEAN_NAME) + && !parserContext.getRegistry().containsBeanDefinition(PATH_MATCHER_BEAN_NAME)) { + RootBeanDefinition pathMatcherDef = new RootBeanDefinition(AntPathMatcher.class); + pathMatcherDef.setSource(source); + pathMatcherDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + parserContext.getRegistry().registerBeanDefinition(PATH_MATCHER_BEAN_NAME, pathMatcherDef); + parserContext.registerComponent(new BeanComponentDefinition(pathMatcherDef, PATH_MATCHER_BEAN_NAME)); + } + return new RuntimeBeanReference(PATH_MATCHER_BEAN_NAME); + } + /** * Registers an {@link HttpRequestHandlerAdapter} under a well-known * name unless already registered. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ResourcesBeanDefinitionParser.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ResourcesBeanDefinitionParser.java index 6bfdb9c56f..18e81969a0 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ResourcesBeanDefinitionParser.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ResourcesBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import java.util.Map; import org.w3c.dom.Element; import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.parsing.BeanComponentDefinition; import org.springframework.beans.factory.support.ManagedList; import org.springframework.beans.factory.support.ManagedMap; @@ -62,10 +63,15 @@ class ResourcesBeanDefinitionParser implements BeanDefinitionParser { } urlMap.put(resourceRequestPath, resourceHandlerName); + RuntimeBeanReference pathMatcherRef = MvcNamespaceUtils.registerPathMatcher(null, parserContext, source); + RuntimeBeanReference pathHelperRef = MvcNamespaceUtils.registerUrlPathHelper(null, parserContext, source); + RootBeanDefinition handlerMappingDef = new RootBeanDefinition(SimpleUrlHandlerMapping.class); handlerMappingDef.setSource(source); handlerMappingDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); handlerMappingDef.getPropertyValues().add("urlMap", urlMap); + handlerMappingDef.getPropertyValues().add("pathMatcher", pathMatcherRef); + handlerMappingDef.getPropertyValues().add("urlPathHelper", pathHelperRef); String order = element.getAttribute("order"); // use a default of near-lowest precedence, still allowing for even lower precedence in other mappings diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ViewControllerBeanDefinitionParser.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ViewControllerBeanDefinitionParser.java index e580aea38b..3d077a7102 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ViewControllerBeanDefinitionParser.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ViewControllerBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -76,6 +76,10 @@ class ViewControllerBeanDefinitionParser implements BeanDefinitionParser { RootBeanDefinition handlerMappingDef = new RootBeanDefinition(SimpleUrlHandlerMapping.class); handlerMappingDef.setSource(source); handlerMappingDef.getPropertyValues().add("order", "1"); + handlerMappingDef.getPropertyValues().add("pathMatcher", + MvcNamespaceUtils.registerPathMatcher(null, parserContext, source)); + handlerMappingDef.getPropertyValues().add("urlPathHelper", + MvcNamespaceUtils.registerUrlPathHelper(null, parserContext, source)); handlerMappingDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); parserContext.getRegistry().registerBeanDefinition(HANDLER_MAPPING_BEAN_NAME, handlerMappingDef); parserContext.registerComponent(new BeanComponentDefinition(handlerMappingDef, HANDLER_MAPPING_BEAN_NAME)); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfiguration.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfiguration.java index 579de783eb..46d4a1842f 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfiguration.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -67,6 +67,11 @@ public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport { this.configurers.configureAsyncSupport(configurer); } + @Override + public void configurePathMatch(PathMatchConfigurer configurer) { + this.configurers.configurePathMatch(configurer); + } + @Override protected void addViewControllers(ViewControllerRegistry registry) { this.configurers.addViewControllers(registry); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/PathMatchConfigurer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/PathMatchConfigurer.java new file mode 100644 index 0000000000..54e1160d92 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/PathMatchConfigurer.java @@ -0,0 +1,129 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet.config.annotation; + +import org.springframework.util.PathMatcher; +import org.springframework.web.util.UrlPathHelper; + +/** + * Helps with configuring HandlerMappings path matching options such as trailing + * slash match, suffix registration, path matcher and path helper. + * + *

Configured path matcher and path helper instances are shared for: + *

+ * + * @author Brian Clozel + * @since 3.2.17 + * @see org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping + * @see org.springframework.web.servlet.handler.SimpleUrlHandlerMapping + */ +public class PathMatchConfigurer { + + private Boolean suffixPatternMatch; + + private Boolean trailingSlashMatch; + + private Boolean registeredSuffixPatternMatch; + + private UrlPathHelper urlPathHelper; + + private PathMatcher pathMatcher; + + + /** + * Whether to use suffix pattern match (".*") when matching patterns to + * requests. If enabled a method mapped to "/users" also matches to "/users.*". + *

By default this is set to {@code true}. + * @see #registeredSuffixPatternMatch + */ + public PathMatchConfigurer setUseSuffixPatternMatch(Boolean suffixPatternMatch) { + this.suffixPatternMatch = suffixPatternMatch; + return this; + } + + /** + * Whether to match to URLs irrespective of the presence of a trailing slash. + * If enabled a method mapped to "/users" also matches to "/users/". + *

The default value is {@code true}. + */ + public PathMatchConfigurer setUseTrailingSlashMatch(Boolean trailingSlashMatch) { + this.trailingSlashMatch = trailingSlashMatch; + return this; + } + + /** + * Whether suffix pattern matching should work only against path extensions + * explicitly registered when you + * {@link WebMvcConfigurer#configureContentNegotiation configure content + * negotiation}. This is generally recommended to reduce ambiguity and to + * avoid issues such as when a "." appears in the path for other reasons. + *

By default this is set to "false". + * @see WebMvcConfigurer#configureContentNegotiation + */ + public PathMatchConfigurer setUseRegisteredSuffixPatternMatch( + Boolean registeredSuffixPatternMatch) { + + this.registeredSuffixPatternMatch = registeredSuffixPatternMatch; + return this; + } + + /** + * Set the UrlPathHelper to use for resolution of lookup paths. + *

Use this to override the default UrlPathHelper with a custom subclass, + * or to share common UrlPathHelper settings across multiple HandlerMappings + * and MethodNameResolvers. + */ + public PathMatchConfigurer setUrlPathHelper(UrlPathHelper urlPathHelper) { + this.urlPathHelper = urlPathHelper; + return this; + } + + /** + * Set the PathMatcher implementation to use for matching URL paths + * against registered URL patterns. Default is AntPathMatcher. + * @see org.springframework.util.AntPathMatcher + */ + public PathMatchConfigurer setPathMatcher(PathMatcher pathMatcher) { + this.pathMatcher = pathMatcher; + return this; + } + + public Boolean isUseSuffixPatternMatch() { + return this.suffixPatternMatch; + } + + public Boolean isUseTrailingSlashMatch() { + return this.trailingSlashMatch; + } + + public Boolean isUseRegisteredSuffixPatternMatch() { + return this.registeredSuffixPatternMatch; + } + + public UrlPathHelper getUrlPathHelper() { + return this.urlPathHelper; + } + + public PathMatcher getPathMatcher() { + return this.pathMatcher; + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java index 7473330449..6742aad8af 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,7 +47,9 @@ import org.springframework.http.converter.json.MappingJacksonHttpMessageConverte import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter; import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter; import org.springframework.http.converter.xml.SourceHttpMessageConverter; +import org.springframework.util.AntPathMatcher; import org.springframework.util.ClassUtils; +import org.springframework.util.PathMatcher; import org.springframework.validation.Errors; import org.springframework.validation.MessageCodesResolver; import org.springframework.validation.Validator; @@ -75,6 +77,7 @@ import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExc import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver; +import org.springframework.web.util.UrlPathHelper; /** * This is the main class providing the configuration behind the MVC Java config. @@ -163,6 +166,8 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv private ContentNegotiationManager contentNegotiationManager; + private PathMatchConfigurer pathMatchConfigurer; + private List> messageConverters; @@ -191,6 +196,24 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv handlerMapping.setOrder(0); handlerMapping.setInterceptors(getInterceptors()); handlerMapping.setContentNegotiationManager(mvcContentNegotiationManager()); + + PathMatchConfigurer configurer = getPathMatchConfigurer(); + if (configurer.isUseSuffixPatternMatch() != null) { + handlerMapping.setUseSuffixPatternMatch(configurer.isUseSuffixPatternMatch()); + } + if (configurer.isUseRegisteredSuffixPatternMatch() != null) { + handlerMapping.setUseRegisteredSuffixPatternMatch(configurer.isUseRegisteredSuffixPatternMatch()); + } + if (configurer.isUseTrailingSlashMatch() != null) { + handlerMapping.setUseTrailingSlashMatch(configurer.isUseTrailingSlashMatch()); + } + if (configurer.getPathMatcher() != null) { + handlerMapping.setPathMatcher(configurer.getPathMatcher()); + } + if (configurer.getUrlPathHelper() != null) { + handlerMapping.setUrlPathHelper(configurer.getUrlPathHelper()); + } + return handlerMapping; } @@ -259,6 +282,61 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv protected void configureContentNegotiation(ContentNegotiationConfigurer configurer) { } + /** + * Callback for building the {@link PathMatchConfigurer}. + * Delegates to {@link #configurePathMatch}. + * @since 3.2.17 + */ + protected PathMatchConfigurer getPathMatchConfigurer() { + if (this.pathMatchConfigurer == null) { + this.pathMatchConfigurer = new PathMatchConfigurer(); + configurePathMatch(this.pathMatchConfigurer); + } + return this.pathMatchConfigurer; + } + + /** + * Override this method to configure path matching options. + * @see PathMatchConfigurer + * @since 3.2.17 + */ + public void configurePathMatch(PathMatchConfigurer configurer) { + } + + /** + * Return a global {@link PathMatcher} instance for path matching + * patterns in {@link HandlerMapping}s. + * This instance can be configured using the {@link PathMatchConfigurer} + * in {@link #configurePathMatch(PathMatchConfigurer)}. + * @since 3.2.17 + */ + @Bean + public PathMatcher mvcPathMatcher() { + if (getPathMatchConfigurer().getPathMatcher() != null) { + return getPathMatchConfigurer().getPathMatcher(); + } + else { + return new AntPathMatcher(); + } + } + + /** + * Return a global {@link UrlPathHelper} instance for path matching + * patterns in {@link HandlerMapping}s. + * This instance can be configured using the {@link PathMatchConfigurer} + * in {@link #configurePathMatch(PathMatchConfigurer)}. + * @since 3.2.17 + */ + @Bean + public UrlPathHelper mvcUrlPathHelper() { + if (getPathMatchConfigurer().getUrlPathHelper() != null) { + return getPathMatchConfigurer().getUrlPathHelper(); + } + else { + return new UrlPathHelper(); + } + } + /** * Return a handler mapping ordered at 1 to map URL paths directly to * view names. To configure view controllers, override @@ -272,6 +350,8 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv AbstractHandlerMapping handlerMapping = registry.getHandlerMapping(); handlerMapping = (handlerMapping != null ? handlerMapping : new EmptyHandlerMapping()); handlerMapping.setInterceptors(getInterceptors()); + handlerMapping.setPathMatcher(mvcPathMatcher()); + handlerMapping.setUrlPathHelper(mvcUrlPathHelper()); return handlerMapping; } @@ -305,7 +385,13 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv addResourceHandlers(registry); AbstractHandlerMapping handlerMapping = registry.getHandlerMapping(); - handlerMapping = (handlerMapping != null ? handlerMapping : new EmptyHandlerMapping()); + if (handlerMapping != null) { + handlerMapping.setPathMatcher(mvcPathMatcher()); + handlerMapping.setUrlPathHelper(mvcUrlPathHelper()); + } + else { + handlerMapping = new EmptyHandlerMapping(); + } return handlerMapping; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurer.java index 215d34b7da..36d720f99e 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurer.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -79,6 +79,20 @@ public interface WebMvcConfigurer { */ void configureAsyncSupport(AsyncSupportConfigurer configurer); + + /** + * Helps with configuring HandlerMappings path matching options such as trailing slash match, + * suffix registration, path matcher and path helper. + * Configured path matcher and path helper instances are shared for: + *

+ * @since 3.2.17 + */ + void configurePathMatch(PathMatchConfigurer configurer); + /** * Add resolvers to support custom controller method argument types. *

This does not override the built-in support for resolving handler diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurerAdapter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurerAdapter.java index 6f6c82733c..e7fd2a88b9 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurerAdapter.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -71,6 +71,13 @@ public abstract class WebMvcConfigurerAdapter implements WebMvcConfigurer { public void configureAsyncSupport(AsyncSupportConfigurer configurer) { } + /** + * {@inheritDoc} + *

This implementation is empty. + */ + public void configurePathMatch(PathMatchConfigurer configurer) { + } + /** * {@inheritDoc} *

This implementation is empty. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurerComposite.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurerComposite.java index e222c3b126..2ec2a4effd 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurerComposite.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurerComposite.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -61,6 +61,12 @@ class WebMvcConfigurerComposite implements WebMvcConfigurer { } } + public void configurePathMatch(PathMatchConfigurer configurer) { + for (WebMvcConfigurer delegate : this.delegates) { + delegate.configurePathMatch(configurer); + } + } + public void configureMessageConverters(List> converters) { for (WebMvcConfigurer delegate : this.delegates) { delegate.configureMessageConverters(converters); diff --git a/spring-webmvc/src/main/resources/org/springframework/web/servlet/config/spring-mvc-3.2.xsd b/spring-webmvc/src/main/resources/org/springframework/web/servlet/config/spring-mvc-3.2.xsd index c5e0c248d9..2fab173ab6 100644 --- a/spring-webmvc/src/main/resources/org/springframework/web/servlet/config/spring-mvc-3.2.xsd +++ b/spring-webmvc/src/main/resources/org/springframework/web/servlet/config/spring-mvc-3.2.xsd @@ -21,6 +21,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fileExtensions = hm.getContentNegotiationManager().getAllFileExtensions(); + assertThat(fileExtensions, Matchers.contains("xml")); + assertThat(fileExtensions, Matchers.hasSize(1)); + } + @Test public void testMessageConverters() { loadBeanDefinitions("mvc-config-message-converters.xml"); @@ -198,3 +219,7 @@ class TestMessageCodesResolver implements MessageCodesResolver { } } + +class TestPathMatcher extends AntPathMatcher { } + +class TestPathHelper extends UrlPathHelper { } \ No newline at end of file diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java index 367ab9f700..a3d14b94bc 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,9 +20,11 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Method; import java.util.Arrays; +import java.util.Comparator; import java.util.Date; import java.util.List; import java.util.Locale; +import java.util.Map; import javax.servlet.RequestDispatcher; import javax.validation.constraints.NotNull; @@ -47,6 +49,7 @@ import org.springframework.mock.web.test.MockRequestDispatcher; import org.springframework.mock.web.test.MockServletContext; import org.springframework.scheduling.concurrent.ConcurrentTaskExecutor; import org.springframework.stereotype.Controller; +import org.springframework.util.PathMatcher; import org.springframework.validation.BindingResult; import org.springframework.validation.Errors; import org.springframework.validation.Validator; @@ -79,6 +82,7 @@ import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandl import org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler; import org.springframework.web.servlet.resource.ResourceHttpRequestHandler; import org.springframework.web.servlet.theme.ThemeChangeInterceptor; +import org.springframework.web.util.UrlPathHelper; import static org.junit.Assert.*; @@ -89,6 +93,10 @@ import static org.junit.Assert.*; */ public class MvcNamespaceTests { + private static final String VIEWCONTROLLER_BEAN_NAME = + "org.springframework.web.servlet.config.viewControllerHandlerMapping"; + + private GenericWebApplicationContext appContext; private TestController handler; @@ -234,7 +242,7 @@ public class MvcNamespaceTests { @Test public void testResources() throws Exception { - loadBeanDefinitions("mvc-config-resources.xml", 5); + loadBeanDefinitions("mvc-config-resources.xml", 7); HttpRequestHandlerAdapter adapter = appContext.getBean(HttpRequestHandlerAdapter.class); assertNotNull(adapter); @@ -267,7 +275,7 @@ public class MvcNamespaceTests { @Test public void testResourcesWithOptionalAttributes() throws Exception { - loadBeanDefinitions("mvc-config-resources-optional-attrs.xml", 5); + loadBeanDefinitions("mvc-config-resources-optional-attrs.xml", 7); SimpleUrlHandlerMapping mapping = appContext.getBean(SimpleUrlHandlerMapping.class); assertNotNull(mapping); @@ -349,7 +357,7 @@ public class MvcNamespaceTests { @Test public void testViewControllers() throws Exception { - loadBeanDefinitions("mvc-config-view-controllers.xml", 15); + loadBeanDefinitions("mvc-config-view-controllers.xml", 17); RequestMappingHandlerMapping mapping = appContext.getBean(RequestMappingHandlerMapping.class); assertNotNull(mapping); @@ -409,7 +417,7 @@ public class MvcNamespaceTests { /** WebSphere gives trailing servlet path slashes by default!! */ @Test public void testViewControllersOnWebSphere() throws Exception { - loadBeanDefinitions("mvc-config-view-controllers.xml", 15); + loadBeanDefinitions("mvc-config-view-controllers.xml", 17); SimpleUrlHandlerMapping mapping2 = appContext.getBean(SimpleUrlHandlerMapping.class); SimpleControllerHandlerAdapter adapter = appContext.getBean(SimpleControllerHandlerAdapter.class); @@ -453,7 +461,7 @@ public class MvcNamespaceTests { @Test public void testViewControllersDefaultConfig() { - loadBeanDefinitions("mvc-config-view-controllers-minimal.xml", 4); + loadBeanDefinitions("mvc-config-view-controllers-minimal.xml", 6); BeanNameUrlHandlerMapping beanNameMapping = appContext.getBean(BeanNameUrlHandlerMapping.class); assertNotNull(beanNameMapping); @@ -492,6 +500,27 @@ public class MvcNamespaceTests { assertEquals(1, deferredResultInterceptors.length); } + @Test + public void testPathMatchingHandlerMappings() throws Exception { + loadBeanDefinitions("mvc-config-path-matching-mappings.xml", 19); + + RequestMappingHandlerMapping requestMapping = appContext.getBean(RequestMappingHandlerMapping.class); + assertNotNull(requestMapping); + assertEquals(TestPathHelper.class, requestMapping.getUrlPathHelper().getClass()); + assertEquals(TestPathMatcher.class, requestMapping.getPathMatcher().getClass()); + + SimpleUrlHandlerMapping viewController = appContext.getBean(VIEWCONTROLLER_BEAN_NAME, SimpleUrlHandlerMapping.class); + assertNotNull(viewController); + assertEquals(TestPathHelper.class, viewController.getUrlPathHelper().getClass()); + assertEquals(TestPathMatcher.class, viewController.getPathMatcher().getClass()); + + for (SimpleUrlHandlerMapping handlerMapping : appContext.getBeansOfType(SimpleUrlHandlerMapping.class).values()) { + assertNotNull(handlerMapping); + assertEquals(TestPathHelper.class, handlerMapping.getUrlPathHelper().getClass()); + assertEquals(TestPathMatcher.class, handlerMapping.getPathMatcher().getClass()); + } + } + private void loadBeanDefinitions(String fileName, int expectedBeanCount) { XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(appContext); @@ -564,4 +593,45 @@ public class MvcNamespaceTests { public static class TestDeferredResultProcessingInterceptor extends DeferredResultProcessingInterceptorAdapter { } + public static class TestPathMatcher implements PathMatcher { + + @Override + public boolean isPattern(String path) { + return false; + } + + @Override + public boolean match(String pattern, String path) { + return path.matches(pattern); + } + + @Override + public boolean matchStart(String pattern, String path) { + return false; + } + + @Override + public String extractPathWithinPattern(String pattern, String path) { + return null; + } + + @Override + public Map extractUriTemplateVariables(String pattern, String path) { + return null; + } + + @Override + public Comparator getPatternComparator(String path) { + return null; + } + + @Override + public String combine(String pattern1, String pattern2) { + return null; + } + } + + public static class TestPathHelper extends UrlPathHelper { + } + } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfigurationTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfigurationTests.java index 392cb9b979..890b310de9 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfigurationTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,6 +30,7 @@ import org.springframework.core.convert.ConversionService; import org.springframework.format.support.FormattingConversionService; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.util.PathMatcher; import org.springframework.validation.DefaultMessageCodesResolver; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; @@ -40,7 +41,9 @@ import org.springframework.web.servlet.handler.HandlerExceptionResolverComposite import org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver; import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver; +import org.springframework.web.util.UrlPathHelper; import static org.junit.Assert.*; import static org.mockito.BDDMockito.*; @@ -178,4 +181,30 @@ public class DelegatingWebMvcConfigurationTests { assertEquals("Only one custom converter is expected", 1, composite.getExceptionResolvers().size()); } + @Test + public void configurePathMatch() throws Exception { + final PathMatcher pathMatcher = mock(PathMatcher.class); + final UrlPathHelper pathHelper = mock(UrlPathHelper.class); + + List configurers = new ArrayList(); + configurers.add(new WebMvcConfigurerAdapter() { + @Override + public void configurePathMatch(PathMatchConfigurer configurer) { + configurer.setUseRegisteredSuffixPatternMatch(true) + .setUseTrailingSlashMatch(false) + .setUrlPathHelper(pathHelper) + .setPathMatcher(pathMatcher); + } + }); + delegatingConfig.setConfigurers(configurers); + + RequestMappingHandlerMapping handlerMapping = delegatingConfig.requestMappingHandlerMapping(); + assertNotNull(handlerMapping); + assertTrue(handlerMapping.useRegisteredSuffixPatternMatch()); + assertTrue(handlerMapping.useSuffixPatternMatch()); + assertFalse(handlerMapping.useTrailingSlashMatch()); + assertSame(pathHelper, handlerMapping.getUrlPathHelper()); + assertSame(pathMatcher, handlerMapping.getPathMatcher()); + } + } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportExtensionTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportExtensionTests.java index 40f10ea7e6..e5ea4a1706 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportExtensionTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportExtensionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,6 +35,7 @@ import org.springframework.mock.web.test.MockHttpServletRequest; import org.springframework.mock.web.test.MockServletContext; import org.springframework.scheduling.concurrent.ConcurrentTaskExecutor; import org.springframework.stereotype.Controller; +import org.springframework.util.AntPathMatcher; import org.springframework.validation.BeanPropertyBindingResult; import org.springframework.validation.DefaultMessageCodesResolver; import org.springframework.validation.Errors; @@ -62,6 +63,7 @@ import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver; import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; +import org.springframework.web.util.UrlPathHelper; /** * A test fixture with a sub-class of {@link WebMvcConfigurationSupport} that @@ -92,6 +94,10 @@ public class WebMvcConfigurationSupportExtensionTests { RequestMappingHandlerMapping rmHandlerMapping = webConfig.requestMappingHandlerMapping(); rmHandlerMapping.setApplicationContext(webAppContext); rmHandlerMapping.afterPropertiesSet(); + + assertEquals(TestPathHelper.class, rmHandlerMapping.getUrlPathHelper().getClass()); + assertEquals(TestPathMatcher.class, rmHandlerMapping.getPathMatcher().getClass()); + HandlerExecutionChain chain = rmHandlerMapping.getHandler(new MockHttpServletRequest("GET", "/")); assertNotNull(chain.getInterceptors()); assertEquals(2, chain.getInterceptors().length); @@ -102,6 +108,8 @@ public class WebMvcConfigurationSupportExtensionTests { handlerMapping.setApplicationContext(webAppContext); assertNotNull(handlerMapping); assertEquals(1, handlerMapping.getOrder()); + assertEquals(TestPathHelper.class, handlerMapping.getUrlPathHelper().getClass()); + assertEquals(TestPathMatcher.class, handlerMapping.getPathMatcher().getClass()); HandlerExecutionChain handler = handlerMapping.getHandler(new MockHttpServletRequest("GET", "/path")); assertNotNull(handler.getHandler()); @@ -109,6 +117,8 @@ public class WebMvcConfigurationSupportExtensionTests { handlerMapping.setApplicationContext(webAppContext); assertNotNull(handlerMapping); assertEquals(Integer.MAX_VALUE-1, handlerMapping.getOrder()); + assertEquals(TestPathHelper.class, handlerMapping.getUrlPathHelper().getClass()); + assertEquals(TestPathMatcher.class, handlerMapping.getPathMatcher().getClass()); handler = handlerMapping.getHandler(new MockHttpServletRequest("GET", "/resources/foo.gif")); assertNotNull(handler.getHandler()); @@ -280,6 +290,12 @@ public class WebMvcConfigurationSupportExtensionTests { registry.addInterceptor(new LocaleChangeInterceptor()); } + @Override + public void configurePathMatch(PathMatchConfigurer configurer) { + configurer.setPathMatcher(new TestPathMatcher()); + configurer.setUrlPathHelper(new TestPathHelper()); + } + @SuppressWarnings("serial") @Override public MessageCodesResolver getMessageCodesResolver() { @@ -308,4 +324,8 @@ public class WebMvcConfigurationSupportExtensionTests { } + private class TestPathHelper extends UrlPathHelper {} + + private class TestPathMatcher extends AntPathMatcher {} + } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java index 68c4460b58..bf02c6318f 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,16 +27,23 @@ import javax.servlet.http.HttpServletRequest; import org.junit.Before; import org.junit.Test; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.core.convert.ConversionService; import org.springframework.format.support.FormattingConversionService; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.mock.web.test.MockHttpServletRequest; import org.springframework.mock.web.test.MockServletContext; import org.springframework.stereotype.Controller; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.PathMatcher; import org.springframework.validation.Validator; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; import org.springframework.web.context.support.StaticWebApplicationContext; import org.springframework.web.servlet.HandlerExceptionResolver; import org.springframework.web.servlet.HandlerExecutionChain; @@ -49,6 +56,7 @@ import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExc import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver; +import org.springframework.web.util.UrlPathHelper; /** * A test fixture with an {@link WebMvcConfigurationSupport} instance. @@ -163,6 +171,32 @@ public class WebMvcConfigurationSupportTests { assertNotNull(eher.getApplicationContext()); } + @Test + public void defaultPathMatchConfiguration() throws Exception { + AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); + context.setServletContext(new MockServletContext()); + context.register(WebConfig.class); + context.refresh(); + + UrlPathHelper urlPathHelper = context.getBean(UrlPathHelper.class); + PathMatcher pathMatcher = context.getBean(PathMatcher.class); + + assertNotNull(urlPathHelper); + assertNotNull(pathMatcher); + assertEquals(AntPathMatcher.class, pathMatcher.getClass()); + } + + + @EnableWebMvc + @Configuration + @SuppressWarnings("unused") + static class WebConfig { + + @Bean + public TestController testController() { + return new TestController(); + } + } @Controller private static class TestController { diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-path-matching-mappings.xml b/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-path-matching-mappings.xml new file mode 100644 index 0000000000..875672b2f4 --- /dev/null +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-path-matching-mappings.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-path-matching.xml b/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-path-matching.xml new file mode 100644 index 0000000000..8b27fa26f5 --- /dev/null +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-path-matching.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + xml=application/rss+xml + + + + \ No newline at end of file