How can I have case insensitive URLS in Spring MVC with annotated mappings
Asked Answered
M

9

39

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 make this happen within Spring MVC, rather than redirecting traffic somehow)

Mayst answered 10/11, 2010 at 23:15 Comment(2)
Also, add the tag 'Java' it will yield you a lot more page views which usually means more answers.Disjointed
similar question with detailed answer about this problem I've asked after seeing this question. #12684683Restrained
J
34

Spring 4.2 will support case-insensitive path matching. You can configure it as follows:

@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        AntPathMatcher matcher = new AntPathMatcher();
        matcher.setCaseSensitive(false);
        configurer.setPathMatcher(matcher);
    }
}
Jolin answered 29/7, 2015 at 12:39 Comment(3)
Do you know of any such configurations for Query Parameters ?Greggs
@Greggs Sorry, I don't know. I think you may request the feature at jira.spring.ioJolin
In Spring Boot 2 or Spring 5, the WebMvcConfigurerAdapter is deprecated. Instead, one should implement the WebMvcConfigurer directly.Nadaba
N
13

According to this webpost you need to add both a HandlerMapping and a HandlerAdapter in Spring MVC. The Mapping maps the request to a corresponding controller, and the adapter is responsible to execute the request using the controller.

You therefore need to override the PathMatcher for both the mapper and adapter.

Ex (will make all @Controllers case-insensitive):

New Matcher:

public class CaseInsenseticePathMatcher extends AntPathMatcher {
    @Override
    protected boolean doMatch(String pattern, String path, boolean fullMatch, Map<String, String> uriTemplateVariables) {
        System.err.println(pattern + " -- " + path);
        return super.doMatch(pattern.toLowerCase(), path.toLowerCase(), fullMatch, uriTemplateVariables);
    }
}

applicationContext.xml:

<bean id="matcher" class="test.CaseInsenseticePathMatcher"/>

<bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping">
    <property name="pathMatcher" ref="matcher"/>
</bean>

<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
    <property name="pathMatcher" ref="matcher"/>
    <property name="webBindingInitializer">
        <bean class="org.springframework.web.bind.support.ConfigurableWebBindingInitializer"/>
    </property>
    <property name="messageConverters">
        <list>
            <bean class="org.springframework.http.converter.ByteArrayHttpMessageConverter"/>
            <bean class="org.springframework.http.converter.StringHttpMessageConverter"/>
            <bean class="org.springframework.http.converter.FormHttpMessageConverter"/>
            <bean class="org.springframework.http.converter.xml.SourceHttpMessageConverter"/>
        </list>
    </property>
</bean>

<bean id="conversion-service" class="org.springframework.format.support.FormattingConversionServiceFactoryBean"/>

Added about the same that <mvc:annotation-driven> would do. (Thanks to David Parks for the link)

Neuralgia answered 25/3, 2011 at 21:8 Comment(1)
For Spring 3.1 replade AnnotationMethodHandlerAdapter with RequestMappingHandlerAdapter. They are using this new class in 3.1.Neuralgia
M
7

In Spring 3.2+ / Spring Boot, you can now set up case insensitive URL matching using the simplified Java config.

First you need to create the CaseInsensitivePathMatcher.groovy or Java class:

import org.springframework.util.AntPathMatcher

class CaseInsensitivePathMatcher extends AntPathMatcher{

    @Override
    protected boolean doMatch(String pattern, String path, boolean fullMatch, Map<String, String> uriTemplateVariables) {
        super.doMatch(pattern.toLowerCase(), path.toLowerCase(), fullMatch, uriTemplateVariables)
    }
}

Next, to make this happen, you should have a class annotated with Springs @Configuration that extends the WebMvcConfigurerAdapter class as shown below (Note that my code is contained within .groovy classes, hence the 'return' keyword is not required in the example):

@Configuration
public class ApplicationConfig extends WebMvcConfigurerAdapter

Then add the following 2 methods to the class:

/**
 * Creates a patchMatcher bean that matches case insensitively
 * @return PathMatcher
 */
@Bean
public PathMatcher pathMatcher() {
    new CaseInsensitivePathMatcher()
}

/**
 * Overrides the configurePathMatch() method in WebMvcConfigurerAdapter
 * <br/>Allows us to set a custom path matcher, used by the MVC for @RequestMapping's
     * @param configurer
     */
    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        configurer.pathMatcher = pathMatcher()
    }
}


That's it, you should now be all setup for case insensitive URL's with minimal configuration

