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: *

    - *

    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: *

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

    Rather than against the existing infrastructure: *

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