diff --git a/spring-core/src/main/java/org/springframework/util/AntPathMatcher.java b/spring-core/src/main/java/org/springframework/util/AntPathMatcher.java index 635f1234f1..e40231270e 100644 --- a/spring-core/src/main/java/org/springframework/util/AntPathMatcher.java +++ b/spring-core/src/main/java/org/springframework/util/AntPathMatcher.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -85,7 +85,7 @@ public class AntPathMatcher implements PathMatcher { private volatile Boolean cachePatterns; - private final Map tokenizedPatternCache = new ConcurrentHashMap(256); + private final Map tokenizedPatternCache = new ConcurrentHashMap(256); final Map stringMatcherCache = new ConcurrentHashMap(256); @@ -187,7 +187,11 @@ public class AntPathMatcher implements PathMatcher { return false; } - String[] pattDirs = tokenizePattern(pattern); + PreprocessedPattern preprocessedPattern = tokenizePattern(pattern); + if (fullMatch && this.caseSensitive && preprocessedPattern.certainlyNotMatch(path)) { + return false; + } + String[] pattDirs = preprocessedPattern.tokenized; String[] pathDirs = tokenizePath(path); int pattIdxStart = 0; @@ -314,14 +318,14 @@ public class AntPathMatcher implements PathMatcher { * @param pattern the pattern to tokenize * @return the tokenized pattern parts */ - protected String[] tokenizePattern(String pattern) { - String[] tokenized = null; + protected PreprocessedPattern tokenizePattern(String pattern) { + PreprocessedPattern tokenized = null; Boolean cachePatterns = this.cachePatterns; if (cachePatterns == null || cachePatterns.booleanValue()) { tokenized = this.tokenizedPatternCache.get(pattern); } if (tokenized == null) { - tokenized = tokenizePath(pattern); + tokenized = compiledPattern(pattern); if (cachePatterns == null && this.tokenizedPatternCache.size() >= CACHE_TURNOFF_THRESHOLD) { // Try to adapt to the runtime situation that we're encountering: // There are obviously too many different patterns coming in here... @@ -345,6 +349,31 @@ public class AntPathMatcher implements PathMatcher { return StringUtils.tokenizeToStringArray(path, this.pathSeparator, this.trimTokens, true); } + private int firstSpecialCharIdx(int specialCharIdx, int prevFoundIdx) { + if (specialCharIdx != -1) { + return prevFoundIdx == -1 ? specialCharIdx : Math.min(prevFoundIdx, specialCharIdx); + } + else { + return prevFoundIdx; + } + } + + private PreprocessedPattern compiledPattern(String pattern) { + String[] tokenized = tokenizePath(pattern); + int specialCharIdx = -1; + specialCharIdx = firstSpecialCharIdx(pattern.indexOf('*'), specialCharIdx); + specialCharIdx = firstSpecialCharIdx(pattern.indexOf('?'), specialCharIdx); + specialCharIdx = firstSpecialCharIdx(pattern.indexOf('{'), specialCharIdx); + final String prefix; + if (specialCharIdx != -1) { + prefix = pattern.substring(0, specialCharIdx); + } + else { + prefix = pattern; + } + return new PreprocessedPattern(tokenized, prefix.isEmpty() ? null : prefix); + } + /** * Test whether or not a string matches against a pattern. * @param pattern the pattern to match against (never {@code null}) @@ -847,4 +876,19 @@ public class AntPathMatcher implements PathMatcher { } } + private static class PreprocessedPattern { + private final String[] tokenized; + + private final String prefix; + + public PreprocessedPattern(String[] tokenized, String prefix) { + this.tokenized = tokenized; + this.prefix = prefix; + } + + private boolean certainlyNotMatch(String path) { + return prefix != null && !path.startsWith(prefix); + } + } + } diff --git a/spring-core/src/test/java/org/springframework/util/AntPathMatcherTests.java b/spring-core/src/test/java/org/springframework/util/AntPathMatcherTests.java index 29a2486a29..f1eab82925 100644 --- a/spring-core/src/test/java/org/springframework/util/AntPathMatcherTests.java +++ b/spring-core/src/test/java/org/springframework/util/AntPathMatcherTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -418,8 +418,8 @@ public class AntPathMatcherTests { assertEquals("/*.html", pathMatcher.combine("/**", "/*.html")); assertEquals("/*.html", pathMatcher.combine("/*", "/*.html")); assertEquals("/*.html", pathMatcher.combine("/*.*", "/*.html")); - assertEquals("/{foo}/bar", pathMatcher.combine("/{foo}", "/bar")); // SPR-8858 - assertEquals("/user/user", pathMatcher.combine("/user", "/user")); // SPR-7970 + assertEquals("/{foo}/bar", pathMatcher.combine("/{foo}", "/bar")); // SPR-8858 + assertEquals("/user/user", pathMatcher.combine("/user", "/user")); // SPR-7970 assertEquals("/{foo:.*[^0-9].*}/edit/", pathMatcher.combine("/{foo:.*[^0-9].*}", "/edit/")); // SPR-10062 assertEquals("/1.0/foo/test", pathMatcher.combine("/1.0", "/foo/test")); // SPR-10554 assertEquals("/hotel", pathMatcher.combine("/", "/hotel")); // SPR-12975 @@ -454,8 +454,8 @@ public class AntPathMatcherTests { // SPR-10550 assertEquals(-1, comparator.compare("/hotels/{hotel}/bookings/{booking}/cutomers/{customer}", "/**")); - assertEquals(1, comparator.compare("/**","/hotels/{hotel}/bookings/{booking}/cutomers/{customer}")); - assertEquals(0, comparator.compare("/**","/**")); + assertEquals(1, comparator.compare("/**", "/hotels/{hotel}/bookings/{booking}/cutomers/{customer}")); + assertEquals(0, comparator.compare("/**", "/**")); assertEquals(-1, comparator.compare("/hotels/{hotel}", "/hotels/*")); assertEquals(1, comparator.compare("/hotels/*", "/hotels/{hotel}")); @@ -618,12 +618,44 @@ public class AntPathMatcherTests { assertTrue(pathMatcher.stringMatcherCache.size() > 20); for (int i = 0; i < 65536; i++) { - pathMatcher.match("test" + i, "test"); + pathMatcher.match("test" + i, "test" + i); } // Cache keeps being alive due to the explicit cache setting assertTrue(pathMatcher.stringMatcherCache.size() > 65536); } + @Test + public void preventCreatingStringMatchersIfPathDoesNotStartsWithPatternPrefix() { + pathMatcher.setCachePatterns(true); + assertEquals(0, pathMatcher.stringMatcherCache.size()); + + pathMatcher.match("test?", "test"); + assertEquals(1, pathMatcher.stringMatcherCache.size()); + + pathMatcher.match("test?", "best"); + pathMatcher.match("test/*", "view/test.jpg"); + pathMatcher.match("test/**/test.jpg", "view/test.jpg"); + pathMatcher.match("test/{name}.jpg", "view/test.jpg"); + assertEquals(1, pathMatcher.stringMatcherCache.size()); + } + + @Test + public void creatingStringMatchersIfPatternPrefixCannotDetermineIfPathMatch() { + pathMatcher.setCachePatterns(true); + assertEquals(0, pathMatcher.stringMatcherCache.size()); + + pathMatcher.match("test", "testian"); + pathMatcher.match("test?", "testFf"); + pathMatcher.match("test/*", "test/dir/name.jpg"); + pathMatcher.match("test/{name}.jpg", "test/lorem.jpg"); + pathMatcher.match("bla/**/test.jpg", "bla/test.jpg"); + pathMatcher.match("**/{name}.jpg", "test/lorem.jpg"); + pathMatcher.match("/**/{name}.jpg", "/test/lorem.jpg"); + pathMatcher.match("/*/dir/{name}.jpg", "/*/dir/lorem.jpg"); + + assertEquals(7, pathMatcher.stringMatcherCache.size()); + } + @Test public void cachePatternsSetToFalse() { pathMatcher.setCachePatterns(false);