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 ResponseEntityExceptionHandler class