Sebastian has posted 1 posts at DZone. View Full User Profile

Spring 3 WebMVC - Optional Path Variables

10.19.2010
| 24842 views |
  • submit to reddit

Introduction

To bind requests to controller methods via request pattern, Spring WebMVC's REST-feature is a perfect choice. Take a request like http://example.domain/houses/213234 and you can easily bind it to a controller method via annotation and bind path variables:

...

@RequestMapping("/houses/{id}")
public String handleHouse(@PathVariable long id) {
return "viewHouse";
}

Problem

But the problem was, I needed optional path segments small and preview. That means, it would like to handle requests like /houses/preview/small/213234, /houses/small/213234, /houses/preview/213234 and the original /houses/213234. Why can't I use only one @RequestMapping for that.

Ok, I could introduce three new methods with request mappings:

...

@RequestMapping("/houses/{id}")
public String handleHouse(@PathVariable long id) {
return "viewHouse";
}

@RequestMapping("/houses/preview/{id}")
...

@RequestMapping("/houses/preview/small/{id}")
...

@RequestMapping("/houses/small/{id}")
...

But imagine I have 3 or 4 optional path segments. I would had 8 or 16 methods to handle all request. So, there must be another option.

Browsing through the source code, I found an org.springframework.util.AntPathMatcher which is reponsable for parsing the request uri and extract variables. It seems to be the right place for an extension.

Here is, how I would like to write my handler method:

@RequestMapping("/houses/[preview/][small/]{id}")
public String handlePreview(@PathVariable long id, @PathVariable("preview/") boolean preview, @PathVariable("small/") boolean small) {
return "view";
}

Solution

Let's do the extension:

package de.herold.spring3;

import java.util.HashMap;
import java.util.Map;

import org.springframework.util.AntPathMatcher;

/**
* Extends {@link AntPathMatcher} to introduce the feature of optional path
* variables. It's supports request mappings like:
*
* <pre>
* @RequestMapping("/houses/[preview/][small/]{id}")
* public String handlePreview(@PathVariable long id, @PathVariable("preview/") boolean preview, @PathVariable("small/") boolean small) {
* ...
* }
* </pre>
*
*/
public class OptionalPathMatcher extends AntPathMatcher {

public static final String ESCAPE_BEGIN = "[";
public static final String ESCAPE_END = "]";

/**
* stores a request mapping pattern and corresponding variable
* configuration.
*/
protected static class PatternVariant {

private final String pattern;
private Map variables;

public Map getVariables() {
return variables;
}

public PatternVariant(String pattern) {
super();
this.pattern = pattern;
}

public PatternVariant(PatternVariant parent, int startPos, int endPos, boolean include) {
final String p = parent.getPattern();
final String varName = p.substring(startPos + 1, endPos);
this.pattern = p.substring(0, startPos) + (include ? varName : "") + p.substring(endPos + 1);

this.variables = new HashMap();
if (parent.getVariables() != null) {
this.variables.putAll(parent.getVariables());
}
this.variables.put(varName, Boolean.toString(include));
}

public String getPattern() {
return pattern;
}
}

/**
* here we use {@link AntPathMatcher#doMatch(String, String, boolean, Map)}
* to do the real match against the
* {@link #getPatternVariants(PatternVariant) calculated patters}. If
* needed, template variables are set.
*/
@Override
protected boolean doMatch(String pattern, String path, boolean fullMatch, Map uriTemplateVariables) {
for (PatternVariant patternVariant : getPatternVariants(new PatternVariant(pattern))) {
if (super.doMatch(patternVariant.getPattern(), path, fullMatch, uriTemplateVariables)) {
if (uriTemplateVariables != null && patternVariant.getVariables() != null) {
uriTemplateVariables.putAll(patternVariant.getVariables());
}
return true;
}
}

return false;
}

/**
* build recursicly all possible request pattern for the given request
* pattern. For pattern: /houses/[preview/][small/]{id}, it
* generates all combinations: /houses/preview/small/{id},
* /houses/preview/{id} /houses/small/{id}
* /houses/{id}
*/
protected PatternVariant[] getPatternVariants(PatternVariant variant) {
final String pattern = variant.getPattern();
if (!pattern.contains(ESCAPE_BEGIN)) {
return new PatternVariant[] { variant };
} else {
int startPos = pattern.indexOf(ESCAPE_BEGIN);
int endPos = pattern.indexOf(ESCAPE_END, startPos + 1);
PatternVariant[] withOptionalParam = getPatternVariants(new PatternVariant(variant, startPos, endPos, true));
PatternVariant[] withOutOptionalParam = getPatternVariants(new PatternVariant(variant, startPos, endPos, false));
return concat(withOptionalParam, withOutOptionalParam);
}
}

/**
* utility function for array concatenation
*/
private static PatternVariant[] concat(PatternVariant[] A, PatternVariant[] B) {
PatternVariant[] C = new PatternVariant[A.length + B.length];
System.arraycopy(A, 0, C, 0, A.length);
System.arraycopy(B, 0, C, A.length, B.length);
return C;
}
}

