I'm using Spring MVC (3.0) with annotation-driven controllers. I would like to create REST-ful URLs for resources and be able to not require (but still optionally allow) file extension on the end of the URL (but assume HTML content type if no extension). This works out-of-the-box with Spring MVC as long as there are no dots (period/full-stop) in the filename part.
However some of my URLs require an identifier with dots in the name. E.g. like this:
http://company.com/widgets/123.456.789.500
In this case Spring looks for a content type for the extension .500
and finds none so errors. I can use work-arounds like adding .html
to the end, encoding the identifier or adding a trailing slash. I'm not happy with any if these but could probably live with adding .html
.
I've unsuccessfully looked for a way of overriding the default file extension detection in Spring.
Is it possible to customize or disable file extension detection for a given controller method or URL pattern, etc?
The @PathVariable
pattern matching is a bit twitchy when it comes to dots in the URL (see SPR-5778). You can make it less twitchy (but more picky), and get better control over dot-heavy URLs, by setting the useDefaultSuffixPattern
property on DefaultAnnotationHandlerMapping
to false
.
If you haven't already explicitly declared a DefaultAnnotationHandlerMapping
in your context (and most people don't since it's declared implicitly for you), then you can add it explicitly, and set that property.
Probably, it's an ugly hack, I just wanted to explore extensibility of Spring @MVC. Here is a customized PathMatcher
. It uses $
in the pattern as the end marker - if pattern ends with it, marker is removed and pattern is matched by the default matcher, but if pattern has $
in the middle (e.g. ...$.*
), such a pattern is not matched.
public class CustomPathMatcher implements PathMatcher {
private PathMatcher target;
public CustomPathMatcher() {
target = new AntPathMatcher();
}
public String combine(String pattern1, String pattern2) {
return target.combine(pattern1, pattern2);
}
public String extractPathWithinPattern(String pattern, String path) {
if (isEncoded(pattern)) {
pattern = resolvePattern(pattern);
if (pattern == null) return "";
}
return target.extractPathWithinPattern(pattern, path);
}
public Map<String, String> extractUriTemplateVariables(String pattern,
String path) {
if (isEncoded(pattern)) {
pattern = resolvePattern(pattern);
if (pattern == null) return Collections.emptyMap();
}
return target.extractUriTemplateVariables(pattern, path);
}
public Comparator<String> getPatternComparator(String pattern) {
final Comparator<String> targetComparator = target.getPatternComparator(pattern);
return new Comparator<String>() {
public int compare(String o1, String o2) {
if (isEncoded(o1)) {
if (isEncoded(o2)) {
return 0;
} else {
return -1;
}
} else if (isEncoded(o2)) {
return 1;
}
return targetComparator.compare(o1, o2);
}
};
}
public boolean isPattern(String pattern) {
if (isEncoded(pattern)) {
pattern = resolvePattern(pattern);
if (pattern == null) return true;
}
return target.isPattern(pattern);
}
public boolean match(String pattern, String path) {
if (isEncoded(pattern)) {
pattern = resolvePattern(pattern);
if (pattern == null) return false;
}
return target.match(pattern, path);
}
public boolean matchStart(String pattern, String path) {
if (isEncoded(pattern)) {
pattern = resolvePattern(pattern);
if (pattern == null) return false;
}
return target.match(pattern, path);
}
private boolean isEncoded(String pattern) {
return pattern != null && pattern.contains("$");
}
private String resolvePattern(String pattern) {
int i = pattern.indexOf('$');
if (i < 0) return pattern;
else if (i == pattern.length() - 1) {
return pattern.substring(0, i);
} else {
String tail = pattern.substring(i + 1);
if (tail.startsWith(".")) return null;
else return pattern.substring(0, i) + tail;
}
}
}
Config:
<bean id = "pathMatcher" class = "sample.CustomPathMatcher" />
<bean class = "org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
<property name = "pathMatcher" ref="pathMatcher" />
</bean>
<bean class = "org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping">
<property name = "pathMatcher" ref="pathMatcher" />
</bean>
And usage (given "/hello/1.2.3", value
is "1.2.3"):
@RequestMapping(value = "/hello/{value}$", method = RequestMethod.GET)
public String hello(@PathVariable("value") String value, ModelMap model)
EDIT:: Now doesn't break "trailing slash doesn't matter" rule
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With