From fe72e8a5f74339e2501507a776681ca1044449fc Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Mon, 17 Nov 2008 16:00:03 +0000 Subject: [PATCH] SPR-5251: URI Templates in @RequestMapping --- .../springframework/util/AntPathMatcher.java | 90 ++++++++++++++----- .../org/springframework/util/PathMatcher.java | 14 +++ ...herTests.java => AntPathMatcherTests.java} | 48 +++++++--- .../web/bind/annotation/PathVariable.java | 26 ++++++ .../web/bind/annotation/RequestMapping.java | 3 + .../support/HandlerMethodInvoker.java | 41 ++++++--- .../web/servlet/HandlerMapping.java | 10 +++ .../handler/AbstractUrlHandlerMapping.java | 65 +++++++++++--- .../AnnotationMethodHandlerAdapter.java | 36 +++++++- .../DefaultAnnotationHandlerMapping.java | 7 +- .../ServletAnnotationControllerTests.java | 37 +++++++- 11 files changed, 313 insertions(+), 64 deletions(-) rename org.springframework.core/src/test/java/org/springframework/util/{PathMatcherTests.java => AntPathMatcherTests.java} (93%) create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/PathVariable.java diff --git a/org.springframework.core/src/main/java/org/springframework/util/AntPathMatcher.java b/org.springframework.core/src/main/java/org/springframework/util/AntPathMatcher.java index 7566153e31..b98a44557f 100644 --- a/org.springframework.core/src/main/java/org/springframework/util/AntPathMatcher.java +++ b/org.springframework.core/src/main/java/org/springframework/util/AntPathMatcher.java @@ -16,6 +16,11 @@ package org.springframework.util; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + /** * PathMatcher implementation for Ant-style path patterns. * Examples are provided below. @@ -56,6 +61,10 @@ public class AntPathMatcher implements PathMatcher { /** Default path separator: "/" */ public static final String DEFAULT_PATH_SEPARATOR = "/"; + /** Captures URI template variable names. */ + private static final Pattern URI_TEMPLATE_NAMES_PATTERN = Pattern.compile("\\{([\\w-~_\\.]+?)\\}"); + + private String pathSeparator = DEFAULT_PATH_SEPARATOR; @@ -91,6 +100,7 @@ public class AntPathMatcher implements PathMatcher { * false if it didn't */ protected boolean doMatch(String pattern, String path, boolean fullMatch) { + pattern = uriTemplateToAntPattern(pattern); if (path.startsWith(this.pathSeparator) != pattern.startsWith(this.pathSeparator)) { return false; } @@ -119,14 +129,13 @@ public class AntPathMatcher implements PathMatcher { if (pathIdxStart > pathIdxEnd) { // Path is exhausted, only match if rest of pattern is * or **'s if (pattIdxStart > pattIdxEnd) { - return (pattern.endsWith(this.pathSeparator) ? - path.endsWith(this.pathSeparator) : !path.endsWith(this.pathSeparator)); + return (pattern.endsWith(this.pathSeparator) ? path.endsWith(this.pathSeparator) : + !path.endsWith(this.pathSeparator)); } if (!fullMatch) { return true; } - if (pattIdxStart == pattIdxEnd && pattDirs[pattIdxStart].equals("*") && - path.endsWith(this.pathSeparator)) { + if (pattIdxStart == pattIdxEnd && pattDirs[pattIdxStart].equals("*") && path.endsWith(this.pathSeparator)) { return true; } for (int i = pattIdxStart; i <= pattIdxEnd; i++) { @@ -187,17 +196,17 @@ public class AntPathMatcher implements PathMatcher { int foundIdx = -1; strLoop: - for (int i = 0; i <= strLength - patLength; i++) { - for (int j = 0; j < patLength; j++) { - String subPat = (String) pattDirs[pattIdxStart + j + 1]; - String subStr = (String) pathDirs[pathIdxStart + i + j]; - if (!matchStrings(subPat, subStr)) { - continue strLoop; - } - } - foundIdx = pathIdxStart + i; - break; - } + for (int i = 0; i <= strLength - patLength; i++) { + for (int j = 0; j < patLength; j++) { + String subPat = (String) pattDirs[pattIdxStart + j + 1]; + String subStr = (String) pathDirs[pathIdxStart + i + j]; + if (!matchStrings(subPat, subStr)) { + continue strLoop; + } + } + foundIdx = pathIdxStart + i; + break; + } if (foundIdx == -1) { return false; @@ -382,7 +391,7 @@ public class AntPathMatcher implements PathMatcher { String[] patternParts = StringUtils.tokenizeToStringArray(pattern, this.pathSeparator); String[] pathParts = StringUtils.tokenizeToStringArray(path, this.pathSeparator); - StringBuffer buffer = new StringBuffer(); + StringBuilder builder = new StringBuilder(); // Add any path parts that have a wildcarded pattern part. int puts = 0; @@ -390,9 +399,9 @@ public class AntPathMatcher implements PathMatcher { String patternPart = patternParts[i]; if ((patternPart.indexOf('*') > -1 || patternPart.indexOf('?') > -1) && pathParts.length >= i + 1) { if (puts > 0 || (i == 0 && !pattern.startsWith(this.pathSeparator))) { - buffer.append(this.pathSeparator); + builder.append(this.pathSeparator); } - buffer.append(pathParts[i]); + builder.append(pathParts[i]); puts++; } } @@ -400,12 +409,51 @@ public class AntPathMatcher implements PathMatcher { // Append any trailing path parts. for (int i = patternParts.length; i < pathParts.length; i++) { if (puts > 0 || i > 0) { - buffer.append(this.pathSeparator); + builder.append(this.pathSeparator); + } + builder.append(pathParts[i]); + } + + return builder.toString(); + } + + /** + * Replaces URI template variables with Ant-style pattern patchs. Looks for variables within curly braces, and replaces + * those with *. + * + *

For example: /hotels/{hotel}/bookings becomes + * /hotels/*/bookings + * + * @param pattern the pattern, possibly containing URI template variables + * @return the Ant-stlye pattern path + * @see org.springframework.util.AntPathMatcher + */ + private static String uriTemplateToAntPattern(String pattern) { + Matcher matcher = URI_TEMPLATE_NAMES_PATTERN.matcher(pattern); + return matcher.replaceAll("*"); + } + + + public Map extractUriTemplateVariables(String pattern, String path) { + if (pattern.contains("**") && pattern.contains("{")) { + throw new IllegalArgumentException("Combining '**' and URI templates is not allowed"); + } + String[] patternParts = StringUtils.tokenizeToStringArray(pattern, this.pathSeparator); + String[] pathParts = StringUtils.tokenizeToStringArray(path, this.pathSeparator); + + Map variables = new LinkedHashMap(); + + for (int i = 0; i < patternParts.length && i < pathParts.length; i++) { + String patternPart = patternParts[i]; + String pathPart = pathParts[i]; + int patternEnd = patternPart.length() -1 ; + if (patternEnd > 1 && patternPart.charAt(0) == '{' && patternPart.charAt(patternEnd) == '}') { + String varName = patternPart.substring(1, patternEnd); + variables.put(varName, pathPart); } - buffer.append(pathParts[i]); } - return buffer.toString(); + return variables; } } diff --git a/org.springframework.core/src/main/java/org/springframework/util/PathMatcher.java b/org.springframework.core/src/main/java/org/springframework/util/PathMatcher.java index b7671d2f87..4ebbf32176 100644 --- a/org.springframework.core/src/main/java/org/springframework/util/PathMatcher.java +++ b/org.springframework.core/src/main/java/org/springframework/util/PathMatcher.java @@ -16,6 +16,8 @@ package org.springframework.util; +import java.util.Map; + /** * Strategy interface for String-based path matching. * @@ -88,4 +90,16 @@ public interface PathMatcher { */ String extractPathWithinPattern(String pattern, String path); + /** + * Given a pattern and a full path, extract the URI template variables. URI template + * variables are expressed through curly brackets ('{' and '}'). + * + *

For example: For pattern "/hotels/{hotel}" and path "/hotels/1", this method will + * return a map containing "hotel"->"1". + * + * @param pattern the path pattern, possibly containing URI templates + * @param path the full path to extract template variables from + * @return a map, containing variable names as keys; variables values as values + */ + Map extractUriTemplateVariables(String pattern, String path); } diff --git a/org.springframework.core/src/test/java/org/springframework/util/PathMatcherTests.java b/org.springframework.core/src/test/java/org/springframework/util/AntPathMatcherTests.java similarity index 93% rename from org.springframework.core/src/test/java/org/springframework/util/PathMatcherTests.java rename to org.springframework.core/src/test/java/org/springframework/util/AntPathMatcherTests.java index 8ac4e00801..c855c66e1e 100644 --- a/org.springframework.core/src/test/java/org/springframework/util/PathMatcherTests.java +++ b/org.springframework.core/src/test/java/org/springframework/util/AntPathMatcherTests.java @@ -16,18 +16,31 @@ package org.springframework.util; -import junit.framework.TestCase; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.junit.Assert.*; +import org.junit.Before; +import org.junit.Test; /** * @author Alef Arendsen * @author Seth Ladd * @author Juergen Hoeller + * @author Arjen Poutsma */ -public class PathMatcherTests extends TestCase { +public class AntPathMatcherTests { + + private AntPathMatcher pathMatcher; - public void testAntPathMatcher() { - PathMatcher pathMatcher = new AntPathMatcher(); + @Before + public void createMatcher() { + pathMatcher = new AntPathMatcher(); + } + @Test + public void standard() { // test exact matching assertTrue(pathMatcher.match("test", "test")); assertTrue(pathMatcher.match("/test", "/test")); @@ -109,9 +122,8 @@ public class PathMatcherTests extends TestCase { assertTrue(pathMatcher.match("", "")); } - public void testAntPathMatcherWithMatchStart() { - PathMatcher pathMatcher = new AntPathMatcher(); - + @Test + public void withMatchStart() { // test exact matching assertTrue(pathMatcher.matchStart("test", "test")); assertTrue(pathMatcher.matchStart("/test", "/test")); @@ -197,8 +209,8 @@ public class PathMatcherTests extends TestCase { assertTrue(pathMatcher.matchStart("", "")); } - public void testAntPathMatcherWithUniqueDeliminator() { - AntPathMatcher pathMatcher = new AntPathMatcher(); + @Test + public void uniqueDeliminator() { pathMatcher.setPathSeparator("."); // test exact matching @@ -259,9 +271,8 @@ public class PathMatcherTests extends TestCase { assertFalse(pathMatcher.match(".*bla.test", "XXXbl.test")); } - public void testAntPathMatcherExtractPathWithinPattern() throws Exception { - PathMatcher pathMatcher = new AntPathMatcher(); - + @Test + public void extractPathWithinPattern() throws Exception { assertEquals("", pathMatcher.extractPathWithinPattern("/docs/commit.html", "/docs/commit.html")); assertEquals("cvs/commit", pathMatcher.extractPathWithinPattern("/docs/*", "/docs/cvs/commit")); @@ -282,4 +293,17 @@ public class PathMatcherTests extends TestCase { assertEquals("docs/cvs/commit.html", pathMatcher.extractPathWithinPattern("/d?cs/**/*.html", "/docs/cvs/commit.html")); } + @Test + public void extractUriTemplateVariables() throws Exception { + Map result = pathMatcher.extractUriTemplateVariables("/hotels/{hotel}", "/hotels/1"); + assertEquals(Collections.singletonMap("hotel", "1"), result); + + result = pathMatcher.extractUriTemplateVariables("/hotels/{hotel}/bookings/{booking}", "/hotels/1/bookings/2"); + Map expected = new LinkedHashMap(); + expected.put("hotel", "1"); + expected.put("booking", "2"); + assertEquals(expected, result); + } + + } diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/PathVariable.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/PathVariable.java new file mode 100644 index 0000000000..9fff5eff2e --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/PathVariable.java @@ -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 ""; + +} diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java index 75ed5560ee..cd80754cf6 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java @@ -72,6 +72,9 @@ import java.lang.annotation.Target; *

  • {@link RequestParam @RequestParam} annotated parameters for access to * specific Servlet/Portlet request parameters. Parameter values will be * converted to the declared method argument type. + *
  • {@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. *
  • {@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. diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvoker.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvoker.java index 8e50de38d2..45a781c2a9 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvoker.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvoker.java @@ -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, diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/HandlerMapping.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/HandlerMapping.java index 6458beea6d..a447cd8b2f 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/HandlerMapping.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/HandlerMapping.java @@ -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. + *

    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 diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/AbstractUrlHandlerMapping.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/AbstractUrlHandlerMapping.java index b594b856f6..d3e32e27d4 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/AbstractUrlHandlerMapping.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/handler/AbstractUrlHandlerMapping.java @@ -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 handlerMap = new LinkedHashMap(); /** @@ -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 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. *

    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 null if no variables found * @return the final handler object */ - protected Object buildPathExposingHandler(Object rawHandler, String pathWithinMapping) { + protected Object buildPathExposingHandler(Object rawHandler, + String pathWithinMapping, + Map 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 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 uriTemplateVariables; + + private UriTemplateVariablesHandlerInterceptor(Map uriTemplateVariables) { + this.uriTemplateVariables = uriTemplateVariables; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + exposeUriTemplateVariables(this.uriTemplateVariables, request); + return true; + } + } + } diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java index 37670bea4f..d7dcd32747 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java @@ -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 uriTemplateVariables = + (Map) 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 { diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/DefaultAnnotationHandlerMapping.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/DefaultAnnotationHandlerMapping.java index 3dbaf590cb..64c77b456e 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/DefaultAnnotationHandlerMapping.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/DefaultAnnotationHandlerMapping.java @@ -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 } } + + } diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationControllerTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationControllerTests.java index 2dae75fb3d..bcf535702f 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationControllerTests.java +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationControllerTests.java @@ -40,6 +40,7 @@ import org.junit.Test; import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.aop.interceptor.SimpleTraceInterceptor; import org.springframework.aop.support.DefaultPointcutAdvisor; +import org.springframework.beans.BeansException; import org.springframework.beans.DerivedTestBean; import org.springframework.beans.ITestBean; import org.springframework.beans.TestBean; @@ -63,6 +64,7 @@ import org.springframework.web.bind.MissingServletRequestParameterException; 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.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; @@ -796,6 +798,29 @@ public class ServletAnnotationControllerTests { } } + @Test + public void uriTemplates() throws Exception { + DispatcherServlet servlet = new DispatcherServlet() { + @Override + protected WebApplicationContext createWebApplicationContext(WebApplicationContext parent) throws BeansException { + GenericWebApplicationContext wac = new GenericWebApplicationContext(); + wac.registerBeanDefinition("controller", new RootBeanDefinition(UriTemplateController.class)); + wac.refresh(); + return wac; + } + }; + servlet.init(new MockServletConfig()); + + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels/42/bookings/21"); + MockHttpServletResponse response = new MockHttpServletResponse(); + servlet.service(request, response); + assertEquals("test-42-21", response.getContentAsString()); + } + + /* + * Controllers + */ + @RequestMapping("/myPath.do") private static class MyController extends AbstractController { @@ -1285,7 +1310,6 @@ public class ServletAnnotationControllerTests { @Controller public static class MethodNotAllowedController { - @RequestMapping(value="/myPath.do", method = RequestMethod.DELETE) public void delete() { } @@ -1314,4 +1338,15 @@ public class ServletAnnotationControllerTests { } } + @Controller + public static class UriTemplateController { + + @RequestMapping("/hotels/{hotel}/bookings/{booking}") + public void handle(@PathVariable("hotel") int hotel, @PathVariable int booking, HttpServletResponse response) + throws IOException { + response.getWriter().write("test-" + hotel + "-" + booking); + } + + } + }