Now let Spring use our new path matcher. Here you have the Spring application context:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">

<bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping" />
<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
<property name="pathMatcher">
<bean class="de.herold.webapp.spring3.OptionalPathMatcher" />
</property>
</bean>
<context:component-scan base-package="de.herold" />
</beans>

That's it. Just set the pathMatcher property of AnnotationMethodHandlerAdapter.

Fazit

I know this implementation is far from being elegant or effective. My intention was to show an extension of the AntPathMatcher. Maybe someone gets inspired and provides an extension to handle pattern like:

@RequestMapping("/houses/{**}/{id}")
public String handleHouse(@PathVariable long id, @PathVariable("**") String inBetween) {
return "viewHouse";
}

to get the "**" as a concrete value.

The sources for a little prove of concept are attached.

AttachmentSize
Archiv.zip7.87 KB
Published at DZone with permission of its author, Sebastian Herold.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

Comments

Gervais Blaise replied on Wed, 2010/10/20 - 2:13am

Thanks for your work. I will surely going to use it soon. One time again we have an example of the incredible spring extensibility.

Zoltan Magyar replied on Wed, 2010/10/20 - 7:47am

I see your intention, and nice to extend the AntPathMatcher, but I feel like shooting to a bird with cannon.

Would not it be simpler and more restful a url like, let's say:

@RequestMapping("/house/{id}/{size}")
public String handleHouse(@PathVariable long id, @PathVariable String size) {
    return "viewHouse";
}

Then the size could be preview, small, whatever.

One could even argue, not to put the size in the url, because strictly spoken, the id identifies already the resource, the size is a secondary parameter, should not even be in the url. But yes, the image can be stored in small size, what is a different resource, so.., anyway it's rather a matter of taste.

Zoltan

Sebastian Herold replied on Sun, 2010/10/24 - 9:49am in response to: Zoltan Magyar

Hi Zoltan

 thanks for your solution. It covers /house/65545/preview, but in Spring path variables aren't optional, so you always have to mention a size like "preview" or "normal". I'd like to have a default size like /house/65545. This request isn't handled by Your code. But you are right: in real REST-APIs it should be set via URL parameters. As a matter of taste, we focused on human readibility.

 Sebastian

Liezel Jane Jandayan replied on Thu, 2011/08/25 - 6:57am

In Spring Web MVC you can use any object as a command or form-backing object; you do not need to implement a framework-specific interface or base class. Spring's data binding is highly flexible: for example, it treats type mismatches as validation errors that can be evaluated by the application, not as system errors.-Jonathan Berkowitz

Hardik Desai replied on Tue, 2014/03/04 - 5:56am

Hi Sebastian,

Great work on this one. I have a requirement to implement a similar optional path matcher in my project which is a commercial product. Do I have any licensing restrictions? 

Please let me know if I have your permission to use this code as-is in my project to meet the optional path variables requirement?

Thank you!

Hardik Desai

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.