Mousey answered 22/4, 2015 at 22:41 Comment(3)
Two years later, but still... WebMvcConfigurerAdapter in Spring 3.2 doesn’t have the method configurePathMatch. The minimum Spring version isn’t correct, probably 4.2 as stated in answers above. More info on docs.spring.io/spring/docs/3.2.13.RELEASE/javadoc-api/org/…Inquire
Works quite allright with Spring 3.2.17 and higher. Only thing missing in above code are several "return" statements which are ommited by error. You can find solution for Spring older than 3.2.17, at: newmiancode.blogspot.com/2010/01/…Farnese
Thanks for the comment @Gjera. you are correct that there are no return statements. As I mentioned in the examples above, my code is written using Groovy, which allows you to omit the 'return' and defaults the last line of code executed in a method as the return statement. I will edit the post to make it in 'bold' so it will stand out more.Mousey
L
3

Spring since version 5.3 does not use PathMatcher anymore by default, it uses PathPatternMatcher instead. To configure PathPatternMatcher, do the following:

@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        PathPatternParser patternParser = new PathPatternParser();
        patternParser.setCaseSensitive(false);
        configurer.setPatternParser(patternParser);
    }
}
Lyophobic answered 7/2, 2022 at 20:54 Comment(0)
M
2

Example from a bean file in Spring 4.2 and this is ONLY SUPPORTED v4.2+:

<mvc:annotation-driven validator="validator">
   <mvc:path-matching path-matcher="pathMatcher" />
</mvc:annotation-driven>

...

<!--Set endpoints case insensitive, spring is case-sensitive by default-->
<bean id="pathMatcher" class="org.springframework.util.AntPathMatcher">
  <property name="caseSensitive" value="false" />
</bean>
Mistakable answered 2/6, 2016 at 20:48 Comment(0)
D
2

Since WebMvcConfigurerAdapter is deprecated and Java 8 brought the concept of default methods in interface, You can implement WebMvcConfigurer interface to get the work done.

@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        AntPathMatcher matcher = new AntPathMatcher();
        matcher.setCaseSensitive(false);
        configurer.setPathMatcher(matcher); 
    }
}
Dunbarton answered 15/5, 2023 at 6:48 Comment(0)
D
1

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<String, String> uriTemplateVariables(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<String, String> uriTemplateVariables 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<String, CaseInsensitiveAntPathStringMatcher> stringMatcherCache = new ConcurrentHashMap<String, CaseInsensitiveAntPathStringMatcher>();

/**
 * Actually match the given <code>path</code> against the given
 * <code>pattern</code>.
 * 
 * @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 <code>true</code> if the supplied <code>path</code> matched,
 *         <code>false</code> if it didn't
 */
protected boolean doMatch(String pattern, String path, boolean fullMatch, Map<String, String> 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:<br>
 * '*' means zero or more characters<br>
 * '?' means one and only one character
 * 
 * @param pattern
 *            pattern to match against. Must not be <code>null</code>.
 * @param str
 *            string which must be matched against the pattern. Must not be
 *            <code>null</code>.
 * @return <code>true</code> if the string matches against the pattern, or
 *         <code>false</code> otherwise.
 */
private boolean matchStrings(String pattern, String str, Map<String, String> 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<String> variableNames = new LinkedList<String>();


/** Construct a new instance of the <code>AntPatchStringMatcher</code>. */
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 <code>true</code> if the string matches against the pattern, or <code>false</code> otherwise.
 */
public boolean matchStrings(String str, Map<String, String> 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;
    }
}
Depositor answered 17/7, 2014 at 3:27 Comment(0)
D
0

Well, I can't answer your question (I tried, I thought I could figure it out). But seeing as you haven't received any responses in 2 days, here are some leads at least:

This example seems to suggest it's possible:

http://webcache.googleusercontent.com/search?q=cache:ELj-ZQ8G4z0J:www.springbyexample.org/examples/sdms-simple-spring-mvc-web-module.html+case+insensitive+requestmapping+spring&cd=3&hl=en&ct=clnk&client=firefox-a

It references this class in Spring

http://static.springsource.org/spring/docs/3.0.4.RELEASE/javadoc-api/org/springframework/web/servlet/mvc/support/ControllerClassNameHandlerMapping.html

My guess (and it's just that, a guess), is that you need to expand <mvc:annotation-driven/> and implement the individual beans with the correct parameters to make it case-insensitive. See:

http://rapid-web.tumblr.com/post/296916668/what-does-annotation-driven-do

A last note, I noticed somewhere else in reading that it said that all paths default to lower case, have you verified that /MyPath isn't handled by @RequestMapping("/mypath")?

Again, just food for thought as best I can do. Maybe it'll get you far enough along to ask a more specific question that leads you to the answer - that's how these things work sometimes. Good luck!

Disjointed answered 13/11, 2010 at 9:9 Comment(0)
D
0

You can use the following solution

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.util.pattern.PathPatternParser;

@Configuration
public class WebConfig {

    @Bean
    PathPatternParser pathPatternParser(PathPatternParser pathPatternParser) {
        pathPatternParser.setCaseSensitive(false);
        return pathPatternParser;
    }
}
Dowdell answered 18/4, 2023 at 11:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.