From 2201dd8c45051230ad1a5a0e895cb5951edbfb74 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Sun, 26 Aug 2012 16:22:37 -0400 Subject: [PATCH] Add support for matrix variables A new @MatrixVariable annotation allows injecting matrix variables into @RequestMapping methods. The matrix variables may appear in any path segment and should be wrapped in a URI template for request mapping purposes to ensure request matching is not affected by the order or the presence/absence of such variables. The @MatrixVariable annotation has an optional "pathVar" attribute that can be used to refer to the URI template where a matrix variable is located. Previously, ";" (semicolon) delimited content was removed from the path used for request mapping purposes. To preserve backwards compatibility that continues to be the case (except for the MVC namespace and Java config) and may be changed by setting the "removeSemicolonContent" property of RequestMappingHandlerMapping to "false". Applications using the MVC namespace and Java config do not need to do anything further to extract and use matrix variables. Issue: SPR-5499, SPR-7818 --- .../web/bind/annotation/MatrixVariable.java | 66 ++++++ .../web/bind/annotation/RequestMapping.java | 7 + ...tractNamedValueMethodArgumentResolver.java | 19 +- .../web/util/UrlPathHelper.java | 68 +++++- .../springframework/web/util/WebUtils.java | 35 +++ .../web/util/UrlPathHelperTests.java | 33 ++- .../web/util/WebUtilsTests.java | 33 +++ .../web/servlet/HandlerMapping.java | 19 +- .../AnnotationDrivenBeanDefinitionParser.java | 1 + .../WebMvcConfigurationSupport.java | 1 + .../handler/AbstractHandlerMapping.java | 31 ++- .../mvc/AbstractUrlViewController.java | 10 +- .../RequestMappingInfoHandlerMapping.java | 48 ++++- .../CopyOfRequestMappingHandlerMapping.java | 199 ++++++++++++++++++ ...trixVariableMapMethodArgumentResolver.java | 88 ++++++++ .../MatrixVariableMethodArgumentResolver.java | 129 ++++++++++++ .../PathVariableMethodArgumentResolver.java | 8 +- .../RequestMappingHandlerAdapter.java | 5 + .../DefaultRequestToViewNameTranslator.java | 8 + .../handler/MappedInterceptorTests.java | 10 +- .../mvc/UrlFilenameViewControllerTests.java | 9 + ...RequestMappingInfoHandlerMappingTests.java | 111 ++++++++-- .../AbstractServletHandlerMethodTests.java | 36 ++-- ...riablesMapMethodArgumentResolverTests.java | 186 ++++++++++++++++ ...xVariablesMethodArgumentResolverTests.java | 159 ++++++++++++++ ...nnotationControllerHandlerMethodTests.java | 82 +++++--- ...faultRequestToViewNameTranslatorTests.java | 6 + src/reference/docbook/mvc.xml | 93 ++++++++ src/reference/docbook/new-in-3.2.xml | 8 + 29 files changed, 1392 insertions(+), 116 deletions(-) create mode 100644 spring-web/src/main/java/org/springframework/web/bind/annotation/MatrixVariable.java create mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/CopyOfRequestMappingHandlerMapping.java create mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariableMapMethodArgumentResolver.java create mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariableMethodArgumentResolver.java create mode 100644 spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariablesMapMethodArgumentResolverTests.java create mode 100644 spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariablesMethodArgumentResolverTests.java diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/MatrixVariable.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/MatrixVariable.java new file mode 100644 index 0000000000..22d5f36ccb --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/MatrixVariable.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2010 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.bind.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation which indicates that a method parameter should be bound to a + * name-value pair within a path segment. Supported for {@link RequestMapping} + * annotated handler methods in Servlet environments. + * + * @author Rossen Stoyanchev + * @since 3.2 + */ +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface MatrixVariable { + + /** + * The name of the matrix variable. + */ + String value() default ""; + + /** + * The name of the URI path variable where the matrix variable is located, + * if necessary for disambiguation (e.g. a matrix variable with the same + * name present in more than one path segment). + */ + String pathVar() default ValueConstants.DEFAULT_NONE; + + /** + * Whether the matrix variable is required. + *

Default is true, leading to an exception thrown in case + * of the variable missing in the request. Switch this to false + * if you prefer a null in case of the variable missing. + *

Alternatively, provide a {@link #defaultValue() defaultValue}, + * which implicitly sets this flag to false. + */ + boolean required() default true; + + /** + * The default value to use as a fallback. Supplying a default value implicitly + * sets {@link #required()} to false. + */ + String defaultValue() default ValueConstants.DEFAULT_NONE; + +} diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java index 9af71225ce..511aecf8bc 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java @@ -82,6 +82,13 @@ import java.util.concurrent.Callable; * Additionally, {@code @PathVariable} can be used on a * {@link java.util.Map Map<String, String>} to gain access to all * URI template variables. + *

  • {@link MatrixVariable @MatrixVariable} annotated parameters (Servlet-only) + * for access to name-value pairs located in URI path segments. Matrix variables + * must be represented with a URI template variable. For example /hotels/{hotel} + * where the incoming URL may be "/hotels/42;q=1". + * Additionally, {@code @MatrixVariable} can be used on a + * {@link java.util.Map Map<String, String>} to gain access to all + * matrix variables in the URL or to those in a specific path variable. *
  • {@link RequestParam @RequestParam} annotated parameters for access to * specific Servlet/Portlet request parameters. Parameter values will be * converted to the declared method argument type. Additionally, diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/AbstractNamedValueMethodArgumentResolver.java b/spring-web/src/main/java/org/springframework/web/method/annotation/AbstractNamedValueMethodArgumentResolver.java index b92133765e..63e318125f 100644 --- a/spring-web/src/main/java/org/springframework/web/method/annotation/AbstractNamedValueMethodArgumentResolver.java +++ b/spring-web/src/main/java/org/springframework/web/method/annotation/AbstractNamedValueMethodArgumentResolver.java @@ -35,8 +35,9 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; /** - * Abstract base class for resolving method arguments from a named value. Request parameters, request headers, and - * path variables are examples of named values. Each may have a name, a required flag, and a default value. + * Abstract base class for resolving method arguments from a named value. + * Request parameters, request headers, and path variables are examples of named + * values. Each may have a name, a required flag, and a default value. *

    Subclasses define how to do the following: *

      *
    • Obtain named value information for a method parameter @@ -44,10 +45,11 @@ import org.springframework.web.method.support.ModelAndViewContainer; *
    • Handle missing argument values when argument values are required *
    • Optionally handle a resolved value *
    - *

    A default value string can contain ${...} placeholders and Spring Expression Language #{...} expressions. - * For this to work a {@link ConfigurableBeanFactory} must be supplied to the class constructor. - *

    A {@link WebDataBinder} is created to apply type conversion to the resolved argument value if it doesn't - * match the method parameter type. + *

    A default value string can contain ${...} placeholders and Spring Expression + * Language #{...} expressions. For this to work a + * {@link ConfigurableBeanFactory} must be supplied to the class constructor. + *

    A {@link WebDataBinder} is created to apply type conversion to the resolved + * argument value if it doesn't match the method parameter type. * * @author Arjen Poutsma * @author Rossen Stoyanchev @@ -63,8 +65,9 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle new ConcurrentHashMap(); /** - * @param beanFactory a bean factory to use for resolving ${...} placeholder and #{...} SpEL expressions - * in default values, or {@code null} if default values are not expected to contain expressions + * @param beanFactory a bean factory to use for resolving ${...} placeholder + * and #{...} SpEL expressions in default values, or {@code null} if default + * values are not expected to contain expressions */ public AbstractNamedValueMethodArgumentResolver(ConfigurableBeanFactory beanFactory) { this.configurableBeanFactory = beanFactory; diff --git a/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java b/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java index 8b13486961..e5ca94a4ac 100644 --- a/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java +++ b/spring-web/src/main/java/org/springframework/web/util/UrlPathHelper.java @@ -61,6 +61,8 @@ public class UrlPathHelper { private boolean urlDecode = true; + private boolean removeSemicolonContent = true; + private String defaultEncoding = WebUtils.DEFAULT_CHARACTER_ENCODING; @@ -92,6 +94,21 @@ public class UrlPathHelper { this.urlDecode = urlDecode; } + /** + * Set if ";" (semicolon) content should be stripped from the request URI. + *

    Default is "true". + */ + public void setRemoveSemicolonContent(boolean removeSemicolonContent) { + this.removeSemicolonContent = removeSemicolonContent; + } + + /** + * Whether configured to remove ";" (semicolon) content from the request URI. + */ + public boolean shouldRemoveSemicolonContent() { + return this.removeSemicolonContent; + } + /** * Set the default character encoding to use for URL decoding. * Default is ISO-8859-1, according to the Servlet spec. @@ -318,9 +335,9 @@ public class UrlPathHelper { * Decode the supplied URI string and strips any extraneous portion after a ';'. */ private String decodeAndCleanUriString(HttpServletRequest request, String uri) { + uri = removeSemicolonContent(uri); uri = decodeRequestString(request, uri); - int semicolonIndex = uri.indexOf(';'); - return (semicolonIndex != -1 ? uri.substring(0, semicolonIndex) : uri); + return uri; } /** @@ -375,10 +392,49 @@ public class UrlPathHelper { } /** - * Decode the given URI path variables via {@link #decodeRequestString(HttpServletRequest, String)} - * unless {@link #setUrlDecode(boolean)} is set to {@code true} in which case - * it is assumed the URL path from which the variables were extracted is - * already decoded through a call to {@link #getLookupPathForRequest(HttpServletRequest)}. + * Remove ";" (semicolon) content from the given request URI if the + * {@linkplain #setRemoveSemicolonContent(boolean) removeSemicolonContent} + * property is set to "true". Note that "jssessionid" is always removed. + * + * @param requestUri the request URI string to remove ";" content from + * @return the updated URI string + */ + public String removeSemicolonContent(String requestUri) { + if (this.removeSemicolonContent) { + return removeSemicolonContentInternal(requestUri); + } + return removeJsessionid(requestUri); + } + + private String removeSemicolonContentInternal(String requestUri) { + int semicolonIndex = requestUri.indexOf(';'); + while (semicolonIndex != -1) { + int slashIndex = requestUri.indexOf('/', semicolonIndex); + String start = requestUri.substring(0, semicolonIndex); + requestUri = (slashIndex != -1) ? start + requestUri.substring(slashIndex) : start; + semicolonIndex = requestUri.indexOf(';', semicolonIndex); + } + return requestUri; + } + + private String removeJsessionid(String requestUri) { + int startIndex = requestUri.indexOf(";jsessionid="); + if (startIndex != -1) { + int endIndex = requestUri.indexOf(';', startIndex + 12); + String start = requestUri.substring(0, startIndex); + requestUri = (endIndex != -1) ? start + requestUri.substring(endIndex) : start; + } + return requestUri; + } + + /** + * Decode the given URI path variables via + * {@link #decodeRequestString(HttpServletRequest, String)} unless + * {@link #setUrlDecode(boolean)} is set to {@code true} in which case it is + * assumed the URL path from which the variables were extracted is already + * decoded through a call to + * {@link #getLookupPathForRequest(HttpServletRequest)}. + * * @param request current HTTP request * @param vars URI variables extracted from the URL path * @return the same Map or a new Map instance diff --git a/spring-web/src/main/java/org/springframework/web/util/WebUtils.java b/spring-web/src/main/java/org/springframework/web/util/WebUtils.java index fa736be6a7..4c2b036a6c 100644 --- a/spring-web/src/main/java/org/springframework/web/util/WebUtils.java +++ b/spring-web/src/main/java/org/springframework/web/util/WebUtils.java @@ -20,6 +20,7 @@ import java.io.File; import java.io.FileNotFoundException; import java.util.Enumeration; import java.util.Map; +import java.util.StringTokenizer; import java.util.TreeMap; import javax.servlet.ServletContext; @@ -33,6 +34,8 @@ import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.springframework.util.Assert; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; /** @@ -706,6 +709,7 @@ public abstract class WebUtils { } return filename; } + /** * Extract the full URL filename (including file extension) from the given request URL path. * Correctly resolves nested paths such as "/products/view.html" as well. @@ -724,4 +728,35 @@ public abstract class WebUtils { return urlPath.substring(begin, end); } + /** + * Parse the given string with matrix variables. An example string would look + * like this {@code "q1=a;q1=b;q2=a,b,c"}. The resulting map would contain + * keys {@code "q1"} and {@code "q2"} with values {@code ["a","b"]} and + * {@code ["a","b","c"]} respectively. + * + * @param matrixVariables the unparsed matrix variables string + * @return a map with matrix variable names and values, never {@code null} + */ + public static MultiValueMap parseMatrixVariables(String matrixVariables) { + MultiValueMap result = new LinkedMultiValueMap(); + if (!StringUtils.hasText(matrixVariables)) { + return result; + } + StringTokenizer pairs = new StringTokenizer(matrixVariables, ";"); + while (pairs.hasMoreTokens()) { + String pair = pairs.nextToken(); + int index = pair.indexOf('='); + if (index != -1) { + String name = pair.substring(0, index); + String rawValue = pair.substring(index + 1); + for (String value : StringUtils.commaDelimitedListToStringArray(rawValue)) { + result.add(name, value); + } + } + else { + result.add(pair, ""); + } + } + return result; + } } diff --git a/spring-web/src/test/java/org/springframework/web/util/UrlPathHelperTests.java b/spring-web/src/test/java/org/springframework/web/util/UrlPathHelperTests.java index f35af797b3..4ba2a16aa9 100644 --- a/spring-web/src/test/java/org/springframework/web/util/UrlPathHelperTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/UrlPathHelperTests.java @@ -16,11 +16,14 @@ package org.springframework.web.util; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import java.io.UnsupportedEncodingException; + import org.junit.Before; import org.junit.Ignore; import org.junit.Test; - import org.springframework.mock.web.MockHttpServletRequest; /** @@ -79,10 +82,32 @@ public class UrlPathHelperTests { } + @Test + public void getRequestRemoveSemicolonContent() throws UnsupportedEncodingException { + helper.setRemoveSemicolonContent(true); + + request.setRequestURI("/foo;f=F;o=O;o=O/bar;b=B;a=A;r=R"); + assertEquals("/foo/bar", helper.getRequestUri(request)); + } + + @Test + public void getRequestKeepSemicolonContent() throws UnsupportedEncodingException { + helper.setRemoveSemicolonContent(false); + + request.setRequestURI("/foo;a=b;c=d"); + assertEquals("/foo;a=b;c=d", helper.getRequestUri(request)); + + request.setRequestURI("/foo;jsessionid=c0o7fszeb1"); + assertEquals("jsessionid should always be removed", "/foo", helper.getRequestUri(request)); + + request.setRequestURI("/foo;a=b;jsessionid=c0o7fszeb1;c=d"); + assertEquals("jsessionid should always be removed", "/foo;a=b;c=d", helper.getRequestUri(request)); + } + // // suite of tests root requests for default servlets (SRV 11.2) on Websphere vs Tomcat and other containers // see: http://jira.springframework.org/browse/SPR-7064 - // + // // @@ -297,7 +322,7 @@ public class UrlPathHelperTests { request.setAttribute(WebUtils.FORWARD_QUERY_STRING_ATTRIBUTE, "original=on"); assertEquals("original=on", this.helper.getOriginatingQueryString(request)); } - + @Test public void getOriginatingQueryStringNotPresent() { request.setQueryString("forward=true"); diff --git a/spring-web/src/test/java/org/springframework/web/util/WebUtilsTests.java b/spring-web/src/test/java/org/springframework/web/util/WebUtilsTests.java index 616dc66686..437e1db604 100644 --- a/spring-web/src/test/java/org/springframework/web/util/WebUtilsTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/WebUtilsTests.java @@ -16,16 +16,19 @@ package org.springframework.web.util; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import org.junit.Test; +import org.springframework.util.MultiValueMap; /** * @author Juergen Hoeller * @author Arjen Poutsma + * @author Rossen Stoyanchev */ public class WebUtilsTests { @@ -64,4 +67,34 @@ public class WebUtilsTests { assertEquals("view.html", WebUtils.extractFullFilenameFromUrlPath("/products/view.html?param=/path/a.do")); } + @Test + public void parseMatrixVariablesString() { + MultiValueMap variables; + + variables = WebUtils.parseMatrixVariables(null); + assertEquals(0, variables.size()); + + variables = WebUtils.parseMatrixVariables("year"); + assertEquals(1, variables.size()); + assertEquals("", variables.getFirst("year")); + + variables = WebUtils.parseMatrixVariables("year=2012"); + assertEquals(1, variables.size()); + assertEquals("2012", variables.getFirst("year")); + + variables = WebUtils.parseMatrixVariables("year=2012;colors=red,blue,green"); + assertEquals(2, variables.size()); + assertEquals(Arrays.asList("red", "blue", "green"), variables.get("colors")); + assertEquals("2012", variables.getFirst("year")); + + variables = WebUtils.parseMatrixVariables(";year=2012;colors=red,blue,green;"); + assertEquals(2, variables.size()); + assertEquals(Arrays.asList("red", "blue", "green"), variables.get("colors")); + assertEquals("2012", variables.getFirst("year")); + + variables = WebUtils.parseMatrixVariables("colors=red;colors=blue;colors=green"); + assertEquals(1, variables.size()); + assertEquals(Arrays.asList("red", "blue", "green"), variables.get("colors")); + } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerMapping.java index 8c381b6c46..684f54a71a 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerMapping.java @@ -93,10 +93,21 @@ public interface HandlerMapping { String URI_TEMPLATE_VARIABLES_ATTRIBUTE = HandlerMapping.class.getName() + ".uriTemplateVariables"; /** - * Name of the {@link HttpServletRequest} attribute that contains the set of producible MediaTypes - * applicable to the mapped handler. - *

    Note: This attribute is not required to be supported by all HandlerMapping implementations. - * Handlers should not necessarily expect this request attribute to be present in all scenarios. + * Name of the {@link HttpServletRequest} attribute that contains a map with + * URI matrix variables. + *

    Note: This attribute is not required to be supported by all + * HandlerMapping implementations and may also not be present depending on + * whether the HandlerMapping is configured to keep matrix variable content + * in the request URI. + */ + String MATRIX_VARIABLES_ATTRIBUTE = HandlerMapping.class.getName() + ".matrixVariables"; + + /** + * Name of the {@link HttpServletRequest} attribute that contains the set of + * producible MediaTypes applicable to the mapped handler. + *

    Note: This attribute is not required to be supported by all + * HandlerMapping implementations. Handlers should not necessarily expect + * this request attribute to be present in all scenarios. */ String PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE = HandlerMapping.class.getName() + ".producibleMediaTypes"; 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 aed8f2a201..ba799a7b4b 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 @@ -157,6 +157,7 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { handlerMappingDef.setSource(source); handlerMappingDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); handlerMappingDef.getPropertyValues().add("order", 0); + handlerMappingDef.getPropertyValues().add("removeSemicolonContent", false); handlerMappingDef.getPropertyValues().add("contentNegotiationManager", contentNegotiationManager); String methodMappingName = parserContext.getReaderContext().registerWithGeneratedName(handlerMappingDef); 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 b9f3539613..bf770af17c 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 @@ -183,6 +183,7 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv public RequestMappingHandlerMapping requestMappingHandlerMapping() { RequestMappingHandlerMapping handlerMapping = new RequestMappingHandlerMapping(); handlerMapping.setOrder(0); + handlerMapping.setRemoveSemicolonContent(false); handlerMapping.setInterceptors(getInterceptors()); handlerMapping.setContentNegotiationManager(mvcContentNegotiationManager()); return handlerMapping; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java index 0d4b303bdd..a0376ae06f 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java @@ -37,7 +37,7 @@ import org.springframework.web.util.UrlPathHelper; /** * Abstract base class for {@link org.springframework.web.servlet.HandlerMapping} - * implementations. Supports ordering, a default handler, handler interceptors, + * implementations. Supports ordering, a default handler, handler interceptors, * including handler interceptors mapped by path patterns. * *

    Note: This base class does not support exposure of the @@ -72,6 +72,7 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport private final List mappedInterceptors = new ArrayList(); + /** * Specify the order value for this HandlerMapping bean. *

    Default value is Integer.MAX_VALUE, meaning that it's non-ordered. @@ -124,6 +125,14 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport this.urlPathHelper.setUrlDecode(urlDecode); } + /** + * Set if ";" (semicolon) content should be stripped from the request URI. + * @see org.springframework.web.util.UrlPathHelper#setRemoveSemicolonContent(boolean) + */ + public void setRemoveSemicolonContent(boolean removeSemicolonContent) { + this.urlPathHelper.setRemoveSemicolonContent(removeSemicolonContent); + } + /** * Set the UrlPathHelper to use for resolution of lookup paths. *

    Use this to override the default UrlPathHelper with a custom subclass, @@ -162,7 +171,7 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport /** * Set the interceptors to apply for all handlers mapped by this handler mapping. - *

    Supported interceptor types are HandlerInterceptor, WebRequestInterceptor, and MappedInterceptor. + *

    Supported interceptor types are HandlerInterceptor, WebRequestInterceptor, and MappedInterceptor. * Mapped interceptors apply only to request URLs that match its path patterns. * Mapped interceptor beans are also detected by type during initialization. * @param interceptors array of handler interceptors, or null if none @@ -203,8 +212,8 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport /** * Detects beans of type {@link MappedInterceptor} and adds them to the list of mapped interceptors. * This is done in addition to any {@link MappedInterceptor}s that may have been provided via - * {@link #setInterceptors(Object[])}. Subclasses can override this method to change that. - * + * {@link #setInterceptors(Object[])}. Subclasses can override this method to change that. + * * @param mappedInterceptors an empty list to add MappedInterceptor types to */ protected void detectMappedInterceptors(List mappedInterceptors) { @@ -214,7 +223,7 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport } /** - * Initialize the specified interceptors, checking for {@link MappedInterceptor}s and adapting + * Initialize the specified interceptors, checking for {@link MappedInterceptor}s and adapting * HandlerInterceptors where necessary. * @see #setInterceptors * @see #adaptInterceptor @@ -235,7 +244,7 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport } } } - + /** * Adapt the given interceptor object to the HandlerInterceptor interface. *

    Supported interceptor types are HandlerInterceptor and WebRequestInterceptor. @@ -276,7 +285,7 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport int count = mappedInterceptors.size(); return (count > 0) ? mappedInterceptors.toArray(new MappedInterceptor[count]) : null; } - + /** * Look up a handler for the given request, falling back to the default * handler if no specific one is found. @@ -330,19 +339,19 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport * @see #getAdaptedInterceptors() */ protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) { - HandlerExecutionChain chain = + HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain) ? (HandlerExecutionChain) handler : new HandlerExecutionChain(handler); - + chain.addInterceptors(getAdaptedInterceptors()); - + String lookupPath = urlPathHelper.getLookupPathForRequest(request); for (MappedInterceptor mappedInterceptor : mappedInterceptors) { if (mappedInterceptor.matches(lookupPath, pathMatcher)) { chain.addInterceptor(mappedInterceptor.getInterceptor()); } } - + return chain; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/AbstractUrlViewController.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/AbstractUrlViewController.java index 2e7056342d..915ba6da70 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/AbstractUrlViewController.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/AbstractUrlViewController.java @@ -65,6 +65,14 @@ public abstract class AbstractUrlViewController extends AbstractController { this.urlPathHelper.setUrlDecode(urlDecode); } + /** + * Set if ";" (semicolon) content should be stripped from the request URI. + * @see org.springframework.web.util.UrlPathHelper#setRemoveSemicolonContent(boolean) + */ + public void setRemoveSemicolonContent(boolean removeSemicolonContent) { + this.urlPathHelper.setRemoveSemicolonContent(removeSemicolonContent); + } + /** * Set the UrlPathHelper to use for the resolution of lookup paths. *

    Use this to override the default UrlPathHelper with a custom subclass, @@ -87,7 +95,7 @@ public abstract class AbstractUrlViewController extends AbstractController { /** * Retrieves the URL path to use for lookup and delegates to - * {@link #getViewNameForRequest}. Also adds the content of + * {@link #getViewNameForRequest}. Also adds the content of * {@link RequestContextUtils#getInputFlashMap} to the model. */ @Override diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMapping.java index b27f49c437..e529f6101f 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMapping.java @@ -19,13 +19,16 @@ package org.springframework.web.servlet.mvc.method; import java.util.ArrayList; import java.util.Comparator; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import org.springframework.http.MediaType; +import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; import org.springframework.web.HttpMediaTypeNotAcceptableException; import org.springframework.web.HttpMediaTypeNotSupportedException; @@ -34,6 +37,7 @@ import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.handler.AbstractHandlerMethodMapping; +import org.springframework.web.util.WebUtils; /** * Abstract base class for classes for which {@link RequestMappingInfo} defines @@ -77,8 +81,10 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe } /** - * Expose URI template variables and producible media types in the request. + * Expose URI template variables, matrix variables, and producible media types in the request. + * * @see HandlerMapping#URI_TEMPLATE_VARIABLES_ATTRIBUTE + * @see HandlerMapping#MATRIX_VARIABLES_ATTRIBUTE * @see HandlerMapping#PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE */ @Override @@ -89,9 +95,13 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe String bestPattern = patterns.isEmpty() ? lookupPath : patterns.iterator().next(); request.setAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE, bestPattern); - Map vars = getPathMatcher().extractUriTemplateVariables(bestPattern, lookupPath); - Map decodedVars = getUrlPathHelper().decodePathVariables(request, vars); - request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, decodedVars); + Map uriVariables = getPathMatcher().extractUriTemplateVariables(bestPattern, lookupPath); + Map decodedUriVariables = getUrlPathHelper().decodePathVariables(request, uriVariables); + request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, decodedUriVariables); + + if (isMatrixVariableContentAvailable()) { + request.setAttribute(HandlerMapping.MATRIX_VARIABLES_ATTRIBUTE, extractMatrixVariables(uriVariables)); + } if (!info.getProducesCondition().getProducibleMediaTypes().isEmpty()) { Set mediaTypes = info.getProducesCondition().getProducibleMediaTypes(); @@ -99,6 +109,36 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe } } + private boolean isMatrixVariableContentAvailable() { + return !getUrlPathHelper().shouldRemoveSemicolonContent(); + } + + private Map> extractMatrixVariables(Map uriVariables) { + Map> result = new LinkedHashMap>(); + for (Entry uriVar : uriVariables.entrySet()) { + String uriVarValue = uriVar.getValue(); + + int equalsIndex = uriVarValue.indexOf('='); + if (equalsIndex == -1) { + continue; + } + + String matrixVariables; + + int semicolonIndex = uriVarValue.indexOf(';'); + if ((semicolonIndex == -1) || (semicolonIndex == 0) || (equalsIndex < semicolonIndex)) { + matrixVariables = uriVarValue; + } + else { + matrixVariables = uriVarValue.substring(semicolonIndex + 1); + uriVariables.put(uriVar.getKey(), uriVarValue.substring(0, semicolonIndex)); + } + + result.put(uriVar.getKey(), WebUtils.parseMatrixVariables(matrixVariables)); + } + return result; + } + /** * Iterate all RequestMappingInfos once again, look if any match by URL at * least and raise exceptions accordingly. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/CopyOfRequestMappingHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/CopyOfRequestMappingHandlerMapping.java new file mode 100644 index 0000000000..6a7333a62e --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/CopyOfRequestMappingHandlerMapping.java @@ -0,0 +1,199 @@ +/* + * Copyright 2002-2012 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.mvc.method.annotation; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.stereotype.Controller; +import org.springframework.util.Assert; +import org.springframework.web.accept.ContentNegotiationManager; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.servlet.mvc.condition.AbstractRequestCondition; +import org.springframework.web.servlet.mvc.condition.CompositeRequestCondition; +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; + +/** + * Creates {@link RequestMappingInfo} instances from type and method-level + * {@link RequestMapping @RequestMapping} annotations in + * {@link Controller @Controller} classes. + * + * @author Arjen Poutsma + * @author Rossen Stoyanchev + * @since 3.1 + */ +public class CopyOfRequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping { + + private boolean useSuffixPatternMatch = true; + + private boolean useTrailingSlashMatch = true; + + private ContentNegotiationManager contentNegotiationManager = new ContentNegotiationManager(); + + private final List contentNegotiationFileExtensions = new ArrayList(); + + /** + * Whether to use suffix pattern match (".*") when matching patterns to + * requests. If enabled a method mapped to "/users" also matches to "/users.*". + *

    The default value is {@code true}. + */ + public void setUseSuffixPatternMatch(boolean useSuffixPatternMatch) { + this.useSuffixPatternMatch = useSuffixPatternMatch; + } + + /** + * 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 void setUseTrailingSlashMatch(boolean useTrailingSlashMatch) { + this.useTrailingSlashMatch = useTrailingSlashMatch; + } + + /** + * Set the {@link ContentNegotiationManager} to use to determine requested media types. + * If not set, the default constructor is used. + */ + public void setContentNegotiationManager(ContentNegotiationManager contentNegotiationManager) { + Assert.notNull(contentNegotiationManager); + this.contentNegotiationManager = contentNegotiationManager; + this.contentNegotiationFileExtensions.addAll(contentNegotiationManager.getAllFileExtensions()); + } + + /** + * Whether to use suffix pattern matching. + */ + public boolean useSuffixPatternMatch() { + return this.useSuffixPatternMatch; + } + /** + * Whether to match to URLs irrespective of the presence of a trailing slash. + */ + public boolean useTrailingSlashMatch() { + return this.useTrailingSlashMatch; + } + + /** + * Return the configured {@link ContentNegotiationManager}. + */ + public ContentNegotiationManager getContentNegotiationManager() { + return this.contentNegotiationManager; + } + + /** + * Return the known file extensions for content negotiation. + */ + public List getContentNegotiationFileExtensions() { + return this.contentNegotiationFileExtensions; + } + + /** + * {@inheritDoc} + * Expects a handler to have a type-level @{@link Controller} annotation. + */ + @Override + protected boolean isHandler(Class beanType) { + return ((AnnotationUtils.findAnnotation(beanType, Controller.class) != null) || + (AnnotationUtils.findAnnotation(beanType, RequestMapping.class) != null)); + } + + /** + * Uses method and type-level @{@link RequestMapping} annotations to create + * the RequestMappingInfo. + * + * @return the created RequestMappingInfo, or {@code null} if the method + * does not have a {@code @RequestMapping} annotation. + * + * @see #getCustomMethodCondition(Method) + * @see #getCustomTypeCondition(Class) + */ + @Override + protected RequestMappingInfo getMappingForMethod(Method method, Class handlerType) { + RequestMappingInfo info = null; + RequestMapping methodAnnotation = AnnotationUtils.findAnnotation(method, RequestMapping.class); + if (methodAnnotation != null) { + RequestCondition methodCondition = getCustomMethodCondition(method); + info = createRequestMappingInfo(methodAnnotation, methodCondition); + RequestMapping typeAnnotation = AnnotationUtils.findAnnotation(handlerType, RequestMapping.class); + if (typeAnnotation != null) { + RequestCondition typeCondition = getCustomTypeCondition(handlerType); + info = createRequestMappingInfo(typeAnnotation, typeCondition).combine(info); + } + } + return info; + } + + /** + * Provide a custom type-level request condition. + * The custom {@link RequestCondition} can be of any type so long as the + * same condition type is returned from all calls to this method in order + * to ensure custom request conditions can be combined and compared. + * + *

    Consider extending {@link AbstractRequestCondition} for custom + * condition types and using {@link CompositeRequestCondition} to provide + * multiple custom conditions. + * + * @param handlerType the handler type for which to create the condition + * @return the condition, or {@code null} + */ + protected RequestCondition getCustomTypeCondition(Class handlerType) { + return null; + } + + /** + * Provide a custom method-level request condition. + * The custom {@link RequestCondition} can be of any type so long as the + * same condition type is returned from all calls to this method in order + * to ensure custom request conditions can be combined and compared. + * + *

    Consider extending {@link AbstractRequestCondition} for custom + * condition types and using {@link CompositeRequestCondition} to provide + * multiple custom conditions. + * + * @param method the handler method for which to create the condition + * @return the condition, or {@code null} + */ + protected RequestCondition getCustomMethodCondition(Method method) { + return null; + } + + /** + * Created a RequestMappingInfo from a RequestMapping annotation. + */ + private RequestMappingInfo createRequestMappingInfo(RequestMapping annotation, RequestCondition customCondition) { + return new RequestMappingInfo( + new PatternsRequestCondition(annotation.value(), getUrlPathHelper(), getPathMatcher(), + this.useSuffixPatternMatch, this.useTrailingSlashMatch, this.contentNegotiationFileExtensions), + new RequestMethodsRequestCondition(annotation.method()), + new ParamsRequestCondition(annotation.params()), + new HeadersRequestCondition(annotation.headers()), + new ConsumesRequestCondition(annotation.consumes(), annotation.headers()), + new ProducesRequestCondition(annotation.produces(), annotation.headers(), getContentNegotiationManager()), + customCondition); + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariableMapMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariableMapMethodArgumentResolver.java new file mode 100644 index 0000000000..9a5160ade9 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariableMapMethodArgumentResolver.java @@ -0,0 +1,88 @@ +/* + * Copyright 2002-2012 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.mvc.method.annotation; + +import java.util.Collections; +import java.util.Map; + +import org.springframework.core.MethodParameter; +import org.springframework.util.CollectionUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.MatrixVariable; +import org.springframework.web.bind.annotation.ValueConstants; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; +import org.springframework.web.servlet.HandlerMapping; + +/** + * Resolves method arguments of type Map annotated with + * {@link MatrixVariable @MatrixVariable} where the annotation the does not + * specify a name. If a name specified then the argument will by resolved by the + * {@link MatrixVariableMethodArgumentResolver} instead. + * + * @author Rossen Stoyanchev + * @since 3.2 + */ +public class MatrixVariableMapMethodArgumentResolver implements HandlerMethodArgumentResolver { + + public boolean supportsParameter(MethodParameter parameter) { + MatrixVariable paramAnnot = parameter.getParameterAnnotation(MatrixVariable.class); + if (paramAnnot != null) { + if (Map.class.isAssignableFrom(parameter.getParameterType())) { + return !StringUtils.hasText(paramAnnot.value()); + } + } + return false; + } + + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest request, WebDataBinderFactory binderFactory) throws Exception { + + @SuppressWarnings("unchecked") + Map> matrixVariables = + (Map>) request.getAttribute( + HandlerMapping.MATRIX_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST); + + if (CollectionUtils.isEmpty(matrixVariables)) { + return Collections.emptyMap(); + } + + String pathVariable = parameter.getParameterAnnotation(MatrixVariable.class).pathVar(); + + if (!pathVariable.equals(ValueConstants.DEFAULT_NONE)) { + MultiValueMap map = matrixVariables.get(pathVariable); + return (map != null) ? map : Collections.emptyMap(); + } + + MultiValueMap map = new LinkedMultiValueMap(); + for (MultiValueMap vars : matrixVariables.values()) { + for (String name : vars.keySet()) { + for (String value : vars.get(name)) { + map.add(name, value); + } + } + } + + return map; + } + +} \ No newline at end of file diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariableMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariableMethodArgumentResolver.java new file mode 100644 index 0000000000..c8c4e4fc61 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariableMethodArgumentResolver.java @@ -0,0 +1,129 @@ +/* + * Copyright 2002-2012 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.mvc.method.annotation; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.springframework.core.MethodParameter; +import org.springframework.util.CollectionUtils; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.ServletRequestBindingException; +import org.springframework.web.bind.annotation.MatrixVariable; +import org.springframework.web.bind.annotation.ValueConstants; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver; +import org.springframework.web.servlet.HandlerMapping; + +/** + * Resolves method arguments annotated with an {@link MatrixVariable @PathParam}. + * + *

    If the method parameter is of type Map and no name is specified, then it will + * by resolved by the {@link MatrixVariableMapMethodArgumentResolver} instead. + * + * @author Rossen Stoyanchev + * @since 3.2 + */ +public class MatrixVariableMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver { + + public MatrixVariableMethodArgumentResolver() { + super(null); + } + + public boolean supportsParameter(MethodParameter parameter) { + if (!parameter.hasParameterAnnotation(MatrixVariable.class)) { + return false; + } + if (Map.class.isAssignableFrom(parameter.getParameterType())) { + String paramName = parameter.getParameterAnnotation(MatrixVariable.class).value(); + return StringUtils.hasText(paramName); + } + return true; + } + + @Override + protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { + MatrixVariable annotation = parameter.getParameterAnnotation(MatrixVariable.class); + return new PathParamNamedValueInfo(annotation); + } + + @Override + protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception { + + @SuppressWarnings("unchecked") + Map> pathParameters = + (Map>) request.getAttribute( + HandlerMapping.MATRIX_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST); + + if (CollectionUtils.isEmpty(pathParameters)) { + return null; + } + + String pathVar = parameter.getParameterAnnotation(MatrixVariable.class).pathVar(); + List paramValues = null; + + if (!pathVar.equals(ValueConstants.DEFAULT_NONE)) { + if (pathParameters.containsKey(pathVar)) { + paramValues = pathParameters.get(pathVar).get(name); + } + } + else { + boolean found = false; + paramValues = new ArrayList(); + for (MultiValueMap params : pathParameters.values()) { + if (params.containsKey(name)) { + if (found) { + String paramType = parameter.getParameterType().getName(); + throw new ServletRequestBindingException( + "Found more than one match for URI path parameter '" + name + + "' for parameter type [" + paramType + "]. Use pathVar attribute to disambiguate."); + } + paramValues.addAll(params.get(name)); + found = true; + } + } + } + + if (CollectionUtils.isEmpty(paramValues)) { + return null; + } + else if (paramValues.size() == 1) { + return paramValues.get(0); + } + else { + return paramValues; + } + } + + @Override + protected void handleMissingValue(String name, MethodParameter param) throws ServletRequestBindingException { + String paramType = param.getParameterType().getName(); + throw new ServletRequestBindingException( + "Missing URI path parameter '" + name + "' for method parameter type [" + paramType + "]"); + } + + + private static class PathParamNamedValueInfo extends NamedValueInfo { + + private PathParamNamedValueInfo(MatrixVariable annotation) { + super(annotation.value(), annotation.required(), annotation.defaultValue()); + } + } +} \ No newline at end of file diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/PathVariableMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/PathVariableMethodArgumentResolver.java index 9e5a88524a..d93b7746a7 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/PathVariableMethodArgumentResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/PathVariableMethodArgumentResolver.java @@ -100,11 +100,9 @@ public class PathVariableMethodArgumentResolver extends AbstractNamedValueMethod @Override @SuppressWarnings("unchecked") - protected void handleResolvedValue(Object arg, - String name, - MethodParameter parameter, - ModelAndViewContainer mavContainer, - NativeWebRequest request) { + protected void handleResolvedValue(Object arg, String name, MethodParameter parameter, + ModelAndViewContainer mavContainer, NativeWebRequest request) { + String key = View.PATH_VARIABLES; int scope = RequestAttributes.SCOPE_REQUEST; Map pathVars = (Map) request.getAttribute(key, scope); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java index 8567a565ba..e41592b315 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java @@ -480,6 +480,8 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i resolvers.add(new RequestParamMapMethodArgumentResolver()); resolvers.add(new PathVariableMethodArgumentResolver()); resolvers.add(new PathVariableMapMethodArgumentResolver()); + resolvers.add(new MatrixVariableMethodArgumentResolver()); + resolvers.add(new MatrixVariableMapMethodArgumentResolver()); resolvers.add(new ServletModelAttributeMethodProcessor(false)); resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters())); resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters())); @@ -522,6 +524,9 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false)); resolvers.add(new RequestParamMapMethodArgumentResolver()); resolvers.add(new PathVariableMethodArgumentResolver()); + resolvers.add(new PathVariableMapMethodArgumentResolver()); + resolvers.add(new MatrixVariableMethodArgumentResolver()); + resolvers.add(new MatrixVariableMapMethodArgumentResolver()); resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory())); // Type-based argument resolution diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/DefaultRequestToViewNameTranslator.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/DefaultRequestToViewNameTranslator.java index 3fea2b436b..bcea6d584d 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/DefaultRequestToViewNameTranslator.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/DefaultRequestToViewNameTranslator.java @@ -145,6 +145,14 @@ public class DefaultRequestToViewNameTranslator implements RequestToViewNameTran this.urlPathHelper.setUrlDecode(urlDecode); } + /** + * Set if ";" (semicolon) content should be stripped from the request URI. + * @see org.springframework.web.util.UrlPathHelper#setRemoveSemicolonContent(boolean) + */ + public void setRemoveSemicolonContent(boolean removeSemicolonContent) { + this.urlPathHelper.setRemoveSemicolonContent(removeSemicolonContent); + } + /** * Set the {@link org.springframework.web.util.UrlPathHelper} to use for * the resolution of lookup paths. diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/MappedInterceptorTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/MappedInterceptorTests.java index 3f71c09786..e3826af215 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/MappedInterceptorTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/MappedInterceptorTests.java @@ -45,7 +45,7 @@ public class MappedInterceptorTests { } @Test - public void includePatternOnly() { + public void includePattern() { MappedInterceptor mappedInterceptor = new MappedInterceptor(new String[] { "/foo/*" }, this.interceptor); assertTrue(mappedInterceptor.matches("/foo/bar", pathMatcher)); @@ -53,7 +53,13 @@ public class MappedInterceptorTests { } @Test - public void excludePatternOnly() { + public void includePatternWithMatrixVariables() { + MappedInterceptor mappedInterceptor = new MappedInterceptor(new String[] { "/foo*/*" }, this.interceptor); + assertTrue(mappedInterceptor.matches("/foo;q=1/bar;s=2", pathMatcher)); + } + + @Test + public void excludePattern() { MappedInterceptor mappedInterceptor = new MappedInterceptor(null, new String[] { "/admin/**" }, this.interceptor); assertTrue(mappedInterceptor.matches("/foo", pathMatcher)); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/UrlFilenameViewControllerTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/UrlFilenameViewControllerTests.java index 2ac2f6977d..1f5865090d 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/UrlFilenameViewControllerTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/UrlFilenameViewControllerTests.java @@ -55,6 +55,15 @@ public class UrlFilenameViewControllerTests extends TestCase { assertTrue(mv.getModel().isEmpty()); } + public void testWithFilenameAndMatrixVariables() throws Exception { + UrlFilenameViewController ctrl = new UrlFilenameViewController(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/index;a=A;b=B"); + MockHttpServletResponse response = new MockHttpServletResponse(); + ModelAndView mv = ctrl.handleRequest(request, response); + assertEquals("index", mv.getViewName()); + assertTrue(mv.getModel().isEmpty()); + } + public void testWithPrefixAndSuffix() throws Exception { UrlFilenameViewController ctrl = new UrlFilenameViewController(); ctrl.setPrefix("mypre_"); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMappingTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMappingTests.java index 36a3e3c778..deea55a349 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMappingTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMappingTests.java @@ -30,12 +30,15 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; +import javax.servlet.http.HttpServletRequest; + import org.junit.Before; import org.junit.Test; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.stereotype.Controller; +import org.springframework.util.MultiValueMap; import org.springframework.web.HttpMediaTypeNotAcceptableException; import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.HttpRequestMethodNotSupportedException; @@ -65,7 +68,7 @@ import org.springframework.web.util.UrlPathHelper; */ public class RequestMappingInfoHandlerMappingTests { - private TestRequestMappingInfoHandlerMapping mapping; + private TestRequestMappingInfoHandlerMapping handlerMapping; private Handler handler; @@ -85,15 +88,16 @@ public class RequestMappingInfoHandlerMappingTests { this.barMethod = new HandlerMethod(handler, "bar"); this.emptyMethod = new HandlerMethod(handler, "empty"); - this.mapping = new TestRequestMappingInfoHandlerMapping(); - this.mapping.registerHandler(this.handler); + this.handlerMapping = new TestRequestMappingInfoHandlerMapping(); + this.handlerMapping.registerHandler(this.handler); + this.handlerMapping.setRemoveSemicolonContent(false); } @Test public void getMappingPathPatterns() throws Exception { RequestMappingInfo info = new RequestMappingInfo( new PatternsRequestCondition("/foo/*", "/foo", "/bar/*", "/bar"), null, null, null, null, null, null); - Set paths = this.mapping.getMappingPathPatterns(info); + Set paths = this.handlerMapping.getMappingPathPatterns(info); HashSet expected = new HashSet(Arrays.asList("/foo/*", "/foo", "/bar/*", "/bar")); assertEquals(expected, paths); @@ -102,25 +106,25 @@ public class RequestMappingInfoHandlerMappingTests { @Test public void directMatch() throws Exception { MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); - HandlerMethod hm = (HandlerMethod) this.mapping.getHandler(request).getHandler(); + HandlerMethod hm = (HandlerMethod) this.handlerMapping.getHandler(request).getHandler(); assertEquals(this.fooMethod.getMethod(), hm.getMethod()); } @Test public void globMatch() throws Exception { MockHttpServletRequest request = new MockHttpServletRequest("GET", "/bar"); - HandlerMethod hm = (HandlerMethod) this.mapping.getHandler(request).getHandler(); + HandlerMethod hm = (HandlerMethod) this.handlerMapping.getHandler(request).getHandler(); assertEquals(this.barMethod.getMethod(), hm.getMethod()); } @Test public void emptyPathMatch() throws Exception { MockHttpServletRequest request = new MockHttpServletRequest("GET", ""); - HandlerMethod hm = (HandlerMethod) this.mapping.getHandler(request).getHandler(); + HandlerMethod hm = (HandlerMethod) this.handlerMapping.getHandler(request).getHandler(); assertEquals(this.emptyMethod.getMethod(), hm.getMethod()); request = new MockHttpServletRequest("GET", "/"); - hm = (HandlerMethod) this.mapping.getHandler(request).getHandler(); + hm = (HandlerMethod) this.handlerMapping.getHandler(request).getHandler(); assertEquals(this.emptyMethod.getMethod(), hm.getMethod()); } @@ -128,7 +132,7 @@ public class RequestMappingInfoHandlerMappingTests { public void bestMatch() throws Exception { MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); request.setParameter("p", "anything"); - HandlerMethod hm = (HandlerMethod) this.mapping.getHandler(request).getHandler(); + HandlerMethod hm = (HandlerMethod) this.handlerMapping.getHandler(request).getHandler(); assertEquals(this.fooParamMethod.getMethod(), hm.getMethod()); } @@ -136,7 +140,7 @@ public class RequestMappingInfoHandlerMappingTests { public void requestMethodNotAllowed() throws Exception { try { MockHttpServletRequest request = new MockHttpServletRequest("POST", "/bar"); - this.mapping.getHandler(request); + this.handlerMapping.getHandler(request); fail("HttpRequestMethodNotSupportedException expected"); } catch (HttpRequestMethodNotSupportedException ex) { @@ -155,7 +159,7 @@ public class RequestMappingInfoHandlerMappingTests { try { MockHttpServletRequest request = new MockHttpServletRequest("PUT", url); request.setContentType("application/json"); - this.mapping.getHandler(request); + this.handlerMapping.getHandler(request); fail("HttpMediaTypeNotSupportedException expected"); } catch (HttpMediaTypeNotSupportedException ex) { @@ -175,7 +179,7 @@ public class RequestMappingInfoHandlerMappingTests { try { MockHttpServletRequest request = new MockHttpServletRequest("GET", url); request.addHeader("Accept", "application/json"); - this.mapping.getHandler(request); + this.handlerMapping.getHandler(request); fail("HttpMediaTypeNotAcceptableException expected"); } catch (HttpMediaTypeNotAcceptableException ex) { @@ -190,7 +194,7 @@ public class RequestMappingInfoHandlerMappingTests { RequestMappingInfo key = new RequestMappingInfo(patterns, null, null, null, null, null, null); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/1/2"); String lookupPath = new UrlPathHelper().getLookupPathForRequest(request); - this.mapping.handleMatch(key, lookupPath, request); + this.handlerMapping.handleMatch(key, lookupPath, request); @SuppressWarnings("unchecked") Map uriVariables = @@ -214,8 +218,8 @@ public class RequestMappingInfoHandlerMappingTests { pathHelper.setUrlDecode(false); String lookupPath = pathHelper.getLookupPathForRequest(request); - this.mapping.setUrlPathHelper(pathHelper); - this.mapping.handleMatch(key, lookupPath, request); + this.handlerMapping.setUrlPathHelper(pathHelper); + this.handlerMapping.handleMatch(key, lookupPath, request); @SuppressWarnings("unchecked") Map uriVariables = @@ -232,7 +236,7 @@ public class RequestMappingInfoHandlerMappingTests { RequestMappingInfo key = new RequestMappingInfo(patterns, null, null, null, null, null, null); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/1/2"); - this.mapping.handleMatch(key, "/1/2", request); + this.handlerMapping.handleMatch(key, "/1/2", request); assertEquals("/{path1}/2", request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE)); } @@ -243,7 +247,7 @@ public class RequestMappingInfoHandlerMappingTests { RequestMappingInfo key = new RequestMappingInfo(patterns, null, null, null, null, null, null); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/1/2"); - this.mapping.handleMatch(key, "/1/2", request); + this.handlerMapping.handleMatch(key, "/1/2", request); assertEquals("/1/2", request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE)); } @@ -252,14 +256,14 @@ public class RequestMappingInfoHandlerMappingTests { public void producibleMediaTypesAttribute() throws Exception { MockHttpServletRequest request = new MockHttpServletRequest("GET", "/content"); request.addHeader("Accept", "application/xml"); - this.mapping.getHandler(request); + this.handlerMapping.getHandler(request); assertEquals(Collections.singleton(MediaType.APPLICATION_XML), request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE)); request = new MockHttpServletRequest("GET", "/content"); request.addHeader("Accept", "application/json"); - this.mapping.getHandler(request); + this.handlerMapping.getHandler(request); assertNull("Negated expression should not be listed as a producible type", request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE)); @@ -285,7 +289,74 @@ public class RequestMappingInfoHandlerMappingTests { assertNull(chain); } - @SuppressWarnings("unused") + @Test + public void matrixVariables() { + + MockHttpServletRequest request; + MultiValueMap matrixVariables; + Map uriVariables; + + String lookupPath = "/cars;colors=red,blue,green;year=2012"; + + // Pattern "/{cars}" : matrix variables stripped from "cars" variable + + request = new MockHttpServletRequest(); + testHandleMatch(request, "/{cars}", lookupPath); + + matrixVariables = getMatrixVariables(request, "cars"); + assertNotNull(matrixVariables); + assertEquals(Arrays.asList("red", "blue", "green"), matrixVariables.get("colors")); + assertEquals("2012", matrixVariables.getFirst("year")); + + uriVariables = getUriTemplateVariables(request); + assertEquals("cars", uriVariables.get("cars")); + + // Pattern "/{cars:[^;]+}{params}" : "cars" and "params" variables unchanged + + request = new MockHttpServletRequest(); + testHandleMatch(request, "/{cars:[^;]+}{params}", lookupPath); + + matrixVariables = getMatrixVariables(request, "params"); + assertNotNull(matrixVariables); + assertEquals(Arrays.asList("red", "blue", "green"), matrixVariables.get("colors")); + assertEquals("2012", matrixVariables.getFirst("year")); + + uriVariables = getUriTemplateVariables(request); + assertEquals("cars", uriVariables.get("cars")); + assertEquals(";colors=red,blue,green;year=2012", uriVariables.get("params")); + + // matrix variables not present : "params" variable is empty + + request = new MockHttpServletRequest(); + testHandleMatch(request, "/{cars:[^;]+}{params}", "/cars"); + + matrixVariables = getMatrixVariables(request, "params"); + assertNull(matrixVariables); + + uriVariables = getUriTemplateVariables(request); + assertEquals("cars", uriVariables.get("cars")); + assertEquals("", uriVariables.get("params")); + } + + private void testHandleMatch(MockHttpServletRequest request, String pattern, String lookupPath) { + PatternsRequestCondition patterns = new PatternsRequestCondition(pattern); + RequestMappingInfo info = new RequestMappingInfo(patterns, null, null, null, null, null, null); + this.handlerMapping.handleMatch(info, lookupPath, request); + } + + @SuppressWarnings("unchecked") + private MultiValueMap getMatrixVariables(HttpServletRequest request, String uriVarName) { + String attrName = HandlerMapping.MATRIX_VARIABLES_ATTRIBUTE; + return ((Map>) request.getAttribute(attrName)).get(uriVarName); + } + + @SuppressWarnings("unchecked") + private Map getUriTemplateVariables(HttpServletRequest request) { + String attrName = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE; + return (Map) request.getAttribute(attrName); + } + + @Controller private static class Handler { diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/AbstractServletHandlerMethodTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/AbstractServletHandlerMethodTests.java index 07eac6d8c2..899cd61f48 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/AbstractServletHandlerMethodTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/AbstractServletHandlerMethodTests.java @@ -31,24 +31,24 @@ import org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionRes import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver; /** - * Base class for tests using on the DispatcherServlet and HandlerMethod infrastructure classes: + * Base class for tests using on the DispatcherServlet and HandlerMethod infrastructure classes: *

      - *
    • RequestMappingHandlerMapping - *
    • RequestMappingHandlerAdapter + *
    • RequestMappingHandlerMapping + *
    • RequestMappingHandlerAdapter *
    • ExceptionHandlerExceptionResolver *
    - * + * * @author Rossen Stoyanchev */ public class AbstractServletHandlerMethodTests { private DispatcherServlet servlet; - + @After public void tearDown() { this.servlet = null; } - + protected DispatcherServlet getServlet() { assertNotNull("DispatcherServlet not initialized", servlet); return servlet; @@ -68,30 +68,32 @@ public class AbstractServletHandlerMethodTests { */ @SuppressWarnings("serial") protected WebApplicationContext initServlet( - final ApplicationContextInitializer initializer, + final ApplicationContextInitializer initializer, final Class... controllerClasses) throws ServletException { - + final GenericWebApplicationContext wac = new GenericWebApplicationContext(); - + servlet = new DispatcherServlet() { @Override protected WebApplicationContext createWebApplicationContext(WebApplicationContext parent) { for (Class clazz : controllerClasses) { wac.registerBeanDefinition(clazz.getSimpleName(), new RootBeanDefinition(clazz)); } - + Class mappingType = RequestMappingHandlerMapping.class; - wac.registerBeanDefinition("handlerMapping", new RootBeanDefinition(mappingType)); - + RootBeanDefinition beanDef = new RootBeanDefinition(mappingType); + beanDef.getPropertyValues().add("removeSemicolonContent", "false"); + wac.registerBeanDefinition("handlerMapping", beanDef); + Class adapterType = RequestMappingHandlerAdapter.class; wac.registerBeanDefinition("handlerAdapter", new RootBeanDefinition(adapterType)); - + Class resolverType = ExceptionHandlerExceptionResolver.class; wac.registerBeanDefinition("requestMappingResolver", new RootBeanDefinition(resolverType)); - + resolverType = ResponseStatusExceptionResolver.class; wac.registerBeanDefinition("responseStatusResolver", new RootBeanDefinition(resolverType)); - + resolverType = DefaultHandlerExceptionResolver.class; wac.registerBeanDefinition("defaultResolver", new RootBeanDefinition(resolverType)); @@ -103,9 +105,9 @@ public class AbstractServletHandlerMethodTests { return wac; } }; - + servlet.init(new MockServletConfig()); - + return wac; } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariablesMapMethodArgumentResolverTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariablesMapMethodArgumentResolverTests.java new file mode 100644 index 0000000000..eb40decd0a --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariablesMapMethodArgumentResolverTests.java @@ -0,0 +1,186 @@ +/* + * Copyright 2002-2011 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.mvc.method.annotation; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.core.MethodParameter; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.annotation.MatrixVariable; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.method.support.ModelAndViewContainer; +import org.springframework.web.servlet.HandlerMapping; + +/** + * Test fixture with {@link MatrixVariableMethodArgumentResolver}. + * + * @author Rossen Stoyanchev + */ +public class MatrixVariablesMapMethodArgumentResolverTests { + + private MatrixVariableMapMethodArgumentResolver resolver; + + private MethodParameter paramString; + private MethodParameter paramMap; + private MethodParameter paramMultivalueMap; + private MethodParameter paramMapForPathVar; + private MethodParameter paramMapWithName; + + private ModelAndViewContainer mavContainer; + + private ServletWebRequest webRequest; + + private MockHttpServletRequest request; + + + @Before + public void setUp() throws Exception { + this.resolver = new MatrixVariableMapMethodArgumentResolver(); + + Method method = getClass().getMethod("handle", String.class, + Map.class, MultiValueMap.class, MultiValueMap.class, Map.class); + + this.paramString = new MethodParameter(method, 0); + this.paramMap = new MethodParameter(method, 1); + this.paramMultivalueMap = new MethodParameter(method, 2); + this.paramMapForPathVar = new MethodParameter(method, 3); + this.paramMapWithName = new MethodParameter(method, 4); + + this.mavContainer = new ModelAndViewContainer(); + this.request = new MockHttpServletRequest(); + this.webRequest = new ServletWebRequest(request, new MockHttpServletResponse()); + + Map> params = new LinkedHashMap>(); + this.request.setAttribute(HandlerMapping.MATRIX_VARIABLES_ATTRIBUTE, params); + } + + @Test + public void supportsParameter() { + assertFalse(resolver.supportsParameter(paramString)); + assertTrue(resolver.supportsParameter(paramMap)); + assertTrue(resolver.supportsParameter(paramMultivalueMap)); + assertTrue(resolver.supportsParameter(paramMapForPathVar)); + assertFalse(resolver.supportsParameter(paramMapWithName)); + } + + @Test + public void resolveArgument() throws Exception { + + MultiValueMap params = getMatrixVariables("cars"); + params.add("colors", "red"); + params.add("colors", "green"); + params.add("colors", "blue"); + params.add("year", "2012"); + + @SuppressWarnings("unchecked") + Map map = (Map) this.resolver.resolveArgument( + this.paramMap, this.mavContainer, this.webRequest, null); + + assertEquals(Arrays.asList("red", "green", "blue"), map.get("colors")); + + @SuppressWarnings("unchecked") + MultiValueMap multivalueMap = (MultiValueMap) this.resolver.resolveArgument( + this.paramMultivalueMap, this.mavContainer, this.webRequest, null); + + assertEquals(Arrays.asList("red", "green", "blue"), multivalueMap.get("colors")); + } + + @Test + public void resolveArgumentPathVariable() throws Exception { + + MultiValueMap params1 = getMatrixVariables("cars"); + params1.add("colors", "red"); + params1.add("colors", "purple"); + + MultiValueMap params2 = getMatrixVariables("planes"); + params2.add("colors", "yellow"); + params2.add("colors", "orange"); + + @SuppressWarnings("unchecked") + Map mapForPathVar = (Map) this.resolver.resolveArgument( + this.paramMapForPathVar, this.mavContainer, this.webRequest, null); + + assertEquals(Arrays.asList("red", "purple"), mapForPathVar.get("colors")); + + @SuppressWarnings("unchecked") + Map mapAll = (Map) this.resolver.resolveArgument( + this.paramMap, this.mavContainer, this.webRequest, null); + + assertEquals(Arrays.asList("red", "purple", "yellow", "orange"), mapAll.get("colors")); + } + + @Test + public void resolveArgumentNoParams() throws Exception { + + @SuppressWarnings("unchecked") + Map map = (Map) this.resolver.resolveArgument( + this.paramMap, this.mavContainer, this.webRequest, null); + + assertEquals(Collections.emptyMap(), map); + } + + @Test + public void resolveArgumentNoMatch() throws Exception { + + MultiValueMap params2 = getMatrixVariables("planes"); + params2.add("colors", "yellow"); + params2.add("colors", "orange"); + + @SuppressWarnings("unchecked") + Map map = (Map) this.resolver.resolveArgument( + this.paramMapForPathVar, this.mavContainer, this.webRequest, null); + + assertEquals(Collections.emptyMap(), map); + } + + + @SuppressWarnings("unchecked") + private MultiValueMap getMatrixVariables(String pathVarName) { + + Map> matrixVariables = + (Map>) this.request.getAttribute( + HandlerMapping.MATRIX_VARIABLES_ATTRIBUTE); + + MultiValueMap params = new LinkedMultiValueMap(); + matrixVariables.put(pathVarName, params); + + return params; + } + + + public void handle( + String stringArg, + @MatrixVariable Map map, + @MatrixVariable MultiValueMap multivalueMap, + @MatrixVariable(pathVar="cars") MultiValueMap mapForPathVar, + @MatrixVariable("name") Map mapWithName) { + } + +} \ No newline at end of file diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariablesMethodArgumentResolverTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariablesMethodArgumentResolverTests.java new file mode 100644 index 0000000000..2a896eb8df --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariablesMethodArgumentResolverTests.java @@ -0,0 +1,159 @@ +/* + * Copyright 2002-2011 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.mvc.method.annotation; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.core.LocalVariableTableParameterNameDiscoverer; +import org.springframework.core.MethodParameter; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.ServletRequestBindingException; +import org.springframework.web.bind.annotation.MatrixVariable; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.method.support.ModelAndViewContainer; +import org.springframework.web.servlet.HandlerMapping; + +/** + * Test fixture with {@link MatrixVariableMethodArgumentResolver}. + * + * @author Rossen Stoyanchev + */ +public class MatrixVariablesMethodArgumentResolverTests { + + private MatrixVariableMethodArgumentResolver resolver; + + private MethodParameter paramString; + private MethodParameter paramColors; + private MethodParameter paramYear; + + private ModelAndViewContainer mavContainer; + + private ServletWebRequest webRequest; + + private MockHttpServletRequest request; + + + @Before + public void setUp() throws Exception { + this.resolver = new MatrixVariableMethodArgumentResolver(); + + Method method = getClass().getMethod("handle", String.class, List.class, int.class); + this.paramString = new MethodParameter(method, 0); + this.paramColors = new MethodParameter(method, 1); + this.paramYear = new MethodParameter(method, 2); + + this.paramColors.initParameterNameDiscovery(new LocalVariableTableParameterNameDiscoverer()); + + this.mavContainer = new ModelAndViewContainer(); + this.request = new MockHttpServletRequest(); + this.webRequest = new ServletWebRequest(request, new MockHttpServletResponse()); + + Map> params = new LinkedHashMap>(); + this.request.setAttribute(HandlerMapping.MATRIX_VARIABLES_ATTRIBUTE, params); + } + + @Test + public void supportsParameter() { + assertFalse(resolver.supportsParameter(paramString)); + assertTrue(resolver.supportsParameter(paramColors)); + assertTrue(resolver.supportsParameter(paramYear)); + } + + @Test + public void resolveArgument() throws Exception { + + MultiValueMap params = getMatrixVariables("cars"); + params.add("colors", "red"); + params.add("colors", "green"); + params.add("colors", "blue"); + + assertEquals(Arrays.asList("red", "green", "blue"), + this.resolver.resolveArgument(this.paramColors, this.mavContainer, this.webRequest, null)); + } + + @Test + public void resolveArgumentPathVariable() throws Exception { + + getMatrixVariables("cars").add("year", "2006"); + getMatrixVariables("bikes").add("year", "2005"); + + assertEquals("2006", this.resolver.resolveArgument(this.paramYear, this.mavContainer, this.webRequest, null)); + } + + @Test + public void resolveArgumentDefaultValue() throws Exception { + assertEquals("2013", resolver.resolveArgument(this.paramYear, this.mavContainer, this.webRequest, null)); + } + + @Test(expected=ServletRequestBindingException.class) + public void resolveArgumentMultipleMatches() throws Exception { + + getMatrixVariables("var1").add("colors", "red"); + getMatrixVariables("var2").add("colors", "green"); + + this.resolver.resolveArgument(this.paramColors, this.mavContainer, this.webRequest, null); + } + + @Test(expected=ServletRequestBindingException.class) + public void resolveArgumentRequired() throws Exception { + resolver.resolveArgument(this.paramColors, this.mavContainer, this.webRequest, null); + } + + @Test + public void resolveArgumentNoMatch() throws Exception { + + MultiValueMap params = getMatrixVariables("cars"); + params.add("anotherYear", "2012"); + + assertEquals("2013", this.resolver.resolveArgument(this.paramYear, this.mavContainer, this.webRequest, null)); + } + + + @SuppressWarnings("unchecked") + private MultiValueMap getMatrixVariables(String pathVarName) { + + Map> matrixVariables = + (Map>) this.request.getAttribute( + HandlerMapping.MATRIX_VARIABLES_ATTRIBUTE); + + MultiValueMap params = new LinkedMultiValueMap(); + matrixVariables.put(pathVarName, params); + + return params; + } + + + public void handle( + String stringArg, + @MatrixVariable List colors, + @MatrixVariable(value="year", pathVar="cars", required=false, defaultValue="2013") int preferredYear) { + } + +} \ No newline at end of file diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/UriTemplateServletAnnotationControllerHandlerMethodTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/UriTemplateServletAnnotationControllerHandlerMethodTests.java index 97c283b67a..1565796196 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/UriTemplateServletAnnotationControllerHandlerMethodTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/UriTemplateServletAnnotationControllerHandlerMethodTests.java @@ -22,9 +22,11 @@ import static org.junit.Assert.assertTrue; import java.io.IOException; import java.io.Writer; import java.text.SimpleDateFormat; +import java.util.Arrays; import java.util.Date; import java.util.GregorianCalendar; import java.util.HashMap; +import java.util.List; import java.util.Locale; import java.util.Map; @@ -38,8 +40,10 @@ import org.springframework.context.ApplicationContextInitializer; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.stereotype.Controller; +import org.springframework.util.MultiValueMap; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.InitBinder; +import org.springframework.web.bind.annotation.MatrixVariable; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @@ -52,22 +56,22 @@ import org.springframework.web.servlet.mvc.annotation.UriTemplateServletAnnotati import org.springframework.web.servlet.view.AbstractView; /** - * The origin of this test class is {@link UriTemplateServletAnnotationControllerTests}. - * + * The origin of this test class is {@link UriTemplateServletAnnotationControllerTests}. + * * Tests in this class run against the {@link HandlerMethod} infrastructure: *
      - *
    • RequestMappingHandlerMapping - *
    • RequestMappingHandlerAdapter + *
    • RequestMappingHandlerMapping + *
    • RequestMappingHandlerAdapter *
    • ExceptionHandlerExceptionResolver *
    - * + * *

    Rather than against the existing infrastructure: *

      *
    • DefaultAnnotationHandlerMapping *
    • AnnotationMethodHandlerAdapter *
    • AnnotationMethodHandlerExceptionResolver - *
    - * + * + * * @author Rossen Stoyanchev * @since 3.1 */ @@ -80,17 +84,18 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab MockHttpServletRequest request = new MockHttpServletRequest("GET", "/42"); MockHttpServletResponse response = new MockHttpServletResponse(); getServlet().service(request, response); - assertEquals("test-42", response.getContentAsString()); + assertEquals("test-42-7", response.getContentAsString()); } @Test public void multiple() throws Exception { initServletWithControllers(MultipleUriTemplateController.class); - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels/42/bookings/21-other"); + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels/42;q=24/bookings/21-other;q=12"); MockHttpServletResponse response = new MockHttpServletResponse(); getServlet().service(request, response); - assertEquals("test-42-21-other", response.getContentAsString()); + assertEquals(200, response.getStatus()); + assertEquals("test-42-q24-21-other-q12", response.getContentAsString()); } @Test @@ -99,8 +104,8 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab pathVars.put("hotel", "42"); pathVars.put("booking", 21); pathVars.put("other", "other"); - - WebApplicationContext wac = + + WebApplicationContext wac = initServlet(new ApplicationContextInitializer() { public void initialize(GenericWebApplicationContext context) { RootBeanDefinition beanDef = new RootBeanDefinition(ModelValidatingViewResolver.class); @@ -109,9 +114,9 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab } }, ViewRenderingController.class); - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels/42/bookings/21-other"); + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels/42;q=1,2/bookings/21-other;q=3;r=R"); getServlet().service(request, new MockHttpServletResponse()); - + ModelValidatingViewResolver resolver = wac.getBean(ModelValidatingViewResolver.class); assertEquals(3, resolver.validatedAttrCount); } @@ -166,10 +171,10 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab public void extension() throws Exception { initServletWithControllers(SimpleUriTemplateController.class); - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/42.xml"); + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/42;jsessionid=c0o7fszeb1;q=24.xml"); MockHttpServletResponse response = new MockHttpServletResponse(); getServlet().service(request, response); - assertEquals("test-42", response.getContentAsString()); + assertEquals("test-42-24", response.getContentAsString()); } @@ -287,10 +292,11 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab public void customRegex() throws Exception { initServletWithControllers(CustomRegexController.class); - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/42"); + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/42;q=1;q=2"); MockHttpServletResponse response = new MockHttpServletResponse(); getServlet().service(request, response); - assertEquals("test-42", response.getContentAsString()); + assertEquals(200, response.getStatus()); + assertEquals("test-42-;q=1;q=2-[1, 2]", response.getContentAsString()); } /* @@ -343,7 +349,7 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab @Test public void doIt() throws Exception { initServletWithControllers(Spr6978Controller.class); - + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo/100"); MockHttpServletResponse response = new MockHttpServletResponse(); getServlet().service(request, response); @@ -375,9 +381,11 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab public static class SimpleUriTemplateController { @RequestMapping("/{root}") - public void handle(@PathVariable("root") int root, Writer writer) throws IOException { + public void handle(@PathVariable("root") int root, @MatrixVariable(required=false, defaultValue="7") int q, + Writer writer) throws IOException { + assertEquals("Invalid path variable value", 42, root); - writer.write("test-" + root); + writer.write("test-" + root + "-" + q); } } @@ -389,10 +397,12 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab public void handle(@PathVariable("hotel") String hotel, @PathVariable int booking, @PathVariable String other, + @MatrixVariable(value="q", pathVar="hotel") int qHotel, + @MatrixVariable(value="q", pathVar="other") int qOther, Writer writer) throws IOException { assertEquals("Invalid path variable value", "42", hotel); assertEquals("Invalid path variable value", 21, booking); - writer.write("test-" + hotel + "-" + booking + "-" + other); + writer.write("test-" + hotel + "-q" + qHotel + "-" + booking + "-" + other + "-q" + qOther); } } @@ -401,9 +411,13 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab public static class ViewRenderingController { @RequestMapping("/hotels/{hotel}/bookings/{booking}-{other}") - public void handle(@PathVariable("hotel") String hotel, @PathVariable int booking, @PathVariable String other) { + public void handle(@PathVariable("hotel") String hotel, @PathVariable int booking, + @PathVariable String other, @MatrixVariable MultiValueMap params) { + assertEquals("Invalid path variable value", "42", hotel); assertEquals("Invalid path variable value", 21, booking); + assertEquals(Arrays.asList("1", "2", "3"), params.get("q")); + assertEquals("R", params.getFirst("r")); } } @@ -499,12 +513,13 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab @Controller public static class CustomRegexController { - @RequestMapping("/{root:\\d+}") - public void handle(@PathVariable("root") int root, Writer writer) throws IOException { - assertEquals("Invalid path variable value", 42, root); - writer.write("test-" + root); - } + @RequestMapping("/{root:\\d+}{params}") + public void handle(@PathVariable("root") int root, @PathVariable("params") String paramString, + @MatrixVariable List q, Writer writer) throws IOException { + assertEquals("Invalid path variable value", 42, root); + writer.write("test-" + root + "-" + paramString + "-" + q); + } } @Controller @@ -515,7 +530,6 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab throws IOException { writer.write("latitude-" + latitude + "-longitude-" + longitude); } - } @@ -650,9 +664,9 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab public static class ModelValidatingViewResolver implements ViewResolver { private final Map attrsToValidate; - + int validatedAttrCount; - + public ModelValidatingViewResolver(Map attrsToValidate) { this.attrsToValidate = attrsToValidate; } @@ -674,8 +688,8 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab }; } } - -// @Ignore("ControllerClassNameHandlerMapping") + +// @Ignore("ControllerClassNameHandlerMapping") // public void controllerClassName() throws Exception { // @Ignore("useDefaultSuffixPattern property not supported") @@ -683,5 +697,5 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab // @Ignore("useDefaultSuffixPattern property not supported") // public void noDefaultSuffixPattern() throws Exception { - + } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/DefaultRequestToViewNameTranslatorTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/DefaultRequestToViewNameTranslatorTests.java index 6249664011..e093f0a230 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/DefaultRequestToViewNameTranslatorTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/DefaultRequestToViewNameTranslatorTests.java @@ -84,6 +84,12 @@ public final class DefaultRequestToViewNameTranslatorTests { assertViewName(VIEW_NAME); } + @Test + public void testGetViewNameWithSemicolonContent() { + request.setRequestURI(CONTEXT_PATH + VIEW_NAME + ";a=A;b=B"); + assertViewName(VIEW_NAME); + } + @Test public void testGetViewNameWithPrefix() { final String prefix = "fiona_"; diff --git a/src/reference/docbook/mvc.xml b/src/reference/docbook/mvc.xml index 76af127bd7..56b2e1671c 100644 --- a/src/reference/docbook/mvc.xml +++ b/src/reference/docbook/mvc.xml @@ -1054,6 +1054,93 @@ public class RelativePathUriTemplateController { /owners/*/pets/{petId}). +
    + Matrix Variables + + The URI specification + RFC 3986 + defines the possibility of including name-value pairs within path segments. + There is no specific term used in the spec. + The general "URI path parameters" could be applied although the more unique + "Matrix URIs", + originating from an old post by Tim Berners-Lee, is also frequently used + and fairly well known. Within Spring MVC these are referred to + as matrix variables. + + Matrix variables can appear in any path segment, each matrix variable + separated with a ";" (semicolon). + For example: "/cars;color=red;year=2012". + Multiple values may be either "," (comma) separated + "color=red,green,blue" or the variable name may be repeated + "color=red;color=green;color=blue". + + If a URL is expected to contain matrix variables, the request mapping + pattern must represent them with a URI template. + This ensures the request can be matched correctly regardless of whether + matrix variables are present or not and in what order they are + provided. + + Below is an example of extracting the matrix variable "q": + + // GET /pets/42;q=11;r=22 + +@RequestMapping(value = "/pets/{petId}", method = RequestMethod.GET) +public void findPet(@PathVariable String petId, @MatrixVariable int q) { + + // petId == 42 + // q == 11 + +} + + Since all path segments may contain matrix variables, in some cases + you need to be more specific to identify where the variable is expected to be: + + // GET /owners/42;q=11/pets/21;q=22 + +@RequestMapping(value = "/owners/{ownerId}/pets/{petId}", method = RequestMethod.GET) +public void findPet( + @MatrixVariable(value="q", pathVar="ownerId") int q1, + @MatrixVariable(value="q", pathVar="petId") int q2) { + + // q1 == 11 + // q2 == 22 + +} + + A matrix variable may be defined as optional and a default value specified: + + // GET /pets/42 + +@RequestMapping(value = "/pets/{petId}", method = RequestMethod.GET) + public void findPet(@MatrixVariable(required=true, defaultValue="1") int q) { + + // q == 1 + + } + + All matrix variables may be obtained in a Map: + + // GET /owners/42;q=11;r=12/pets/21;q=22;s=23 + +@RequestMapping(value = "/owners/{ownerId}/pets/{petId}", method = RequestMethod.GET) + public void findPet( + @MatrixVariable Map<String, String> matrixVars, + @MatrixVariable(pathVar="petId"") Map<String, String> petMatrixVars) { + + // matrixVars: ["q" : [11,22], "r" : 12, "s" : 23] + // petMatrixVars: ["q" : 11, "s" : 23] + + } + + Note that to enable the use of matrix variables, you must set the + removeSemicolonContent property of + RequestMappingHandlerMapping to false. + By default it is set to true with the exception of the + MVC namespace and the MVC Java config both of which automatically enable + the use of matrix variables. + +
    +
    Consumable Media Types @@ -1254,6 +1341,12 @@ public class RelativePathUriTemplateController { linkend="mvc-ann-requestmapping-uri-templates" />. + + @MatrixVariable annotated parameters + for access to name-value pairs located in URI path segments. + See . + + @RequestParam annotated parameters for access to specific Servlet request parameters. Parameter diff --git a/src/reference/docbook/new-in-3.2.xml b/src/reference/docbook/new-in-3.2.xml index 547b10bf1d..98bce34e74 100644 --- a/src/reference/docbook/new-in-3.2.xml +++ b/src/reference/docbook/new-in-3.2.xml @@ -80,6 +80,14 @@
    +
    + Matrix variables + + A new @MatrixVariable annotation + adds support for extracting matrix variables from the request URI. + For more details see . +
    +
    New <classname>ResponseEntityExceptionHandler</classname> class