I have annotated mappings working great through my spring mvc web app, however, they are case sensitive. I cannot find a way to make them case insensitive. (I\'d love to mak
Problem report for solution by smat
In solution by smat, there is one little side-effect (I would blame spring-mvc for that).
At first, AntPathMatcher.doMatch()
seems to return true/false depending on requested-url and controller-method's request-mapping string (That's the only thing should be done here). But, this method is used for one more purpose as well (which is not written in documentation!). Another purpose is to collect corresponding values for @PathVariable
in controller-method. These values are collected in Map
(last parameter).And these collected values are used to pass to controller-method as parameter value.
For example, we have controller-method like this,
@RequestMapping("/code/{userCode}")
public String getCode(@PathVariable("userCode") String userCode) {
System.out.println(userCode);
}
and if we access with URL, /code/AbD
then with solution by smat AntPathMatcher.doMatch()
will collect @PathVariable
value in Map
as userCode->abd
. As we are lower-casing the path string, values collected are also lower-cased. And this lower-cased userCode value is passed to our controller.
But, I am thankful to solution by smat which served me well so far without any other problems.
Solution
Solved this problem by doing work around solution by smat. Without lower-casing path or pattern string in extended AntPathMatcher
class, I forced my extended AntPathMatcher
to use my custom AntPathStringMatcher
. my custom AntPathStringMatcher
does case-insesitive matching without changing the case of actual string.
In following solution code most of the code is copied from original class code(code I wanted to customize was hidden for subclass because of private access).
Custom AntPathMatcher,
public class CaseInsensitivePathMatcher extends AntPathMatcher {
private final Map stringMatcherCache = new ConcurrentHashMap();
/**
* Actually match the given path
against the given
* pattern
.
*
* @param pattern
* the pattern to match against
* @param path
* the path String to test
* @param fullMatch
* whether a full pattern match is required (else a pattern match
* as far as the given base path goes is sufficient)
* @return true
if the supplied path
matched,
* false
if it didn't
*/
protected boolean doMatch(String pattern, String path, boolean fullMatch, Map uriTemplateVariables) {
if (path.startsWith(AntPathMatcher.DEFAULT_PATH_SEPARATOR) != pattern.startsWith(AntPathMatcher.DEFAULT_PATH_SEPARATOR)) {
return false;
}
String[] pattDirs = StringUtils.tokenizeToStringArray(pattern, AntPathMatcher.DEFAULT_PATH_SEPARATOR);
String[] pathDirs = StringUtils.tokenizeToStringArray(path, AntPathMatcher.DEFAULT_PATH_SEPARATOR);
int pattIdxStart = 0;
int pattIdxEnd = pattDirs.length - 1;
int pathIdxStart = 0;
int pathIdxEnd = pathDirs.length - 1;
// Match all elements up to the first **
while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {
String patDir = pattDirs[pattIdxStart];
if ("**".equals(patDir)) {
break;
}
if (!matchStrings(patDir, pathDirs[pathIdxStart], uriTemplateVariables)) {
return false;
}
pattIdxStart++;
pathIdxStart++;
}
if (pathIdxStart > pathIdxEnd) {
// Path is exhausted, only match if rest of pattern is * or **'s
if (pattIdxStart > pattIdxEnd) {
return (pattern.endsWith(AntPathMatcher.DEFAULT_PATH_SEPARATOR) ? path.endsWith(AntPathMatcher.DEFAULT_PATH_SEPARATOR) : !path
.endsWith(AntPathMatcher.DEFAULT_PATH_SEPARATOR));
}
if (!fullMatch) {
return true;
}
if (pattIdxStart == pattIdxEnd && pattDirs[pattIdxStart].equals("*") && path.endsWith(AntPathMatcher.DEFAULT_PATH_SEPARATOR)) {
return true;
}
for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
if (!pattDirs[i].equals("**")) {
return false;
}
}
return true;
} else if (pattIdxStart > pattIdxEnd) {
// String not exhausted, but pattern is. Failure.
return false;
} else if (!fullMatch && "**".equals(pattDirs[pattIdxStart])) {
// Path start definitely matches due to "**" part in pattern.
return true;
}
// up to last '**'
while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {
String patDir = pattDirs[pattIdxEnd];
if (patDir.equals("**")) {
break;
}
if (!matchStrings(patDir, pathDirs[pathIdxEnd], uriTemplateVariables)) {
return false;
}
pattIdxEnd--;
pathIdxEnd--;
}
if (pathIdxStart > pathIdxEnd) {
// String is exhausted
for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
if (!pattDirs[i].equals("**")) {
return false;
}
}
return true;
}
while (pattIdxStart != pattIdxEnd && pathIdxStart <= pathIdxEnd) {
int patIdxTmp = -1;
for (int i = pattIdxStart + 1; i <= pattIdxEnd; i++) {
if (pattDirs[i].equals("**")) {
patIdxTmp = i;
break;
}
}
if (patIdxTmp == pattIdxStart + 1) {
// '**/**' situation, so skip one
pattIdxStart++;
continue;
}
// Find the pattern between padIdxStart & padIdxTmp in str between
// strIdxStart & strIdxEnd
int patLength = (patIdxTmp - pattIdxStart - 1);
int strLength = (pathIdxEnd - pathIdxStart + 1);
int foundIdx = -1;
strLoop: for (int i = 0; i <= strLength - patLength; i++) {
for (int j = 0; j < patLength; j++) {
String subPat = pattDirs[pattIdxStart + j + 1];
String subStr = pathDirs[pathIdxStart + i + j];
if (!matchStrings(subPat, subStr, uriTemplateVariables)) {
continue strLoop;
}
}
foundIdx = pathIdxStart + i;
break;
}
if (foundIdx == -1) {
return false;
}
pattIdxStart = patIdxTmp;
pathIdxStart = foundIdx + patLength;
}
for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
if (!pattDirs[i].equals("**")) {
return false;
}
}
return true;
}
/**
* Tests whether or not a string matches against a pattern. The pattern may
* contain two special characters:
* '*' means zero or more characters
* '?' means one and only one character
*
* @param pattern
* pattern to match against. Must not be null
.
* @param str
* string which must be matched against the pattern. Must not be
* null
.
* @return true
if the string matches against the pattern, or
* false
otherwise.
*/
private boolean matchStrings(String pattern, String str, Map uriTemplateVariables) {
CaseInsensitiveAntPathStringMatcher matcher = this.stringMatcherCache.get(pattern);
if (matcher == null) {
matcher = new CaseInsensitiveAntPathStringMatcher(pattern);
this.stringMatcherCache.put(pattern, matcher);
}
return matcher.matchStrings(str, uriTemplateVariables);
}
}
Custom AntPathStringMatcher,
public class CaseInsensitiveAntPathStringMatcher {
private static final Pattern GLOB_PATTERN = Pattern.compile("\\?|\\*|\\{((?:\\{[^/]+?\\}|[^/{}]|\\\\[{}])+?)\\}");
private static final String DEFAULT_VARIABLE_PATTERN = "(.*)";
private final Pattern pattern;
private final List variableNames = new LinkedList();
/** Construct a new instance of the AntPatchStringMatcher
. */
CaseInsensitiveAntPathStringMatcher(String pattern) {
this.pattern = createPattern(pattern);
}
private Pattern createPattern(String pattern) {
StringBuilder patternBuilder = new StringBuilder();
Matcher m = GLOB_PATTERN.matcher(pattern);
int end = 0;
while (m.find()) {
patternBuilder.append(quote(pattern, end, m.start()));
String match = m.group();
if ("?".equals(match)) {
patternBuilder.append('.');
}
else if ("*".equals(match)) {
patternBuilder.append(".*");
}
else if (match.startsWith("{") && match.endsWith("}")) {
int colonIdx = match.indexOf(':');
if (colonIdx == -1) {
patternBuilder.append(DEFAULT_VARIABLE_PATTERN);
variableNames.add(m.group(1));
}
else {
String variablePattern = match.substring(colonIdx + 1, match.length() - 1);
patternBuilder.append('(');
patternBuilder.append(variablePattern);
patternBuilder.append(')');
String variableName = match.substring(1, colonIdx);
variableNames.add(variableName);
}
}
end = m.end();
}
patternBuilder.append(quote(pattern, end, pattern.length()));
return Pattern.compile(patternBuilder.toString(), Pattern.CASE_INSENSITIVE); // this line is updated to create case-insensitive pattern object
}
private String quote(String s, int start, int end) {
if (start == end) {
return "";
}
return Pattern.quote(s.substring(start, end));
}
/**
* Main entry point.
*
* @return true
if the string matches against the pattern, or false
otherwise.
*/
public boolean matchStrings(String str, Map uriTemplateVariables) {
Matcher matcher = pattern.matcher(str);
if (matcher.matches()) {
if (uriTemplateVariables != null) {
// SPR-8455
Assert.isTrue(variableNames.size() == matcher.groupCount(),
"The number of capturing groups in the pattern segment " + pattern +
" does not match the number of URI template variables it defines, which can occur if " +
" capturing groups are used in a URI template regex. Use non-capturing groups instead.");
for (int i = 1; i <= matcher.groupCount(); i++) {
String name = this.variableNames.get(i - 1);
String value = matcher.group(i);
uriTemplateVariables.put(name, value);
}
}
return true;
}
else {
return false;
}
}