From 9a7a5e5b60f5d5911e158d5a5cd5f388d5e2b44f Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Tue, 18 Nov 2008 15:12:28 +0000 Subject: [PATCH] SPR-5251: URI Templates support /**-style paths git-svn-id: https://src.springframework.org/svn/spring-framework/trunk@295 50f2f4bb-b051-0410-bef5-90022cba6387 --- .../util/AntPatchStringMatcher.java | 212 ++++++++++++++++++ .../springframework/util/AntPathMatcher.java | 194 ++-------------- .../util/AntPathMatcherTests.java | 3 + 3 files changed, 228 insertions(+), 181 deletions(-) create mode 100644 org.springframework.core/src/main/java/org/springframework/util/AntPatchStringMatcher.java diff --git a/org.springframework.core/src/main/java/org/springframework/util/AntPatchStringMatcher.java b/org.springframework.core/src/main/java/org/springframework/util/AntPatchStringMatcher.java new file mode 100644 index 00000000000..013d3ce02c7 --- /dev/null +++ b/org.springframework.core/src/main/java/org/springframework/util/AntPatchStringMatcher.java @@ -0,0 +1,212 @@ +package org.springframework.util; + +import java.util.Map; + +/** + * Package-protected helper class for {@link AntPathMatcher}. Tests whether or not a string matches against a pattern. + * The pattern may contain special characters:
'*' means zero or more characters
'?' means one and only one + * character, '{' and '}' indicate a uri template pattern + * + * @author Arjen Poutsma + * @since 3.0 + */ +class AntPatchStringMatcher { + + private final char[] patArr; + + private final char[] strArr; + + private int patIdxStart = 0; + + private int patIdxEnd; + + private int strIdxStart = 0; + + private int strIdxEnd; + + private char ch; + + private final Map uriTemplateVariables; + + /** + * Constructs a new instance of the AntPatchStringMatcher. + * @param pattern + * @param str + * @param uriTemplateVariables + */ + AntPatchStringMatcher(String pattern, String str, Map uriTemplateVariables) { + patArr = pattern.toCharArray(); + strArr = str.toCharArray(); + this.uriTemplateVariables = uriTemplateVariables; + patIdxEnd = patArr.length - 1; + strIdxEnd = strArr.length - 1; + } + + private void addTemplateVariable(String varName, String varValue) { + if (uriTemplateVariables != null) { + uriTemplateVariables.put(varName, varValue); + } + } + + private void addTemplateVariable(int curlyIdxStart, int curlyIdxEnd, int valIdxStart, int valIdxEnd) { + if (uriTemplateVariables != null) { + String varName = new String(patArr, curlyIdxStart + 1, curlyIdxEnd - curlyIdxStart - 1); + String varValue = new String(strArr, valIdxStart, valIdxEnd - valIdxStart + 1); + uriTemplateVariables.put(varName, varValue); + } + } + + boolean matchStrings() { + if (shortcutPossible()) { + return doShortcut(); + } + if (patternContainsOnlyStar()) { + return true; + } + if (patternContainsOneTemplateVariable()) { + addTemplateVariable(0, patIdxEnd, 0, strIdxEnd); + return true; + } + if (!matchBeforeFirstStarOrCurly()) { + return false; + } + if (allCharsUsed()) { + return onlyStarsLeft(); + } + if (!matchAfterLastStarOrCurly()) { + return false; + } + if (allCharsUsed()) { + return onlyStarsLeft(); + } + // process pattern between stars. padIdxStart and patIdxEnd point + // always to a '*'. + while (patIdxStart != patIdxEnd && strIdxStart <= strIdxEnd) { + int patIdxTmp = findNextStar(); + if (patIdxTmp == patIdxStart + 1 && patArr[patIdxTmp] == '*') { + // Two stars next to each other, skip the first one. + patIdxStart++; + continue; + } + // Find the pattern between padIdxStart & padIdxTmp in str between + // strIdxStart & strIdxEnd + int patLength = (patIdxTmp - patIdxStart - 1); + int strLength = (strIdxEnd - strIdxStart + 1); + int foundIdx = -1; + strLoop: + for (int i = 0; i <= strLength - patLength; i++) { + for (int j = 0; j < patLength; j++) { + ch = patArr[patIdxStart + j + 1]; + if (ch != '?') { + if (ch != strArr[strIdxStart + i + j]) { + continue strLoop; + } + } + } + + foundIdx = strIdxStart + i; + break; + } + + if (foundIdx == -1) { + return false; + } + + patIdxStart = patIdxTmp; + strIdxStart = foundIdx + patLength; + } + + return onlyStarsLeft(); + } + + private int findNextStar() { + for (int i = patIdxStart + 1; i <= patIdxEnd; i++) { + if (patArr[i] == '*') { + return i; + } + } + return -1; + } + + private boolean onlyStarsLeft() { + for (int i = patIdxStart; i <= patIdxEnd; i++) { + if (patArr[i] != '*') { + return false; + } + } + return true; + } + + private boolean allCharsUsed() { + return strIdxStart > strIdxEnd; + } + + private boolean shortcutPossible() { + for (char ch : patArr) { + if (ch == '*' || ch == '{' || ch == '}') { + return false; + } + } + return true; + } + + private boolean doShortcut() { + if (patIdxEnd != strIdxEnd) { + return false; // Pattern and string do not have the same size + } + for (int i = 0; i <= patIdxEnd; i++) { + ch = patArr[i]; + if (ch != '?') { + if (ch != strArr[i]) { + return false;// Character mismatch + } + } + } + return true; // String matches against pattern + } + + private boolean patternContainsOnlyStar() { + return (patIdxEnd == 0 && patArr[0] == '*'); + } + + private boolean patternContainsOneTemplateVariable() { + if ((patIdxEnd >= 2 && patArr[0] == '{' && patArr[patIdxEnd] == '}')) { + for (int i = 1; i < patIdxEnd; i++) { + if (patArr[i] == '}') { + return false; + } + } + return true; + } + else { + return false; + } + } + + private boolean matchBeforeFirstStarOrCurly() { + while ((ch = patArr[patIdxStart]) != '*' && ch != '{' && strIdxStart <= strIdxEnd) { + if (ch != '?') { + if (ch != strArr[strIdxStart]) { + return false; + } + } + patIdxStart++; + strIdxStart++; + } + return true; + } + + private boolean matchAfterLastStarOrCurly() { + while ((ch = patArr[patIdxEnd]) != '*' && ch != '}' && strIdxStart <= strIdxEnd) { + if (ch != '?') { + if (ch != strArr[strIdxEnd]) { + return false; + } + } + patIdxEnd--; + strIdxEnd--; + } + return true; + } + +} diff --git a/org.springframework.core/src/main/java/org/springframework/util/AntPathMatcher.java b/org.springframework.core/src/main/java/org/springframework/util/AntPathMatcher.java index b98a44557fa..01994949d5d 100644 --- a/org.springframework.core/src/main/java/org/springframework/util/AntPathMatcher.java +++ b/org.springframework.core/src/main/java/org/springframework/util/AntPathMatcher.java @@ -18,8 +18,6 @@ package org.springframework.util; import java.util.LinkedHashMap; import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; /** * PathMatcher implementation for Ant-style path patterns. @@ -61,10 +59,6 @@ public class AntPathMatcher implements PathMatcher { /** Default path separator: "/" */ public static final String DEFAULT_PATH_SEPARATOR = "/"; - /** Captures URI template variable names. */ - private static final Pattern URI_TEMPLATE_NAMES_PATTERN = Pattern.compile("\\{([\\w-~_\\.]+?)\\}"); - - private String pathSeparator = DEFAULT_PATH_SEPARATOR; @@ -82,11 +76,11 @@ public class AntPathMatcher implements PathMatcher { } public boolean match(String pattern, String path) { - return doMatch(pattern, path, true); + return doMatch(pattern, path, true, null); } public boolean matchStart(String pattern, String path) { - return doMatch(pattern, path, false); + return doMatch(pattern, path, false, null); } @@ -99,8 +93,7 @@ public class AntPathMatcher implements PathMatcher { * @return true if the supplied path matched, * false if it didn't */ - protected boolean doMatch(String pattern, String path, boolean fullMatch) { - pattern = uriTemplateToAntPattern(pattern); + protected boolean doMatch(String pattern, String path, boolean fullMatch, Map uriTemplateVariables) { if (path.startsWith(this.pathSeparator) != pattern.startsWith(this.pathSeparator)) { return false; } @@ -119,7 +112,7 @@ public class AntPathMatcher implements PathMatcher { if ("**".equals(patDir)) { break; } - if (!matchStrings(patDir, pathDirs[pathIdxStart])) { + if (!matchStrings(patDir, pathDirs[pathIdxStart], uriTemplateVariables)) { return false; } pattIdxStart++; @@ -160,7 +153,7 @@ public class AntPathMatcher implements PathMatcher { if (patDir.equals("**")) { break; } - if (!matchStrings(patDir, pathDirs[pathIdxEnd])) { + if (!matchStrings(patDir, pathDirs[pathIdxEnd], uriTemplateVariables)) { return false; } pattIdxEnd--; @@ -198,9 +191,9 @@ public class AntPathMatcher implements PathMatcher { strLoop: for (int i = 0; i <= strLength - patLength; i++) { for (int j = 0; j < patLength; j++) { - String subPat = (String) pattDirs[pattIdxStart + j + 1]; - String subStr = (String) pathDirs[pathIdxStart + i + j]; - if (!matchStrings(subPat, subStr)) { + String subPat = pattDirs[pattIdxStart + j + 1]; + String subStr = pathDirs[pathIdxStart + i + j]; + if (!matchStrings(subPat, subStr, uriTemplateVariables)) { continue strLoop; } } @@ -237,138 +230,9 @@ public class AntPathMatcher implements PathMatcher { * @return true if the string matches against the * pattern, or false otherwise. */ - private boolean matchStrings(String pattern, String str) { - char[] patArr = pattern.toCharArray(); - char[] strArr = str.toCharArray(); - int patIdxStart = 0; - int patIdxEnd = patArr.length - 1; - int strIdxStart = 0; - int strIdxEnd = strArr.length - 1; - char ch; - - boolean containsStar = false; - for (int i = 0; i < patArr.length; i++) { - if (patArr[i] == '*') { - containsStar = true; - break; - } - } - - if (!containsStar) { - // No '*'s, so we make a shortcut - if (patIdxEnd != strIdxEnd) { - return false; // Pattern and string do not have the same size - } - for (int i = 0; i <= patIdxEnd; i++) { - ch = patArr[i]; - if (ch != '?') { - if (ch != strArr[i]) { - return false;// Character mismatch - } - } - } - return true; // String matches against pattern - } - - - if (patIdxEnd == 0) { - return true; // Pattern contains only '*', which matches anything - } - - // Process characters before first star - while ((ch = patArr[patIdxStart]) != '*' && strIdxStart <= strIdxEnd) { - if (ch != '?') { - if (ch != strArr[strIdxStart]) { - return false;// Character mismatch - } - } - patIdxStart++; - strIdxStart++; - } - if (strIdxStart > strIdxEnd) { - // All characters in the string are used. Check if only '*'s are - // left in the pattern. If so, we succeeded. Otherwise failure. - for (int i = patIdxStart; i <= patIdxEnd; i++) { - if (patArr[i] != '*') { - return false; - } - } - return true; - } - - // Process characters after last star - while ((ch = patArr[patIdxEnd]) != '*' && strIdxStart <= strIdxEnd) { - if (ch != '?') { - if (ch != strArr[strIdxEnd]) { - return false;// Character mismatch - } - } - patIdxEnd--; - strIdxEnd--; - } - if (strIdxStart > strIdxEnd) { - // All characters in the string are used. Check if only '*'s are - // left in the pattern. If so, we succeeded. Otherwise failure. - for (int i = patIdxStart; i <= patIdxEnd; i++) { - if (patArr[i] != '*') { - return false; - } - } - return true; - } - - // process pattern between stars. padIdxStart and patIdxEnd point - // always to a '*'. - while (patIdxStart != patIdxEnd && strIdxStart <= strIdxEnd) { - int patIdxTmp = -1; - for (int i = patIdxStart + 1; i <= patIdxEnd; i++) { - if (patArr[i] == '*') { - patIdxTmp = i; - break; - } - } - if (patIdxTmp == patIdxStart + 1) { - // Two stars next to each other, skip the first one. - patIdxStart++; - continue; - } - // Find the pattern between padIdxStart & padIdxTmp in str between - // strIdxStart & strIdxEnd - int patLength = (patIdxTmp - patIdxStart - 1); - int strLength = (strIdxEnd - strIdxStart + 1); - int foundIdx = -1; - strLoop: - for (int i = 0; i <= strLength - patLength; i++) { - for (int j = 0; j < patLength; j++) { - ch = patArr[patIdxStart + j + 1]; - if (ch != '?') { - if (ch != strArr[strIdxStart + i + j]) { - continue strLoop; - } - } - } - - foundIdx = strIdxStart + i; - break; - } - - if (foundIdx == -1) { - return false; - } - - patIdxStart = patIdxTmp; - strIdxStart = foundIdx + patLength; - } - - // All characters in the string are used. Check if only '*'s are left - // in the pattern. If so, we succeeded. Otherwise failure. - for (int i = patIdxStart; i <= patIdxEnd; i++) { - if (patArr[i] != '*') { - return false; - } - } - - return true; + private boolean matchStrings(String pattern, String str, Map uriTemplateVariables) { + AntPatchStringMatcher matcher = new AntPatchStringMatcher(pattern, str, uriTemplateVariables); + return matcher.matchStrings(); } /** @@ -417,42 +281,10 @@ public class AntPathMatcher implements PathMatcher { return builder.toString(); } - /** - * Replaces URI template variables with Ant-style pattern patchs. Looks for variables within curly braces, and replaces - * those with *. - * - *

For example: /hotels/{hotel}/bookings becomes - * /hotels/*/bookings - * - * @param pattern the pattern, possibly containing URI template variables - * @return the Ant-stlye pattern path - * @see org.springframework.util.AntPathMatcher - */ - private static String uriTemplateToAntPattern(String pattern) { - Matcher matcher = URI_TEMPLATE_NAMES_PATTERN.matcher(pattern); - return matcher.replaceAll("*"); - } - - public Map extractUriTemplateVariables(String pattern, String path) { - if (pattern.contains("**") && pattern.contains("{")) { - throw new IllegalArgumentException("Combining '**' and URI templates is not allowed"); - } - String[] patternParts = StringUtils.tokenizeToStringArray(pattern, this.pathSeparator); - String[] pathParts = StringUtils.tokenizeToStringArray(path, this.pathSeparator); - Map variables = new LinkedHashMap(); - - for (int i = 0; i < patternParts.length && i < pathParts.length; i++) { - String patternPart = patternParts[i]; - String pathPart = pathParts[i]; - int patternEnd = patternPart.length() -1 ; - if (patternEnd > 1 && patternPart.charAt(0) == '{' && patternPart.charAt(patternEnd) == '}') { - String varName = patternPart.substring(1, patternEnd); - variables.put(varName, pathPart); - } - } - + boolean result = doMatch(pattern, path, true, variables); + Assert.state(result, "Pattern \"" + pattern + "\" is not a match for \"" + path + "\""); return variables; } diff --git a/org.springframework.core/src/test/java/org/springframework/util/AntPathMatcherTests.java b/org.springframework.core/src/test/java/org/springframework/util/AntPathMatcherTests.java index c855c66e1eb..3fe76a28d3b 100644 --- a/org.springframework.core/src/test/java/org/springframework/util/AntPathMatcherTests.java +++ b/org.springframework.core/src/test/java/org/springframework/util/AntPathMatcherTests.java @@ -303,6 +303,9 @@ public class AntPathMatcherTests { expected.put("hotel", "1"); expected.put("booking", "2"); assertEquals(expected, result); + + result = pathMatcher.extractUriTemplateVariables("/**/hotels/**/{hotel}", "/foo/hotels/bar/1"); + assertEquals(Collections.singletonMap("hotel", "1"), result); }