« Using UrlRewriteFilter with the Spring Framework | Main | Dojo drag and drop gotcha in IE »
June 11, 2007
Parameterized REST URLs with Spring MVC
At Carbon Five, we've been working REST-ful practices into our web applications for some time now. Providing simple URLs for application entities is a key principal of this style, but parsing parameters out of the request path has been klunky in Spring MVC. Spring's WebFlow apparently supports REST-ful URLs, but I've never found anything in what I've read or heard to recommended that project (though I've heard nothing bad).
I finally got fed up with the situation and worked out a solution that lets developers specify path parameters in the dispatcher mappings, which will appear as request parameters in the controller and view. I've made the project available on our public SVN server. The solution requires only 4 classes, so you can download them instead. Read on for configuration information.
ParameterizedUrlHandlerMapping
The solution is to replace Spring MVC's SimpleUrlHandlerMapping with ParameterizedUrlHandlerMapping. This class is responsible for routing requests to the appropriate handler (servlet, controller, or JSP), so it's an ideal place to specify the parameters. Of course, at this point, it has no more access to the request than a controller, so the best it can do is add the path parameters to the request attributes. It adds them as a Map of String name/value pairs with the key "ParameterizedUrlHandlerMapping.path-parameters".
To use ParameterizedUrlHandlerMapping, replace the SimpleUrlHandlerMapping bean in the dispatcher configuration file with something that looks like this:
<bean class="carbonfive.spring.web.pathparameter.ParameterizedUrlHandlerMapping">
<property name="alwaysUseFullPath" value="true"/>
<property name="mappings">
<props>
<prop key="/view/noparameters">controller1</prop>
<prop key="/view/(bar:foo)">controller2</prop>
<prop key="/view/(*.html:html)">controller3</prop>
<prop key="/view/(**/*:view).view">controller4</prop>
<prop key="/view/c/(*:controller)/(*:id)">controller5</prop>
</props>
</property>
</bean>
The ParameterizedUrlHandlerMapping supports all mappings that are valid using SimpleUrlHandlerMapping's default AntPathMatcher. The special parenthetical sections of the patterns have the syntax:
'(' + [ant_style_path] + ':' + [parameter_name] + ')'
Any part of the path pattern can be within parenthesis. The above example will having the following effect:
| path | controller | parameters |
|---|---|---|
| /view/noparameters | controller1 | |
| /view/bar | controller2 | foo -> bar |
| /view/piglet.html | controller3 | html -> piglet.html |
| /view/this/that/the-other.view | controller4 | view -> this/that/the-other |
| /view/c/save/2342443 | controller5 | controller -> save id -> 2342443 |
ParameterizedPathFilter
If accessing the parameters from request attributes is all you need, you can stop here. But to elevate your path parameters to first class citizen status in your web app, you need them to appear as request parameters. Once they are request parameters, Spring will bind them to your command or form objects, and your controllers can once again forget the details of request parsing and focus on business logic. To complete the solution, we need the ParameterizedPathFilter configured as a web filter. It proxies the request with a wrapper class that listens for ParameterizedUrlHandlerMapping's request attribute. When it is set, the wrapper class adds the parameters to the request parameters for the remaining life of the request.
To configure the ParameterizedPathFitler, add the following to your web.xml:
<filter>
<filter-name>PathParameterFilter</filter-name>
<filter-class>carbonfive.spring.web.pathparameter.ParameterizedPathFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>PathParameterFilter</filter-name>
<url-pattern>/path/to/dispatcher/servlet/*</url-pattern>
</filter-mapping>
You should have a filter mapping for each path pattern mapped to a Spring dispatcher servlet.
That's all it takes to add this functionality to your webapp. We've been using it in our current project. It's been problem free and the architectural improvements have been satisfying.
Posted by Alex Cruikshank at June 11, 2007 9:45 AM
Comments
You could also use something like UrlRewrite Filter to map all your routes, converting path parameters to query parameters using regular expressions.
http://tuckey.org/urlrewrite/
Posted by: Kim Pepper at June 20, 2007 5:53 PM
I don't believe URLRewriteFilter does provide the ability to set query parameters.
http://tuckey.org/urlrewrite/manual/3.0/index.html#configuration
You can set attributes in the request but the docs do not indicate that you can set parameters. Of course, it would be a possible feature using the same approach as Alex's Filter. UrlRewriteFilter does already wrap the request and response which is where you would add this feature.
Posted by: Alon at June 22, 2007 9:39 AM
I'm trying to use this as a filter so to continue using my command bean setup with with the REST-style URLs.
However, it seems that when passing multiple parameters like in the example: /view/c/save/2342443, each of them end up with a trailing slash except the last one.
So using the example from above, a call to cmd.getController() would return "save/" instead of just "save" and cmd.getId() would return "2342443".
I'm wondering if you can replicate this behaviour or if something that I'm doing wrongly...but it seems pretty straight-forward...
Posted by: Andy at July 3, 2007 12:43 PM
I tried to use the ParameterizedUrlHandlerMapping with a simple mapping like "/user/(*\:user)/found" but strangely that sets user to "stefan/" when I request "/user/stefan/found".
Posted by: Stefan Arentz at July 13, 2007 12:10 AM
You could try something like:
<rule>
<from>^/world/([a-z]+)/([a-z]+)$</from>
<to>/world.jsp?country=$1&city=$2</to>
</rule>
Posted by: Kim Pepper at July 19, 2007 10:50 PM
To Stefan and Alon:
I had made an error in the dispatcher configuration in this post. The line "<prop key="/view/c/(*:controller)/(*:id)">controller5</prop>" used to have back slashes before the colons. It's necessary to escape the colons if you use the <property name="mappings"><value>prop1=value1</value></property> syntax, but not if you use the <property name="mappings><props><prop key="...">...</prop></props><property> syntax. If you escape the colons in the latter syntax, parameters taken from within the path will have a slash appended. I'm not sure exactly why. I would expect that escaping the colon would either behave identically or cause it to miss the parameter altogether. Anyway, sorry about the mistake. It's been corrected in the text.
To Kim:
I agree that a regexp-based rule syntax would be superior, but I want this to be a drop-in extension to spring's URLHandlerMapping (or at least as close as possible). Its important that developers not have to configure their mappings in two different places, and I want to use the existing spring functionality for the actual routing of requests. These requirements led me to the current set of classes. Given these constraints, the only real flexibility in syntax is in the way we map a segment of the ant style path pattern to a parameter name. Thanks for the suggestion though.
Posted by: Alex Cruikshank at July 27, 2007 2:40 PM
This looks like an interesting approach to implementing REST-ful urls.
One question -- I didn't see a LICENSE.txt in the svn repository. Is this code available for use by the general public?
Posted by: dave at August 2, 2007 10:53 PM
Excellent point, Dave. I've just checked in an Apache 2.0 style license to the project and included it within each of the class files. Hopefully this makes adoptable by anyone that has a need for it.
Posted by: Alex Cruikshank at August 3, 2007 10:04 AM
Hi ...
Is it worked correctly to anyone? I am stuck at compilation itself at ParameterizedUrlHandlerMapping class.
...
Map handlerMap = (Map) getHandlerMap();
...
It complaits that it cannot find the getHandelerMap() method.
Please any insights ,,,
Thanks
Vin
Posted by: vin at August 28, 2007 9:16 AM
Hi Vin,
This is almost certainly a problem with your Spring MVC dependency. Either you're missing the Spring jars altogether, or you have an older version with a different API (one in which SimpleUrlHandlerMapping has no getHandlerMap() method). The jars are available at http://www.springframework.org . If you are going to compile the stand-alone java files, you are also going to need the servelet API in your classpath. Hope this helps - alex.
Posted by: Alex Cruikshank at August 28, 2007 9:52 AM
Alex. Thanks! Works great. Only problem is the ParameterizedPathFilter did not work for us, since the handler is a Controller subclass so request does not go back through the RequestDispatcher. Tell Don I say hello!
Posted by: Steve Mayhew at September 13, 2007 5:21 PM
I used a small HandlerAdapter in place of the Filter:
public class ParameterizedUrlHandlerAdapter extends SimpleControllerHandlerAdapter implements HandlerAdapter {
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (request.getAttribute(ParameterizedUrlHandlerMapping.PATH_PARAMETERS) != null) {
request = new ParameterizedPathServletRequest(request);
}
return super.handle(request, response, handler);
}
}
Posted by: Steve Mayhew at September 13, 2007 5:52 PM
Hi Steve,
I'm glad you found a solution that works for you. I considered using a HandlerAdapter instead of a filter. It's much cleaner, but it only wraps the controller, not the view, so the additional parameters won't be available to the JSP (or whatever technology you're using). You should note that the request does not need to go back through the filter after processing by the Dispatcher servlet. So long as it hit the filter before the dispatcher servlet, the request wrapper will add the parameters as soon as the magic attribute is set. Thanks for your contribution.
Posted by: Alex Cruikshank at September 13, 2007 7:47 PM
Logan is down it seems. I am trying to get the source to look at it. Could you check what's going on?
Would you be so kind to email me the source otherwise?
Thank you much!
Posted by: Anthony at December 21, 2007 8:44 AM
This sounds like a great utility, but there seems to be something wrong with the svn server. Specifically, when I click on the download link in the article, it says:
"logan.carbonfive.com has sent an incorrect or unexpected message. Error Code: -12263"
Can I get the code somewhere else?
Posted by: Harry at December 21, 2007 11:26 AM
Ah, yes. SVN now has a dedicated server so the URL has changed. I fixed the links in the post above.
Posted by: Alon at January 2, 2008 3:50 PM
Thanks for this. It has made my job incredibly easier.
I just wanted to say that if you want this to work with annotated controllers and @RequestMapping annotations then you should change the ParameterizedUrlHandlerMapping class to extend DefaultAnnotationHandlerMapping from the spring library instead. This simple change will allow you to use requestmappings and requestparam annotations.
Thanks again for this.
Posted by: charfles at January 21, 2008 9:26 PM
I'd love to figure out how to use this. Maybe I am missing something..
I want to use urls like:
http://mydomain.com/mywebapp/1/2/3/4/5/6/7/8/10
http://mydomain.com/mywebapp/search/somename/someaddress
and so on.
Seems I can't figure out how to allow a single controller handle all cases where it can just get ALL the values aftert the initial /mywebapp context.
I am still learning Spring, very confused with the variety of bean wiring required. Honestly Struts seemed easier to just specify the action class, bean(s), and go. But Spring has so much industry interest I want to learn it.
So can you point out how I might set a controller in the mywebapp-servlet.xml file so that I can just get the list of values after the webapp context name, and do with them as needed?
Thank you.
Posted by: john at February 9, 2008 11:12 PM
We had a problem while using the c:param tag of our jsp's. The parameters were not getting set onto the request correctly. So we changed the getParameterValues() method in the ParameterizedPathFiler to get it to set these parameters. This is our new method.
@Override
public String[] getParameterValues(String string) {
String[] values = parameters.get(string);
if(values != null && values.length > 0) {
return values;
}
return super.getParameterValues(string);
}
Other than this, the code works wonderfully! Thanks :)
Posted by: Rachel at March 7, 2008 6:24 AM
