提交 2201dd8c 编写于 作者: R Rossen Stoyanchev

Add support for matrix variables

A new @MatrixVariable annotation allows injecting matrix variables
into @RequestMapping methods. The matrix variables may appear in any
path segment and should be wrapped in a URI template for request
mapping purposes to ensure request matching is not affected by the
order or the presence/absence of such variables. The @MatrixVariable
annotation has an optional "pathVar" attribute that can be used to
refer to the URI template where a matrix variable is located.

Previously, ";" (semicolon) delimited content was removed from the
path used for request mapping purposes. To preserve backwards
compatibility that continues to be the case (except for the MVC
namespace and Java config) and may be changed by setting the
"removeSemicolonContent" property of RequestMappingHandlerMapping to
"false". Applications using the  MVC namespace and Java config do not
need to do anything further to extract and use matrix variables.

Issue: SPR-5499, SPR-7818
上级 da05b094
/*
* 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.
* <p>Default is <code>true</code>, leading to an exception thrown in case
* of the variable missing in the request. Switch this to <code>false</code>
* if you prefer a <code>null</value> in case of the variable missing.
* <p>Alternatively, provide a {@link #defaultValue() defaultValue},
* which implicitly sets this flag to <code>false</code>.
*/
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;
}
......@@ -82,6 +82,13 @@ import java.util.concurrent.Callable;
* Additionally, {@code @PathVariable} can be used on a
* {@link java.util.Map Map&lt;String, String&gt;} to gain access to all
* URI template variables.
* <li>{@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&lt;String, String&gt;} to gain access to all
* matrix variables in the URL or to those in a specific path variable.
* <li>{@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,
......
......@@ -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.
* <p>Subclasses define how to do the following:
* <ul>
* <li>Obtain named value information for a method parameter
......@@ -44,10 +45,11 @@ import org.springframework.web.method.support.ModelAndViewContainer;
* <li>Handle missing argument values when argument values are required
* <li>Optionally handle a resolved value
* </ul>
* <p>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.
* <p>A {@link WebDataBinder} is created to apply type conversion to the resolved argument value if it doesn't
* match the method parameter type.
* <p>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.
* <p>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<MethodParameter, NamedValueInfo>();
/**
* @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;
......
......@@ -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.
* <p>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
......
......@@ -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<String, String> parseMatrixVariables(String matrixVariables) {
MultiValueMap<String, String> result = new LinkedMultiValueMap<String, String>();
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;
}
}
......@@ -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");
......
......@@ -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<String, String> 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"));
}
}
......@@ -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.
* <p>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.
* <p>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.
* <p>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";
......
......@@ -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);
......
......@@ -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;
......
......@@ -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.
*
* <p>Note: This base class does <i>not</i> support exposure of the
......@@ -72,6 +72,7 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport
private final List<MappedInterceptor> mappedInterceptors = new ArrayList<MappedInterceptor>();
/**
* Specify the order value for this HandlerMapping bean.
* <p>Default value is <code>Integer.MAX_VALUE</code>, 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.
* <p>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.
* <p>Supported interceptor types are HandlerInterceptor, WebRequestInterceptor, and MappedInterceptor.
* <p>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 <code>null</code> 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<MappedInterceptor> 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.
* <p>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;
}
......
......@@ -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.
* <p>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
......
......@@ -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<String, String> vars = getPathMatcher().extractUriTemplateVariables(bestPattern, lookupPath);
Map<String, String> decodedVars = getUrlPathHelper().decodePathVariables(request, vars);
request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, decodedVars);
Map<String, String> uriVariables = getPathMatcher().extractUriTemplateVariables(bestPattern, lookupPath);
Map<String, String> 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<MediaType> mediaTypes = info.getProducesCondition().getProducibleMediaTypes();
......@@ -99,6 +109,36 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe
}
}
private boolean isMatrixVariableContentAvailable() {
return !getUrlPathHelper().shouldRemoveSemicolonContent();
}
private Map<String, MultiValueMap<String, String>> extractMatrixVariables(Map<String, String> uriVariables) {
Map<String, MultiValueMap<String, String>> result = new LinkedHashMap<String, MultiValueMap<String, String>>();
for (Entry<String, String> 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.
......
/*
* 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<String> contentNegotiationFileExtensions = new ArrayList<String>();
/**
* Whether to use suffix pattern match (".*") when matching patterns to
* requests. If enabled a method mapped to "/users" also matches to "/users.*".
* <p>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/".
* <p>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<String> 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.
*
* <p>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.
*
* <p>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);
}
}
/*
* 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<String, MultiValueMap<String, String>> matrixVariables =
(Map<String, MultiValueMap<String, String>>) 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<String, String> map = matrixVariables.get(pathVariable);
return (map != null) ? map : Collections.emptyMap();
}
MultiValueMap<String, String> map = new LinkedMultiValueMap<String, String>();
for (MultiValueMap<String, String> 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
/*
* 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}.
*
* <p>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<String, MultiValueMap<String, String>> pathParameters =
(Map<String, MultiValueMap<String, String>>) request.getAttribute(
HandlerMapping.MATRIX_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
if (CollectionUtils.isEmpty(pathParameters)) {
return null;
}
String pathVar = parameter.getParameterAnnotation(MatrixVariable.class).pathVar();
List<String> 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<String>();
for (MultiValueMap<String, String> 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
......@@ -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<String, Object> pathVars = (Map<String, Object>) request.getAttribute(key, scope);
......
......@@ -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
......
......@@ -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.
......
......@@ -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));
......
......@@ -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_");
......
......@@ -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<String> paths = this.mapping.getMappingPathPatterns(info);
Set<String> paths = this.handlerMapping.getMappingPathPatterns(info);
HashSet<String> expected = new HashSet<String>(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<String, String> 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<String, String> 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<String, String> matrixVariables;
Map<String, String> 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<String, String> getMatrixVariables(HttpServletRequest request, String uriVarName) {
String attrName = HandlerMapping.MATRIX_VARIABLES_ATTRIBUTE;
return ((Map<String, MultiValueMap<String, String>>) request.getAttribute(attrName)).get(uriVarName);
}
@SuppressWarnings("unchecked")
private Map<String, String> getUriTemplateVariables(HttpServletRequest request) {
String attrName = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE;
return (Map<String, String>) request.getAttribute(attrName);
}
@Controller
private static class Handler {
......
......@@ -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:
* <ul>
* <li>RequestMappingHandlerMapping
* <li>RequestMappingHandlerAdapter
* <li>RequestMappingHandlerMapping
* <li>RequestMappingHandlerAdapter
* <li>ExceptionHandlerExceptionResolver
* </ul>
*
*
* @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<GenericWebApplicationContext> initializer,
final ApplicationContextInitializer<GenericWebApplicationContext> 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;
}
......
/*
* 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<String, MultiValueMap<String, String>> params = new LinkedHashMap<String, MultiValueMap<String, String>>();
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<String, String> params = getMatrixVariables("cars");
params.add("colors", "red");
params.add("colors", "green");
params.add("colors", "blue");
params.add("year", "2012");
@SuppressWarnings("unchecked")
Map<String, String> map = (Map<String, String>) this.resolver.resolveArgument(
this.paramMap, this.mavContainer, this.webRequest, null);
assertEquals(Arrays.asList("red", "green", "blue"), map.get("colors"));
@SuppressWarnings("unchecked")
MultiValueMap<String, String> multivalueMap = (MultiValueMap<String, String>) 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<String, String> params1 = getMatrixVariables("cars");
params1.add("colors", "red");
params1.add("colors", "purple");
MultiValueMap<String, String> params2 = getMatrixVariables("planes");
params2.add("colors", "yellow");
params2.add("colors", "orange");
@SuppressWarnings("unchecked")
Map<String, String> mapForPathVar = (Map<String, String>) this.resolver.resolveArgument(
this.paramMapForPathVar, this.mavContainer, this.webRequest, null);
assertEquals(Arrays.asList("red", "purple"), mapForPathVar.get("colors"));
@SuppressWarnings("unchecked")
Map<String, String> mapAll = (Map<String, String>) 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<String, String> map = (Map<String, String>) this.resolver.resolveArgument(
this.paramMap, this.mavContainer, this.webRequest, null);
assertEquals(Collections.emptyMap(), map);
}
@Test
public void resolveArgumentNoMatch() throws Exception {
MultiValueMap<String, String> params2 = getMatrixVariables("planes");
params2.add("colors", "yellow");
params2.add("colors", "orange");
@SuppressWarnings("unchecked")
Map<String, String> map = (Map<String, String>) this.resolver.resolveArgument(
this.paramMapForPathVar, this.mavContainer, this.webRequest, null);
assertEquals(Collections.emptyMap(), map);
}
@SuppressWarnings("unchecked")
private MultiValueMap<String, String> getMatrixVariables(String pathVarName) {
Map<String, MultiValueMap<String, String>> matrixVariables =
(Map<String, MultiValueMap<String, String>>) this.request.getAttribute(
HandlerMapping.MATRIX_VARIABLES_ATTRIBUTE);
MultiValueMap<String, String> params = new LinkedMultiValueMap<String, String>();
matrixVariables.put(pathVarName, params);
return params;
}
public void handle(
String stringArg,
@MatrixVariable Map<String, String> map,
@MatrixVariable MultiValueMap<String, String> multivalueMap,
@MatrixVariable(pathVar="cars") MultiValueMap<String, String> mapForPathVar,
@MatrixVariable("name") Map<String, String> mapWithName) {
}
}
\ No newline at end of file
/*
* 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<String, MultiValueMap<String, String>> params = new LinkedHashMap<String, MultiValueMap<String, String>>();
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<String, String> 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<String, String> params = getMatrixVariables("cars");
params.add("anotherYear", "2012");
assertEquals("2013", this.resolver.resolveArgument(this.paramYear, this.mavContainer, this.webRequest, null));
}
@SuppressWarnings("unchecked")
private MultiValueMap<String, String> getMatrixVariables(String pathVarName) {
Map<String, MultiValueMap<String, String>> matrixVariables =
(Map<String, MultiValueMap<String, String>>) this.request.getAttribute(
HandlerMapping.MATRIX_VARIABLES_ATTRIBUTE);
MultiValueMap<String, String> params = new LinkedMultiValueMap<String, String>();
matrixVariables.put(pathVarName, params);
return params;
}
public void handle(
String stringArg,
@MatrixVariable List<String> colors,
@MatrixVariable(value="year", pathVar="cars", required=false, defaultValue="2013") int preferredYear) {
}
}
\ No newline at end of file
......@@ -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:
* <ul>
* <li>RequestMappingHandlerMapping
* <li>RequestMappingHandlerAdapter
* <li>RequestMappingHandlerMapping
* <li>RequestMappingHandlerAdapter
* <li>ExceptionHandlerExceptionResolver
* </ul>
*
*
* <p>Rather than against the existing infrastructure:
* <ul>
* <li>DefaultAnnotationHandlerMapping
* <li>AnnotationMethodHandlerAdapter
* <li>AnnotationMethodHandlerExceptionResolver
* </ul>
*
* </ul>
*
* @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<GenericWebApplicationContext>() {
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<String, String> 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 {
@RequestMapping("/{root:\\d+}{params}")
public void handle(@PathVariable("root") int root, @PathVariable("params") String paramString,
@MatrixVariable List<Integer> q, Writer writer) throws IOException {
assertEquals("Invalid path variable value", 42, root);
writer.write("test-" + 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<String, Object> attrsToValidate;
int validatedAttrCount;
public ModelValidatingViewResolver(Map<String, Object> 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 {
}
......@@ -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_";
......
......@@ -1054,6 +1054,93 @@ public class RelativePathUriTemplateController {
<filename>/owners/*/pets/{petId}</filename>).</para>
</section>
<section id="mvc-ann-matrix-variables">
<title>Matrix Variables</title>
<para>The URI specification
<ulink url="http://tools.ietf.org/html/rfc3986#section-3.3">RFC 3986</ulink>
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
<ulink url="http://www.w3.org/DesignIssues/MatrixURIs.html">"Matrix URIs"</ulink>,
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.</para>
<para>Matrix variables can appear in any path segment, each matrix variable
separated with a ";" (semicolon).
For example: <code>"/cars;color=red;year=2012"</code>.
Multiple values may be either "," (comma) separated
<code>"color=red,green,blue"</code> or the variable name may be repeated
<code>"color=red;color=green;color=blue"</code>.</para>
<para>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.</para>
<para>Below is an example of extracting the matrix variable "q":</para>
<programlisting language="java">// 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
}</programlisting>
<para>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:</para>
<programlisting language="java">// 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
}</programlisting>
<para>A matrix variable may be defined as optional and a default value specified:</para>
<programlisting language="java">// GET /pets/42
@RequestMapping(value = "/pets/{petId}", method = RequestMethod.GET)
public void findPet(@MatrixVariable(required=true, defaultValue="1") int q) {
// q == 1
}</programlisting>
<para>All matrix variables may be obtained in a Map:</para>
<programlisting language="java">// 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&lt;String, String&gt; matrixVars,
@MatrixVariable(pathVar="petId"") Map&lt;String, String&gt; petMatrixVars) {
// matrixVars: ["q" : [11,22], "r" : 12, "s" : 23]
// petMatrixVars: ["q" : 11, "s" : 23]
}</programlisting>
<para>Note that to enable the use of matrix variables, you must set the
<classname>removeSemicolonContent</classname> property of
<classname>RequestMappingHandlerMapping</classname> to <code>false</code>.
By default it is set to <code>true</code> with the exception of the
MVC namespace and the MVC Java config both of which automatically enable
the use of matrix variables.</para>
</section>
<section id="mvc-ann-requestmapping-consumes">
<title>Consumable Media Types</title>
......@@ -1254,6 +1341,12 @@ public class RelativePathUriTemplateController {
linkend="mvc-ann-requestmapping-uri-templates" />.</para>
</listitem>
<listitem>
<para><classname>@MatrixVariable</classname> annotated parameters
for access to name-value pairs located in URI path segments.
See <xref linkend="mvc-ann-matrix-variables" />.</para>
</listitem>
<listitem>
<para><classname>@RequestParam</classname> annotated parameters
for access to specific Servlet request parameters. Parameter
......
......@@ -80,6 +80,14 @@
</section>
<section id="new-in-3.2-matrix-variables">
<title>Matrix variables</title>
<para>A new <interfacename>@MatrixVariable</interfacename> annotation
adds support for extracting matrix variables from the request URI.
For more details see <xref linkend="mvc-ann-matrix-variables"/>.</para>
</section>
<section id="new-in-3.2-webmvc-exception-handler-support">
<title>New <classname>ResponseEntityExceptionHandler</classname> class</title>
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册