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
This commit is contained in:
Rossen Stoyanchev
2012-08-26 16:22:37 -04:00
parent da05b094f5
commit 2201dd8c45
29 changed files with 1392 additions and 116 deletions

View File

@@ -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.
* <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;
}

View File

@@ -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,

View File

@@ -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;

View File

@@ -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

View File

@@ -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;
}
}