diff --git a/restlight-benchmarks/src/main/java/esa/restlight/jmh/server/PathMatcherMultiThreadBenchmark.java b/restlight-benchmarks/src/main/java/esa/restlight/jmh/server/PathMatcherMultiThreadBenchmark.java index 6ec6400b..aa15b661 100644 --- a/restlight-benchmarks/src/main/java/esa/restlight/jmh/server/PathMatcherMultiThreadBenchmark.java +++ b/restlight-benchmarks/src/main/java/esa/restlight/jmh/server/PathMatcherMultiThreadBenchmark.java @@ -33,6 +33,9 @@ public class PathMatcherMultiThreadBenchmark { private org.springframework.util.PathMatcher springMatcher; @Param({"/abcd/efgh/ijkl", + "/{foo}/efgh/ijkl", + "/abcd/e{foo}h/ijkl", + "/abcd/e{foo}/{bar}l", "/ab??/??gh/i?jkl", "/abc*/e*h/*jkl", "/ab?d/e?g*/*k*", diff --git a/restlight-server/src/main/java/esa/restlight/server/util/PathMatcher.java b/restlight-server/src/main/java/esa/restlight/server/util/PathMatcher.java index abae1c7e..4c482180 100644 --- a/restlight-server/src/main/java/esa/restlight/server/util/PathMatcher.java +++ b/restlight-server/src/main/java/esa/restlight/server/util/PathMatcher.java @@ -18,13 +18,13 @@ import esa.commons.StringUtils; import io.netty.util.concurrent.FastThreadLocal; -import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.StringTokenizer; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiPredicate; import java.util.regex.Pattern; /** @@ -91,7 +91,6 @@ private static PatternDir[] getPatternDirs(String pattern, boolean caseSensitive * * @param pattern the pattern to match against * @param path the path String to test - * * @return {@code true} if the given path matched, otherwise {@code false} */ public static boolean match(String pattern, String path) { @@ -109,7 +108,6 @@ public static boolean match(String pattern, String path) { * Determines Whether the given pattern is a pattern, such as {@code /fo?/bar}, {@code /f*o/bar} * * @param pattern path - * * @return {@code true} if it is a pattern, otherwise {@code false}. */ public static boolean isPattern(String pattern) { @@ -123,7 +121,6 @@ public static boolean isPattern(String pattern) { * Determines Whether the given pattern contains a template variable pattern, such as "/{foo}", "/foo/{bar}". * * @param pattern path - * * @return {@code true} if it is a template variable pattern, otherwise {@code false}. */ public static boolean isTemplateVarPattern(String pattern) { @@ -166,7 +163,6 @@ public static boolean isTemplateVarPattern(String pattern) { * * @param pattern pattern, such as '/foo/?ar/q*x' * @param subject target path or a pattern. - * * @return {@code true} if give pattern's semantic certainly includes given subject path, otherwise {@code false} */ public static boolean certainlyIncludes(String pattern, String subject) { @@ -206,7 +202,6 @@ public static boolean certainlyIncludes(String pattern, String subject) { * * @param pattern1 pattern1 * @param pattern2 pattern2 - * * @return {@code true} patterns may intersect potentially, otherwise {@code false}. */ public static boolean isPotentialIntersect(String pattern1, String pattern2) { @@ -322,10 +317,12 @@ public static boolean isPotentialIntersect(String pattern1, String pattern2) { index++; } - if (patternDirs1.length > patternDirs2.length && !patternDirs2[patternDirs2.length - 1].isDoubleWildcards) { + if (patternDirs1.length > patternDirs2.length + && !patternDirs2[patternDirs2.length - 1].isDoubleWildcards) { return false; } - if (patternDirs2.length > patternDirs1.length && !patternDirs1[patternDirs1.length - 1].isDoubleWildcards) { + if (patternDirs2.length > patternDirs1.length + && !patternDirs1[patternDirs1.length - 1].isDoubleWildcards) { return false; } } @@ -357,12 +354,11 @@ public static boolean isPotentialIntersect(String pattern1, String pattern2) { * * @param pattern1 the first pattern * @param pattern2 the second pattern - * * @return the combination of the two patterns */ public static String combine(String pattern1, String pattern2) { if (StringUtils.isEmpty(pattern1) && StringUtils.isEmpty(pattern2)) { - return ""; + return StringUtils.empty(); } if (StringUtils.isEmpty(pattern1)) { return pattern2; @@ -399,7 +395,7 @@ public static String combine(String pattern1, String pattern2) { String ext1 = pattern1.substring(starDotPos1 + 1); int dotPos2 = pattern2.indexOf('.'); String file2 = (dotPos2 == -1 ? pattern2 : pattern2.substring(0, dotPos2)); - String ext2 = (dotPos2 == -1 ? "" : pattern2.substring(dotPos2)); + String ext2 = (dotPos2 == -1 ? StringUtils.empty() : pattern2.substring(dotPos2)); boolean ext1All = (".*".equals(ext1) || ext1.isEmpty()); boolean ext2All = (".*".equals(ext2) || ext2.isEmpty()); if (!ext1All && !ext2All) { @@ -455,7 +451,6 @@ public boolean isTemplateVarPattern() { * #pattern} is a template variable pattern. * * @param path the path String to test - * * @return a not {@code null} map if the given path matched, otherwise {@code null}. */ public Map matchAndExtractUriTemplateVariables(String path) { @@ -472,7 +467,6 @@ public Map matchAndExtractUriTemplateVariables(String path) { * Matches the given path against the given {@link #pattern}. * * @param path the path String to test - * * @return {@code true} if the given path matched, otherwise {@code false}. */ public boolean match(String path) { @@ -486,7 +480,6 @@ public boolean match(String path) { * then match as well. * * @param path the path String to test - * * @return {@code true} if the supplied {@code path} matched, {@code false} if it didn't */ public boolean matchStart(String path) { @@ -705,17 +698,32 @@ private static class PatternDir { protected static class Matcher { - private static final Pattern GLOB_PATTERN = Pattern.compile("\\?|\\*|\\{((?:\\{[^/]+?}|[^/{}]|\\\\[{}])+?)}"); - private static final String DEFAULT_VARIABLE_PATTERN = "(.*)"; - private final Pattern pattern; - private final List variableNames = new ArrayList<>(4); + private static final Pattern GLOB_PATTERN = + Pattern.compile("\\?|\\*|\\{((?:\\{[^/]+?}|[^/{}]|\\\\[{}])+?)}"); + private final BiPredicate> matcher; Matcher(String pattern, boolean caseSensitive) { - StringBuilder patternBuilder = new StringBuilder(); - java.util.regex.Matcher matcher = GLOB_PATTERN.matcher(pattern); + this.matcher = toMatcher(pattern, caseSensitive); + } + + boolean matchStrings(String str) { + return matchStrings(str, null); + } + + boolean matchStrings(String str, Map uriTemplateVariables) { + return matcher.test(str, uriTemplateVariables); + } + + private static BiPredicate> toMatcher(String patternStr, boolean caseSensitive) { + final StringBuilder patternBuilder = new StringBuilder(); + final java.util.regex.Matcher matcher = GLOB_PATTERN.matcher(patternStr); + final List variableNames = new LinkedList<>(); + boolean isPathVarOnly = false; + int start = 0; int end = 0; while (matcher.find()) { - patternBuilder.append(quote(pattern, end, matcher.start())); + isPathVarOnly = false; + patternBuilder.append(quote(patternStr, end, start = matcher.start())); String match = matcher.group(); if ("?".equals(match)) { patternBuilder.append('.'); @@ -724,55 +732,138 @@ protected static class Matcher { } else if (match.startsWith("{") && match.endsWith("}")) { int colonIdx = match.indexOf(':'); if (colonIdx == -1) { - patternBuilder.append(DEFAULT_VARIABLE_PATTERN); - this.variableNames.add(matcher.group(1)); + patternBuilder.append("(.*)"); + variableNames.add(matcher.group(1)); + isPathVarOnly = true; } else { String variablePattern = match.substring(colonIdx + 1, match.length() - 1); patternBuilder.append('('); patternBuilder.append(variablePattern); patternBuilder.append(')'); String variableName = match.substring(1, colonIdx); - this.variableNames.add(variableName); + variableNames.add(variableName); } } end = matcher.end(); } - patternBuilder.append(quote(pattern, end, pattern.length())); - this.pattern = (caseSensitive ? Pattern.compile(patternBuilder.toString()) : - Pattern.compile(patternBuilder.toString(), Pattern.CASE_INSENSITIVE)); + + if (patternBuilder.length() == 0) { + return caseSensitive + ? (target, uriTemplateVars) -> patternStr.equals(target) + : (target, uriTemplateVars) -> patternStr.equalsIgnoreCase(target); + } else if (isPathVarOnly && variableNames.size() == 1) { + final String prefix = start == 0 ? null : patternStr.substring(0, start); + final String suffix = end == patternStr.length() ? null : patternStr.substring(end); + final String varName = variableNames.get(0); + if (prefix == null && suffix == null) { + // /{foo} -> /bar + return (target, uriTemplateVars) -> { + if (uriTemplateVars != null) { + uriTemplateVars.put(varName, target); + } + return true; + }; + } else if (prefix == null) { + // /{foo}bar -> /fbar + return (target, uriTemplateVars) -> { + if (caseSensitive) { + if (target.endsWith(suffix)) { + if (uriTemplateVars != null) { + uriTemplateVars.put(varName, + target.substring(0, target.length() - suffix.length())); + } + return true; + } + } else if (target.length() >= suffix.length()) { + int idx = target.length() - suffix.length(); + if (target.substring(idx).equalsIgnoreCase(suffix)) { + if (uriTemplateVars != null) { + uriTemplateVars.put(varName, target.substring(0, idx)); + } + return true; + } + } + return false; + }; + } else if (suffix == null) { + // /foo{bar} -> /foob + return (target, uriTemplateVars) -> { + if (caseSensitive) { + if (target.startsWith(prefix)) { + if (uriTemplateVars != null) { + uriTemplateVars.put(varName, + target.substring(prefix.length())); + } + return true; + } + } else if (target.length() >= prefix.length()) { + if (target.substring(0, prefix.length()).equalsIgnoreCase(prefix)) { + if (uriTemplateVars != null) { + uriTemplateVars.put(varName, target.substring(prefix.length())); + } + return true; + } + } + return false; + }; + } else { + // /foo{bar}baz -> /foobbaz + return (target, uriTemplateVars) -> { + if (target.length() >= prefix.length() + suffix.length()) { + if (caseSensitive) { + if (target.startsWith(prefix) && target.endsWith(suffix)) { + if (uriTemplateVars != null) { + uriTemplateVars.put(varName, target.substring(prefix.length(), + target.length() - suffix.length())); + } + return true; + } + } else { + int idx = target.length() - suffix.length(); + if (target.substring(0, prefix.length()).equalsIgnoreCase(prefix) + && target.substring(idx).equalsIgnoreCase(suffix)) { + if (uriTemplateVars != null) { + uriTemplateVars.put(varName, target.substring(prefix.length(), idx)); + } + return true; + } + } + } + return false; + }; + } + } else { + patternBuilder.append(quote(patternStr, end, patternStr.length())); + final Pattern pattern = (caseSensitive ? Pattern.compile(patternBuilder.toString()) : + Pattern.compile(patternBuilder.toString(), Pattern.CASE_INSENSITIVE)); + final String[] varNames = variableNames.toArray(new String[0]); + return (target, uriTemplateVars) -> matchByPattern(target, uriTemplateVars, pattern, varNames); + } } - private String quote(String s, int start, int end) { + private static String quote(String s, int start, int end) { if (start == end) { - return ""; + return StringUtils.empty(); } return Pattern.quote(s.substring(start, end)); } - boolean matchStrings(String str) { - return matchStrings(str, null); - } - - /** - * Main entry point. - * - * @return {@code true} if the string matches against the pattern, or {@code false} otherwise. - */ - boolean matchStrings(String str, Map uriTemplateVariables) { - java.util.regex.Matcher matcher = this.pattern.matcher(str); + private static boolean matchByPattern(String str, + Map uriTemplateVariables, + Pattern pattern, + String[] varNames) { + java.util.regex.Matcher matcher = pattern.matcher(str); if (matcher.matches()) { if (uriTemplateVariables != null) { - // SPR-8455 - if (this.variableNames.size() != matcher.groupCount()) { + final int groupCount = matcher.groupCount(); + if (varNames.length != groupCount) { throw new IllegalArgumentException("The number of capturing groups in the pattern segment " + - this.pattern + " does not match the number of URI template variables it defines, " + + 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); + for (int i = 1; i <= groupCount; i++) { + uriTemplateVariables.put(varNames[i - 1], matcher.group(i)); } } return true; diff --git a/restlight-server/src/test/java/esa/restlight/server/util/PathMatcherTest.java b/restlight-server/src/test/java/esa/restlight/server/util/PathMatcherTest.java index 060d337d..2c0b4984 100644 --- a/restlight-server/src/test/java/esa/restlight/server/util/PathMatcherTest.java +++ b/restlight-server/src/test/java/esa/restlight/server/util/PathMatcherTest.java @@ -36,6 +36,10 @@ void testExactMatch() { assertFalse(PathMatcher.match("foo", "/foo")); assertFalse(PathMatcher.match("/foo", "foo")); assertFalse(PathMatcher.match("/foo/", "/foo")); + assertFalse(PathMatcher.match("foo", "FOO")); + + assertTrue(new PathMatcher("foo", false).match("foo")); + assertTrue(new PathMatcher("foo", false).match("FOO")); } @Test @@ -100,6 +104,38 @@ void testMatchWithWildcard() { assertTrue(PathMatcher.match("", "")); assertTrue(PathMatcher.match("/{foo}.*", "/testing.bar")); + + assertTrue(PathMatcher.match("/{foo}", "/f")); + assertTrue(PathMatcher.match("/{foo}bar", "/bar")); + assertTrue(new PathMatcher("/{foo}bar", false).match("/BAR")); + assertTrue(PathMatcher.match("/foo{bar}", "/foo")); + assertTrue(new PathMatcher("/foo{bar}", false).match("/FOO")); + assertTrue(PathMatcher.match("/{foo}bar/{baz}", "/bar/b")); + assertTrue(new PathMatcher("/{foo}bar/{baz}", false).match("/BAR/B")); + assertTrue(PathMatcher.match("/foo{bar}/{baz}", "/foo/b")); + assertTrue(new PathMatcher("/foo{bar}/{baz}", false).match("/FOO/B")); + assertTrue(PathMatcher.match("/foo{bar}baz", "/foobbaz")); + assertTrue(new PathMatcher("/foo{bar}baz", false).match("/FOObBAZ")); + assertTrue(PathMatcher.match("/foo{bar}baz", "/foobaz")); + assertTrue(new PathMatcher("/foo{bar}baz", false).match("/FOOBAZ")); + + assertFalse(PathMatcher.match("/{foo}bar", "/BAR")); + assertFalse(PathMatcher.match("/{foo}bar", "/ar")); + assertFalse(new PathMatcher("/{foo}bar", false).match("/AR")); + assertFalse(PathMatcher.match("/{foo}bar", "/aaaaaaar")); + assertFalse(new PathMatcher("/{foo}bar", false).match("/AAAAAAAR")); + assertFalse(PathMatcher.match("/foo{bar}", "/FOO")); + assertFalse(PathMatcher.match("/foo{bar}", "/fo")); + assertFalse(new PathMatcher("/foo{bar}", false).match("/FO")); + assertFalse(PathMatcher.match("/foo{bar}", "/fffffffo")); + assertFalse(new PathMatcher("/foo{bar}", false).match("/FFFFFFFFFO")); + assertFalse(PathMatcher.match("/foo{bar}baz", "/FOOBAZ")); + assertFalse(PathMatcher.match("/foo{bar}baz", "/foaz")); + assertFalse(new PathMatcher("/foo{bar}baz", false).match("/FOAZ")); + assertFalse(PathMatcher.match("/foo{bar}baz", "/fooaz")); + assertFalse(new PathMatcher("/foo{bar}baz", false).match("/FOOAZ")); + assertFalse(PathMatcher.match("/foo{bar}baz", "/fobaz")); + assertFalse(new PathMatcher("/foo{bar}baz", false).match("/FOBAZ")); } @Test @@ -305,45 +341,82 @@ void testMatchAndExtractUriTemplateVariables() { assertTrue(new PathMatcher("/foo").matchAndExtractUriTemplateVariables("/foo").isEmpty()); assertNull(new PathMatcher("/foo").matchAndExtractUriTemplateVariables("/bar")); - final Map ret1 = new PathMatcher("/foo/{bar}") + Map ret = new PathMatcher("/foo/{bar}") .matchAndExtractUriTemplateVariables("/foo/baz"); - assertEquals("baz", ret1.get("bar")); - - final Map ret2 = new PathMatcher("/foo/{bar}/{baz}") - .matchAndExtractUriTemplateVariables("/foo/abc/def"); - assertEquals("abc", ret2.get("bar")); - assertEquals("def", ret2.get("baz")); - - final Map ret3 = new PathMatcher("{symbolicName:[\\w\\.]+}-{version:[\\w\\.]+}.jar") + assertEquals("baz", ret.get("bar")); + + ret = new PathMatcher("/foo/{bar}", false) + .matchAndExtractUriTemplateVariables("/FOO/baz"); + assertEquals("baz", ret.get("bar")); + + ret = new PathMatcher("/{foo}/abc{bar}/{baz}def/gh{qux}ijk") + .matchAndExtractUriTemplateVariables("/fff/abc/def/ghijk"); + assertEquals("fff", ret.get("foo")); + assertEquals("", ret.get("bar")); + assertEquals("", ret.get("bar")); + assertEquals("", ret.get("baz")); + + ret = new PathMatcher("/{foo}/abc{bar}/{baz}def/gh{qux}ijk", false) + .matchAndExtractUriTemplateVariables("/fff/ABC/DEF/GHIJK"); + assertEquals("fff", ret.get("foo")); + assertEquals("", ret.get("bar")); + assertEquals("", ret.get("bar")); + assertEquals("", ret.get("baz")); + + ret = new PathMatcher("{symbolicName:[\\w\\.]+}-{version:[\\w\\.]+}.jar") .matchAndExtractUriTemplateVariables("com.example-1.0.0.jar"); - assertEquals("com.example", ret3.get("symbolicName")); - assertEquals("1.0.0", ret3.get("version")); - - - final Map ret4 = - new PathMatcher("{symbolicName:[\\p{L}\\.]+}-sources-{version:[\\p{N}\\.]+}.jar") - .matchAndExtractUriTemplateVariables("com.example-sources-1.0.0.jar"); - assertEquals("com.example", ret4.get("symbolicName")); - assertEquals("1.0.0", ret4.get("version")); - - final Map ret5 = - new PathMatcher("{symbolicName:[\\w\\.]+}-sources-{version:[\\d\\" + - ".]+}-{year:\\d{4}}{month:\\d{2}}{day:\\d{2}}.jar") - .matchAndExtractUriTemplateVariables("com.example-sources-1.0.0-20201229.jar"); - assertEquals("com.example", ret5.get("symbolicName")); - assertEquals("1.0.0", ret5.get("version")); - assertEquals("2020", ret5.get("year")); - assertEquals("12", ret5.get("month")); - assertEquals("29", ret5.get("day")); - - final Map ret6 = - new PathMatcher("{symbolicName:[\\p{L}\\.]+}-sources-{version:[\\p{N}\\.\\{\\}]+}.jar") - .matchAndExtractUriTemplateVariables("com.example-sources-1.0.0.{12}.jar"); - assertEquals("com.example", ret6.get("symbolicName")); - assertEquals("1.0.0.{12}", ret6.get("version")); + assertEquals("com.example", ret.get("symbolicName")); + assertEquals("1.0.0", ret.get("version")); + + ret = new PathMatcher("{symbolicName:[\\w\\.]+}-{version:[\\w\\.]+}.jar", false) + .matchAndExtractUriTemplateVariables("com.example-1.0.0.JAR"); + assertEquals("com.example", ret.get("symbolicName")); + assertEquals("1.0.0", ret.get("version")); + + ret = new PathMatcher("{symbolicName:[\\p{L}\\.]+}-sources-{version:[\\p{N}\\.]+}.jar") + .matchAndExtractUriTemplateVariables("com.example-sources-1.0.0.jar"); + assertEquals("com.example", ret.get("symbolicName")); + assertEquals("1.0.0", ret.get("version")); + + ret = new PathMatcher("{symbolicName:[\\p{L}\\.]+}-sources-{version:[\\p{N}\\.]+}.jar", false) + .matchAndExtractUriTemplateVariables("com.example-SOURCES-1.0.0.JAR"); + assertEquals("com.example", ret.get("symbolicName")); + assertEquals("1.0.0", ret.get("version")); + + ret = new PathMatcher("{symbolicName:[\\w\\.]+}-sources-{version:[\\d\\" + + ".]+}-{year:\\d{4}}{month:\\d{2}}{day:\\d{2}}.jar") + .matchAndExtractUriTemplateVariables("com.example-sources-1.0.0-20201229.jar"); + assertEquals("com.example", ret.get("symbolicName")); + assertEquals("1.0.0", ret.get("version")); + assertEquals("2020", ret.get("year")); + assertEquals("12", ret.get("month")); + assertEquals("29", ret.get("day")); + + ret = new PathMatcher("{symbolicName:[\\w\\.]+}-sources-{version:[\\d\\" + + ".]+}-{year:\\d{4}}{month:\\d{2}}{day:\\d{2}}.jar", false) + .matchAndExtractUriTemplateVariables("com.example-SOURCES-1.0.0-20201229.JAR"); + assertEquals("com.example", ret.get("symbolicName")); + assertEquals("1.0.0", ret.get("version")); + assertEquals("2020", ret.get("year")); + assertEquals("12", ret.get("month")); + assertEquals("29", ret.get("day")); + + ret = new PathMatcher("{symbolicName:[\\p{L}\\.]+}-sources-{version:[\\p{N}\\.\\{\\}]+}.jar") + .matchAndExtractUriTemplateVariables("com.example-sources-1.0.0.{12}.jar"); + assertEquals("com.example", ret.get("symbolicName")); + assertEquals("1.0.0.{12}", ret.get("version")); + + ret = new PathMatcher("{symbolicName:[\\p{L}\\.]+}-sources-{version:[\\p{N}\\.\\{\\}]+}.jar", false) + .matchAndExtractUriTemplateVariables("com.example-SOURCES-1.0.0.{12}.JAR"); + assertEquals("com.example", ret.get("symbolicName")); + assertEquals("1.0.0.{12}", ret.get("version")); assertThrows(IllegalArgumentException.class, () -> new PathMatcher("/foo/{id:bar(baz)?}") .matchAndExtractUriTemplateVariables("/foo/barbaz")); + + assertThrows(IllegalArgumentException.class, + () -> new PathMatcher("/foo/{id:bar(baz)?}", false) + .matchAndExtractUriTemplateVariables("/foo/BARBAZ")); } }