SPR-5251: URI Templates in @RequestMapping

This commit is contained in:
Arjen Poutsma
2008-11-17 16:00:03 +00:00
parent a1faaad9fa
commit fe72e8a5f7
11 changed files with 313 additions and 64 deletions

View File

@@ -0,0 +1,26 @@
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 URI template variable. Supported for {@link
* RequestMapping} annotated handler methods in Servlet environments.
*
* @author Arjen Poutsma
* @see RequestMapping
* @see org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter
* @since 3.0
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PathVariable {
/** The URI template variable to bind to. */
String value() default "";
}

View File

@@ -72,6 +72,9 @@ import java.lang.annotation.Target;
* <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.
* <li>{@link PathVariable @PathVariable} annotated parameters for acces to
* URI template values (i.e. /hotels/{hotel}). Variable values will be
* converted to the declared method argument type.
* <li>{@link java.util.Map} / {@link org.springframework.ui.Model} /
* {@link org.springframework.ui.ModelMap} for enriching the implicit model
* that will be exposed to the web view.

View File

@@ -44,6 +44,7 @@ import org.springframework.validation.Errors;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.support.DefaultSessionAttributeStore;
import org.springframework.web.bind.support.SessionAttributeStore;
@@ -158,11 +159,11 @@ public class HandlerMethodInvoker {
String paramName = null;
boolean paramRequired = false;
String paramDefaultValue = null;
String pathVarName = null;
String attrName = null;
Object[] paramAnns = methodParam.getParameterAnnotations();
for (int j = 0; j < paramAnns.length; j++) {
Object paramAnn = paramAnns[j];
for (Object paramAnn : paramAnns) {
if (RequestParam.class.isInstance(paramAnn)) {
RequestParam requestParam = (RequestParam) paramAnn;
paramName = requestParam.value();
@@ -173,21 +174,24 @@ public class HandlerMethodInvoker {
else if (ModelAttribute.class.isInstance(paramAnn)) {
ModelAttribute attr = (ModelAttribute) paramAnn;
attrName = attr.value();
} else if (PathVariable.class.isInstance(paramAnn)) {
PathVariable pathVar = (PathVariable) paramAnn;
pathVarName = pathVar.value();
}
}
if (paramName != null && attrName != null) {
throw new IllegalStateException("@RequestParam and @ModelAttribute are an exclusive choice -" +
"do not specify both on the same parameter: " + handlerMethod);
if ((paramName != null && attrName != null) || (paramName != null && pathVarName != null) ||
(pathVarName != null && attrName != null)) {
throw new IllegalStateException("@RequestParam, @PathVariable and @ModelAttribute are exclusive " +
"choices - do not specify both on the same parameter: " + handlerMethod);
}
Class paramType = methodParam.getParameterType();
if (paramName == null && attrName == null) {
if (paramName == null && attrName == null && pathVarName == null) {
Object argValue = resolveCommonArgument(methodParam, webRequest);
if (argValue != WebArgumentResolver.UNRESOLVED) {
args[i] = argValue;
}
else {
Class paramType = methodParam.getParameterType();
if (Model.class.isAssignableFrom(paramType) || Map.class.isAssignableFrom(paramType)) {
args[i] = implicitModel;
}
@@ -223,13 +227,15 @@ public class HandlerMethodInvoker {
i++;
}
implicitModel.putAll(binder.getBindingResult().getModel());
} else if (pathVarName != null) {
args[i] = resolvePathVariable(pathVarName, methodParam, webRequest, handler);
}
}
return args;
}
private void initBinder(Object handler, String attrName, WebDataBinder binder, NativeWebRequest webRequest)
protected void initBinder(Object handler, String attrName, WebDataBinder binder, NativeWebRequest webRequest)
throws Exception {
if (this.bindingInitializer != null) {
@@ -276,8 +282,7 @@ public class HandlerMethodInvoker {
String paramDefaultValue = null;
Object[] paramAnns = methodParam.getParameterAnnotations();
for (int j = 0; j < paramAnns.length; j++) {
Object paramAnn = paramAnns[j];
for (Object paramAnn : paramAnns) {
if (RequestParam.class.isInstance(paramAnn)) {
RequestParam requestParam = (RequestParam) paramAnn;
paramName = requestParam.value();
@@ -328,7 +333,7 @@ public class HandlerMethodInvoker {
Object handlerForInitBinderCall) throws Exception {
Class paramType = methodParam.getParameterType();
if ("".equals(paramName)) {
if (paramName.length() == 0) {
paramName = methodParam.getParameterName();
if (paramName == null) {
throw new IllegalStateException("No parameter specified for @RequestParam argument of type [" +
@@ -393,6 +398,18 @@ public class HandlerMethodInvoker {
return binder;
}
/**
* Resolves the given {@link org.springframework.web.bind.annotation.PathVariable @PathVariable} variable. Overriden in
* {@link org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter.ServletHandlerMethodInvoker},
* throws an UnsupportedOperationException by default.
*/
protected Object resolvePathVariable(String pathVarName,
MethodParameter methodParam,
NativeWebRequest webRequest,
Object handlerForInitBinderCall) throws Exception {
throw new UnsupportedOperationException("@PathVariable not supported");
}
@SuppressWarnings("unchecked")
public final void updateModelAttributes(Object handler,
Map mavModel,

View File

@@ -64,6 +64,16 @@ public interface HandlerMapping {
*/
String PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE = HandlerMapping.class.getName() + ".pathWithinHandlerMapping";
/**
* Name of the {@link HttpServletRequest} attribute that contains the URI
* templates map, mapping variable names to values.
* <p>Note: This attribute is not required to be supported by all
* HandlerMapping implementations. URL-based HandlerMappings will
* typically support it, but handlers should not necessarily expect
* this request attribute to be present in all scenarios.
*/
String URI_TEMPLATE_VARIABLES_ATTRIBUTE = HandlerMapping.class.getName() + ".uriTemplateVariables";
/**
* Return a handler and any interceptors for this request. The choice may be made

View File

@@ -17,7 +17,6 @@
package org.springframework.web.servlet.handler;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
@@ -26,6 +25,7 @@ import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.BeansException;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.PathMatcher;
import org.springframework.web.servlet.HandlerExecutionChain;
import org.springframework.web.servlet.HandlerMapping;
@@ -47,6 +47,7 @@ import org.springframework.web.util.UrlPathHelper;
* path pattern that matches the current request path.
*
* @author Juergen Hoeller
* @author Arjen Poutsma
* @since 16.04.2003
* @see #setAlwaysUseFullPath
* @see #setUrlDecode
@@ -62,7 +63,7 @@ public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping {
private boolean lazyInitHandlers = false;
private final Map handlerMap = new LinkedHashMap();
private final Map<String, Object> handlerMap = new LinkedHashMap<String, Object>();
/**
@@ -170,7 +171,7 @@ public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping {
}
if (rawHandler != null) {
validateHandler(rawHandler, request);
handler = buildPathExposingHandler(rawHandler, lookupPath);
handler = buildPathExposingHandler(rawHandler, lookupPath, null);
}
}
if (handler != null && logger.isDebugEnabled()) {
@@ -200,12 +201,11 @@ public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping {
Object handler = this.handlerMap.get(urlPath);
if (handler != null) {
validateHandler(handler, request);
return buildPathExposingHandler(handler, urlPath);
return buildPathExposingHandler(handler, urlPath, null);
}
// Pattern match?
String bestPathMatch = null;
for (Iterator it = this.handlerMap.keySet().iterator(); it.hasNext();) {
String registeredPath = (String) it.next();
for (String registeredPath : this.handlerMap.keySet()) {
if (getPathMatcher().match(registeredPath, urlPath) &&
(bestPathMatch == null || bestPathMatch.length() < registeredPath.length())) {
bestPathMatch = registeredPath;
@@ -215,7 +215,9 @@ public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping {
handler = this.handlerMap.get(bestPathMatch);
validateHandler(handler, request);
String pathWithinMapping = getPathMatcher().extractPathWithinPattern(bestPathMatch, urlPath);
return buildPathExposingHandler(handler, pathWithinMapping);
Map<String, String> uriTemplateVariables =
getPathMatcher().extractUriTemplateVariables(bestPathMatch, urlPath);
return buildPathExposingHandler(handler, pathWithinMapping, uriTemplateVariables);
}
// No handler found...
return null;
@@ -234,15 +236,18 @@ public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping {
/**
* Build a handler object for the given raw handler, exposing the actual
* handler as well as the {@link #PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE}
* before executing the handler.
* handler, the {@link #PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE}, as well as
* the {@link #URI_TEMPLATE_VARIABLES_ATTRIBUTE} before executing the handler.
* <p>The default implementation builds a {@link HandlerExecutionChain}
* with a special interceptor that exposes the path attribute.
* with a special interceptor that exposes the path attribute and uri template variables
* @param rawHandler the raw handler to expose
* @param pathWithinMapping the path to expose before executing the handler
* @param uriTemplateVariables the URI template variables, can be <code>null</code> if no variables found
* @return the final handler object
*/
protected Object buildPathExposingHandler(Object rawHandler, String pathWithinMapping) {
protected Object buildPathExposingHandler(Object rawHandler,
String pathWithinMapping,
Map<String, String> uriTemplateVariables) {
// Bean name or resolved handler?
if (rawHandler instanceof String) {
String handlerName = (String) rawHandler;
@@ -250,6 +255,9 @@ public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping {
}
HandlerExecutionChain chain = new HandlerExecutionChain(rawHandler);
chain.addInterceptor(new PathExposingHandlerInterceptor(pathWithinMapping));
if (!CollectionUtils.isEmpty(uriTemplateVariables)) {
chain.addInterceptor(new UriTemplateVariablesHandlerInterceptor(uriTemplateVariables));
}
return chain;
}
@@ -263,6 +271,15 @@ public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping {
request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, pathWithinMapping);
}
/**
* Expose the URI templates variables as request attribute.
* @param uriTemplateVariables the URI template variables
* @param request the request to expose the path to
* @see #PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE
*/
protected void exposeUriTemplateVariables(Map<String, String> uriTemplateVariables, HttpServletRequest request) {
request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVariables);
}
/**
* Register the specified handler for the given URL paths.
@@ -273,8 +290,8 @@ public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping {
*/
protected void registerHandler(String[] urlPaths, String beanName) throws BeansException, IllegalStateException {
Assert.notNull(urlPaths, "URL path array must not be null");
for (int j = 0; j < urlPaths.length; j++) {
registerHandler(urlPaths[j], beanName);
for (String urlPath : urlPaths) {
registerHandler(urlPath, beanName);
}
}
@@ -350,7 +367,7 @@ public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping {
private final String pathWithinMapping;
public PathExposingHandlerInterceptor(String pathWithinMapping) {
private PathExposingHandlerInterceptor(String pathWithinMapping) {
this.pathWithinMapping = pathWithinMapping;
}
@@ -361,4 +378,24 @@ public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping {
}
}
/**
* Special interceptor for exposing the
* {@link AbstractUrlHandlerMapping#URI_TEMPLATE_VARIABLES_ATTRIBUTE} attribute.
* @link AbstractUrlHandlerMapping#exposePathWithinMapping
*/
private class UriTemplateVariablesHandlerInterceptor extends HandlerInterceptorAdapter {
private final Map<String, String> uriTemplateVariables;
private UriTemplateVariablesHandlerInterceptor(Map<String, String> uriTemplateVariables) {
this.uriTemplateVariables = uriTemplateVariables;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
exposeUriTemplateVariables(this.uriTemplateVariables, request);
return true;
}
}
}

View File

@@ -74,6 +74,7 @@ import org.springframework.web.bind.support.WebBindingInitializer;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.servlet.HandlerAdapter;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.mvc.multiaction.InternalPathMethodNameResolver;
@@ -420,9 +421,12 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen
}
/**
* Servlet-specific subclass of {@link HandlerMethodResolver}.
*/
private class ServletHandlerMethodResolver extends HandlerMethodResolver {
public ServletHandlerMethodResolver(Class<?> handlerType) {
private ServletHandlerMethodResolver(Class<?> handlerType) {
super(handlerType);
}
@@ -569,6 +573,9 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen
}
/**
* Servlet-specific subclass of {@link HandlerMethodInvoker}.
*/
private class ServletHandlerMethodInvoker extends HandlerMethodInvoker {
private boolean responseArgumentUsed = false;
@@ -607,6 +614,33 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen
}
}
@Override
@SuppressWarnings({"unchecked"})
protected Object resolvePathVariable(String pathVarName,
MethodParameter methodParam,
NativeWebRequest webRequest,
Object handlerForInitBinderCall) throws Exception {
Class paramType = methodParam.getParameterType();
if (pathVarName.length() == 0) {
pathVarName = methodParam.getParameterName();
if (pathVarName == null) {
throw new IllegalStateException("No variable name specified for @PathVariable argument of type [" +
paramType.getName() + "], and no parameter name information found in class file either.");
}
}
HttpServletRequest servletRequest = (HttpServletRequest) webRequest.getNativeRequest();
Map<String, String> uriTemplateVariables =
(Map<String, String>) servletRequest.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
if (uriTemplateVariables == null || !uriTemplateVariables.containsKey(pathVarName)) {
throw new IllegalStateException("Could not find @PathVariable [" + pathVarName + "] in @RequestMapping");
}
String pathVarValue = uriTemplateVariables.get(pathVarName);
WebDataBinder binder = createBinder(webRequest, null, pathVarName);
initBinder(handlerForInitBinderCall, pathVarName, binder, webRequest);
return binder.convertIfNecessary(pathVarValue, paramType, methodParam);
}
@Override
protected Object resolveStandardArgument(Class parameterType, NativeWebRequest webRequest)
throws Exception {

View File

@@ -21,7 +21,6 @@ import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
@@ -152,8 +151,8 @@ public class DefaultAnnotationHandlerMapping extends AbstractDetectingUrlHandler
RequestMapping mapping = method.getAnnotation(RequestMapping.class);
if (mapping != null) {
String[] mappedPaths = mapping.value();
for (int i = 0; i < mappedPaths.length; i++) {
addUrlsForPath(urls, mappedPaths[i]);
for (String mappedPath : mappedPaths) {
addUrlsForPath(urls, mappedPath);
}
}
}
@@ -214,4 +213,6 @@ public class DefaultAnnotationHandlerMapping extends AbstractDetectingUrlHandler
}
}
}