diff --git a/spring-web/src/main/java/org/springframework/web/util/pattern/CaptureTheRestPathElement.java b/spring-web/src/main/java/org/springframework/web/util/pattern/CaptureTheRestPathElement.java index f5529188210..13df50e691d 100644 --- a/spring-web/src/main/java/org/springframework/web/util/pattern/CaptureTheRestPathElement.java +++ b/spring-web/src/main/java/org/springframework/web/util/pattern/CaptureTheRestPathElement.java @@ -16,8 +16,15 @@ package org.springframework.web.util.pattern; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import org.springframework.web.util.pattern.PathPattern.MatchingContext; +import java.util.List; + +import org.springframework.http.server.reactive.PathContainer.Element; +import org.springframework.http.server.reactive.PathContainer.Segment; + /** * A path element representing capturing the rest of a path. In the pattern * '/foo/{*foobar}' the /{*foobar} is represented as a {@link CaptureTheRestPathElement}. @@ -42,25 +49,52 @@ class CaptureTheRestPathElement extends PathElement { @Override - public boolean matches(int candidateIndex, MatchingContext matchingContext) { + public boolean matches(int pathIndex, MatchingContext matchingContext) { // No need to handle 'match start' checking as this captures everything // anyway and cannot be followed by anything else // assert next == null // If there is more data, it must start with the separator - if (candidateIndex < matchingContext.candidateLength && - matchingContext.candidate[candidateIndex] != separator) { + if (pathIndex < matchingContext.pathLength && !matchingContext.isSeparator(pathIndex)) { return false; } if (matchingContext.determineRemainingPath) { - matchingContext.remainingPathIndex = matchingContext.candidateLength; + matchingContext.remainingPathIndex = matchingContext.pathLength; } if (matchingContext.extractingVariables) { - matchingContext.set(variableName, decode(new String(matchingContext.candidate, candidateIndex, - matchingContext.candidateLength - candidateIndex))); + // Collect the parameters from all the remaining segments + MultiValueMap parametersCollector = null; + for (int i = pathIndex; i < matchingContext.pathLength; i++) { + Element element = matchingContext.pathElements.get(i); + if (element instanceof Segment) { + MultiValueMap parameters = ((Segment)element).parameters(); + if (parameters != null && parameters.size()!=0) { + if (parametersCollector == null) { + parametersCollector = new LinkedMultiValueMap<>(); + } + parametersCollector.addAll(parameters); + } + } + } + matchingContext.set(variableName, pathToString(pathIndex, matchingContext.pathElements), + parametersCollector == null?NO_PARAMETERS:parametersCollector); } return true; } + + private String pathToString(int fromSegment, List pathElements) { + StringBuilder buf = new StringBuilder(); + for (int i = fromSegment, max = pathElements.size(); i < max; i++) { + Element element = pathElements.get(i); + if (element instanceof Segment) { + buf.append(((Segment)element).valueDecoded()); + } + else { + buf.append(element.value()); + } + } + return buf.toString(); + } @Override public int getNormalizedLength() { @@ -82,4 +116,8 @@ class CaptureTheRestPathElement extends PathElement { return "CaptureTheRest(/{*" + this.variableName + "})"; } + @Override + public char[] getChars() { + return ("/{*"+this.variableName+"}").toCharArray(); + } } diff --git a/spring-web/src/main/java/org/springframework/web/util/pattern/CaptureVariablePathElement.java b/spring-web/src/main/java/org/springframework/web/util/pattern/CaptureVariablePathElement.java index 5fac2e85323..f5c3051618c 100644 --- a/spring-web/src/main/java/org/springframework/web/util/pattern/CaptureVariablePathElement.java +++ b/spring-web/src/main/java/org/springframework/web/util/pattern/CaptureVariablePathElement.java @@ -22,6 +22,7 @@ import java.util.regex.Pattern; import org.springframework.lang.Nullable; import org.springframework.web.util.UriUtils; +import org.springframework.http.server.reactive.PathContainer.Segment; /** * A path element representing capturing a piece of the path as a variable. In the pattern @@ -72,24 +73,18 @@ class CaptureVariablePathElement extends PathElement { @Override - public boolean matches(int candidateIndex, PathPattern.MatchingContext matchingContext) { - int nextPos = matchingContext.scanAhead(candidateIndex); - // There must be at least one character to capture: - if (nextPos == candidateIndex) { + public boolean matches(int pathIndex, PathPattern.MatchingContext matchingContext) { + if (pathIndex >= matchingContext.pathLength) { + // no more path left to match this element + return false; + } + String candidateCapture = matchingContext.pathElementValue(pathIndex); + if (candidateCapture.length() == 0) { return false; } - String substringForDecoding = null; - CharSequence candidateCapture = null; if (this.constraintPattern != null) { // TODO possible optimization - only regex match if rest of pattern matches? Benefit likely to vary pattern to pattern - if (includesPercent(matchingContext.candidate, candidateIndex, nextPos)) { - substringForDecoding = new String(matchingContext.candidate, candidateIndex, nextPos); - candidateCapture = UriUtils.decode(substringForDecoding, StandardCharsets.UTF_8); - } - else { - candidateCapture = new SubSequence(matchingContext.candidate, candidateIndex, nextPos); - } Matcher matcher = constraintPattern.matcher(candidateCapture); if (matcher.groupCount() != 0) { throw new IllegalArgumentException( @@ -101,34 +96,33 @@ class CaptureVariablePathElement extends PathElement { } boolean match = false; - if (this.next == null) { - if (matchingContext.determineRemainingPath && nextPos > candidateIndex) { - matchingContext.remainingPathIndex = nextPos; + pathIndex++; + if (isNoMorePattern()) { + if (matchingContext.determineRemainingPath) { + matchingContext.remainingPathIndex = pathIndex; match = true; } else { // Needs to be at least one character #SPR15264 - match = (nextPos == matchingContext.candidateLength && nextPos > candidateIndex); + match = (pathIndex == matchingContext.pathLength); if (!match && matchingContext.isAllowOptionalTrailingSlash()) { - match = (nextPos > candidateIndex) && - (nextPos + 1) == matchingContext.candidateLength && - matchingContext.candidate[nextPos] == separator; + match = //(nextPos > candidateIndex) && + (pathIndex + 1) == matchingContext.pathLength && + matchingContext.isSeparator(pathIndex); } } } else { - if (matchingContext.isMatchStartMatching && nextPos == matchingContext.candidateLength) { + if (matchingContext.isMatchStartMatching && pathIndex == matchingContext.pathLength) { match = true; // no more data but matches up to this point } else { - match = this.next.matches(nextPos, matchingContext); + match = this.next.matches(pathIndex, matchingContext); } } if (match && matchingContext.extractingVariables) { - matchingContext.set(this.variableName, - candidateCapture != null ? candidateCapture.toString(): - decode(new String(matchingContext.candidate, candidateIndex, nextPos - candidateIndex))); + matchingContext.set(this.variableName, candidateCapture, ((Segment)matchingContext.pathElements.get(pathIndex-1)).parameters()); } return match; } @@ -163,4 +157,15 @@ class CaptureVariablePathElement extends PathElement { (this.constraintPattern != null ? ":" + this.constraintPattern.pattern() : "") + "})"; } + public char[] getChars() { + StringBuilder b = new StringBuilder(); + b.append("{"); + b.append(this.variableName); + if (this.constraintPattern != null) { + b.append(":").append(this.constraintPattern.pattern()); + } + b.append("}"); + return b.toString().toCharArray(); + } + } diff --git a/spring-web/src/main/java/org/springframework/web/util/pattern/InternalPathPatternParser.java b/spring-web/src/main/java/org/springframework/web/util/pattern/InternalPathPatternParser.java index 7622af1506f..6aa6ef67d73 100644 --- a/spring-web/src/main/java/org/springframework/web/util/pattern/InternalPathPatternParser.java +++ b/spring-web/src/main/java/org/springframework/web/util/pattern/InternalPathPatternParser.java @@ -34,6 +34,8 @@ import org.springframework.web.util.pattern.PatternParseException.PatternMessage */ class InternalPathPatternParser { + private PathPatternParser parser; + // The expected path separator to split path elements during parsing char separator = PathPatternParser.DEFAULT_SEPARATOR; @@ -99,6 +101,9 @@ class InternalPathPatternParser { this.separator = separator; this.caseSensitive = caseSensitive; this.matchOptionalTrailingSlash = matchOptionalTrailingSlash; + this.parser = new PathPatternParser(this.separator); + this.parser.setCaseSensitive(this.caseSensitive); + this.parser.setMatchOptionalTrailingSlash(this.matchOptionalTrailingSlash); } @@ -112,6 +117,9 @@ class InternalPathPatternParser { * @throws PatternParseException in case of parse errors */ public PathPattern parse(String pathPattern) throws PatternParseException { + if (pathPattern == null) { + pathPattern = ""; + } this.pathPatternData = pathPattern.toCharArray(); this.pathPatternLength = pathPatternData.length; this.headPE = null; @@ -205,7 +213,7 @@ class InternalPathPatternParser { pushPathElement(createPathElement()); } return new PathPattern( - pathPattern, this.headPE, this.separator, this.caseSensitive, this.matchOptionalTrailingSlash); + pathPattern, this.parser, this.headPE, this.separator, this.caseSensitive, this.matchOptionalTrailingSlash); } /** @@ -311,17 +319,10 @@ class InternalPathPatternParser { resetPathElementState(); } - private char[] getPathElementText(boolean encodeElement) { + private char[] getPathElementText() { char[] pathElementText = new char[this.pos - this.pathElementStart]; - if (encodeElement) { - String unencoded = new String(this.pathPatternData, this.pathElementStart, this.pos - this.pathElementStart); - String encoded = UriUtils.encodeFragment(unencoded, StandardCharsets.UTF_8); - pathElementText = encoded.toCharArray(); - } - else { - System.arraycopy(this.pathPatternData, this.pathElementStart, pathElementText, 0, - this.pos - this.pathElementStart); - } + System.arraycopy(this.pathPatternData, this.pathElementStart, pathElementText, 0, + this.pos - this.pathElementStart); return pathElementText; } @@ -342,12 +343,12 @@ class InternalPathPatternParser { this.pathPatternData[this.pos - 1] == '}') { if (this.isCaptureTheRestVariable) { // It is {*....} - newPE = new CaptureTheRestPathElement(pathElementStart, getPathElementText(false), separator); + newPE = new CaptureTheRestPathElement(pathElementStart, getPathElementText(), separator); } else { // It is a full capture of this element (possibly with constraint), for example: /foo/{abc}/ try { - newPE = new CaptureVariablePathElement(this.pathElementStart, getPathElementText(false), + newPE = new CaptureVariablePathElement(this.pathElementStart, getPathElementText(), this.caseSensitive, this.separator); } catch (PatternSyntaxException pse) { @@ -365,7 +366,7 @@ class InternalPathPatternParser { PatternMessage.CAPTURE_ALL_IS_STANDALONE_CONSTRUCT); } RegexPathElement newRegexSection = new RegexPathElement(this.pathElementStart, - getPathElementText(false), this.caseSensitive, + getPathElementText(), this.caseSensitive, this.pathPatternData, this.separator); for (String variableName : newRegexSection.getVariableNames()) { recordCapturedVariable(this.pathElementStart, variableName); @@ -379,16 +380,16 @@ class InternalPathPatternParser { newPE = new WildcardPathElement(this.pathElementStart, this.separator); } else { - newPE = new RegexPathElement(this.pathElementStart, getPathElementText(false), + newPE = new RegexPathElement(this.pathElementStart, getPathElementText(), this.caseSensitive, this.pathPatternData, this.separator); } } else if (this.singleCharWildcardCount != 0) { - newPE = new SingleCharWildcardedPathElement(this.pathElementStart, getPathElementText(true), + newPE = new SingleCharWildcardedPathElement(this.pathElementStart, getPathElementText(), this.singleCharWildcardCount, this.caseSensitive, this.separator); } else { - newPE = new LiteralPathElement(this.pathElementStart, getPathElementText(true), + newPE = new LiteralPathElement(this.pathElementStart, getPathElementText(), this.caseSensitive, this.separator); } } diff --git a/spring-web/src/main/java/org/springframework/web/util/pattern/LiteralPathElement.java b/spring-web/src/main/java/org/springframework/web/util/pattern/LiteralPathElement.java index 809dc4a467a..4d485a6107e 100644 --- a/spring-web/src/main/java/org/springframework/web/util/pattern/LiteralPathElement.java +++ b/spring-web/src/main/java/org/springframework/web/util/pattern/LiteralPathElement.java @@ -16,6 +16,8 @@ package org.springframework.web.util.pattern; +import org.springframework.http.server.reactive.PathContainer.Element; +import org.springframework.http.server.reactive.PathContainer.Segment; import org.springframework.web.util.pattern.PathPattern.MatchingContext; /** @@ -49,52 +51,60 @@ class LiteralPathElement extends PathElement { } @Override - public boolean matches(int candidateIndex, MatchingContext matchingContext) { - if ((candidateIndex + text.length) > matchingContext.candidateLength) { - return false; // not enough data, cannot be a match + public boolean matches(int pathIndex, MatchingContext matchingContext) { + if (pathIndex >= matchingContext.pathLength) { + // no more path left to match this element + return false; + } + Element element = matchingContext.pathElements.get(pathIndex); + if (!(element instanceof Segment)) { + return false; + } + String value = ((Segment)element).valueDecoded(); + if (value.length() != len) { + // Not enough data to match this path element + return false; } + char[] data = ((Segment)element).valueDecodedChars(); if (this.caseSensitive) { for (int i = 0; i < len; i++) { - if (matchingContext.candidate[candidateIndex++] != this.text[i]) { - // TODO unfortunate performance hit here on comparison when encoded data is the less likely case - if (i < 3 || matchingContext.candidate[candidateIndex-3] != '%' || - Character.toUpperCase(matchingContext.candidate[candidateIndex-1]) != this.text[i]) { - return false; - } + if (data[i] != this.text[i]) { + return false; } } } else { for (int i = 0; i < len; i++) { // TODO revisit performance if doing a lot of case insensitive matching - if (Character.toLowerCase(matchingContext.candidate[candidateIndex++]) != this.text[i]) { + if (Character.toLowerCase(data[i]) != this.text[i]) { return false; } } } - if (this.next == null) { - if (matchingContext.determineRemainingPath && nextIfExistsIsSeparator(candidateIndex, matchingContext)) { - matchingContext.remainingPathIndex = candidateIndex; + pathIndex++; + if (isNoMorePattern()) { + if (matchingContext.determineRemainingPath) { + matchingContext.remainingPathIndex = pathIndex; return true; } else { - if (candidateIndex == matchingContext.candidateLength) { + if (pathIndex == matchingContext.pathLength) { return true; } else { return (matchingContext.isAllowOptionalTrailingSlash() && - (candidateIndex + 1) == matchingContext.candidateLength && - matchingContext.candidate[candidateIndex] == separator); + (pathIndex + 1) == matchingContext.pathLength && + matchingContext.isSeparator(pathIndex)); } } } else { - if (matchingContext.isMatchStartMatching && candidateIndex == matchingContext.candidateLength) { + if (matchingContext.isMatchStartMatching && pathIndex == matchingContext.pathLength) { return true; // no more data but everything matched so far } - return this.next.matches(candidateIndex, matchingContext); + return this.next.matches(pathIndex, matchingContext); } } @@ -107,5 +117,9 @@ class LiteralPathElement extends PathElement { public String toString() { return "Literal(" + String.valueOf(this.text) + ")"; } + + public char[] getChars() { + return this.text; + } } diff --git a/spring-web/src/main/java/org/springframework/web/util/pattern/ParsingPathMatcher.java b/spring-web/src/main/java/org/springframework/web/util/pattern/ParsingPathMatcher.java index 72c0c64b416..041f5d247a2 100644 --- a/spring-web/src/main/java/org/springframework/web/util/pattern/ParsingPathMatcher.java +++ b/spring-web/src/main/java/org/springframework/web/util/pattern/ParsingPathMatcher.java @@ -16,13 +16,18 @@ package org.springframework.web.util.pattern; +import java.nio.charset.StandardCharsets; +import java.util.Collections; import java.util.Comparator; +import java.util.LinkedHashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import org.springframework.http.server.reactive.PathContainer; import org.springframework.lang.Nullable; import org.springframework.util.PathMatcher; +import org.springframework.web.util.pattern.PathPattern.PathMatchResult; /** * {@link PathMatcher} implementation for path patterns parsed @@ -56,13 +61,13 @@ public class ParsingPathMatcher implements PathMatcher { @Override public boolean match(String pattern, String path) { PathPattern pathPattern = getPathPattern(pattern); - return pathPattern.matches(path); + return pathPattern.matches(PathContainer.parse(path, StandardCharsets.UTF_8)); } @Override public boolean matchStart(String pattern, String path) { PathPattern pathPattern = getPathPattern(pattern); - return pathPattern.matchStart(path); + return pathPattern.matchStart(PathContainer.parse(path, StandardCharsets.UTF_8)); } @Override @@ -74,7 +79,19 @@ public class ParsingPathMatcher implements PathMatcher { @Override public Map extractUriTemplateVariables(String pattern, String path) { PathPattern pathPattern = getPathPattern(pattern); - return pathPattern.matchAndExtract(path); + Map results = pathPattern.matchAndExtract(PathContainer.parse(path, StandardCharsets.UTF_8)); + // Collapse PathMatchResults to simple value results (path parameters are lost in this translation) + Map boundVariables = null; + if (results.size() == 0) { + boundVariables = Collections.emptyMap(); + } + else { + boundVariables = new LinkedHashMap<>(); + for (Map.Entry entries: results.entrySet()) { + boundVariables.put(entries.getKey(), entries.getValue().value()); + } + } + return boundVariables; } @Override @@ -85,7 +102,7 @@ public class ParsingPathMatcher implements PathMatcher { @Override public String combine(String pattern1, String pattern2) { PathPattern pathPattern = getPathPattern(pattern1); - return pathPattern.combine(pattern2); + return pathPattern.combine(getPathPattern(pattern2)).getPatternString(); } private PathPattern getPathPattern(String pattern) { diff --git a/spring-web/src/main/java/org/springframework/web/util/pattern/PathElement.java b/spring-web/src/main/java/org/springframework/web/util/pattern/PathElement.java index bd23ec5756d..f75fbc1e376 100644 --- a/spring-web/src/main/java/org/springframework/web/util/pattern/PathElement.java +++ b/spring-web/src/main/java/org/springframework/web/util/pattern/PathElement.java @@ -19,6 +19,8 @@ package org.springframework.web.util.pattern; import java.nio.charset.StandardCharsets; import org.springframework.lang.Nullable; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import org.springframework.web.util.UriUtils; import org.springframework.web.util.pattern.PathPattern.MatchingContext; @@ -35,6 +37,7 @@ abstract class PathElement { protected static final int CAPTURE_VARIABLE_WEIGHT = 1; + protected final static MultiValueMap NO_PARAMETERS = new LinkedMultiValueMap<>(); // Position in the pattern where this path element starts protected final int pos; @@ -75,6 +78,8 @@ abstract class PathElement { */ public abstract int getNormalizedLength(); + public abstract char[] getChars(); + /** * Return the number of variables captured by the path element. */ @@ -97,52 +102,10 @@ abstract class PathElement { } /** - * Return {@code true} if there is no next character, or if there is then it is a separator. - */ - protected boolean nextIfExistsIsSeparator(int nextIndex, MatchingContext matchingContext) { - return (nextIndex >= matchingContext.candidateLength || - matchingContext.candidate[nextIndex] == this.separator); - } - - /** - * Decode an input CharSequence if necessary. - * @param toDecode the input char sequence that should be decoded if necessary - * @return the decoded result - */ - protected String decode(CharSequence toDecode) { - CharSequence decoded = toDecode; - if (includesPercent(toDecode)) { - decoded = UriUtils.decode(toDecode.toString(), StandardCharsets.UTF_8); - } - return decoded.toString(); - } - - /** - * @param chars sequence of characters - * @param from start position (included in check) - * @param to end position (excluded from check) - * @return true if the chars array includes a '%' character between the specified positions - */ - protected boolean includesPercent(char[] chars, int from, int to) { - for (int i = from; i < to; i++) { - if (chars[i] == '%') { - return true; - } - } - return false; - } - - /** - * @param chars string that may include a '%' character indicating it is encoded - * @return true if the string contains a '%' character + * @return true if the there are no more PathElements in the pattern */ - protected boolean includesPercent(CharSequence chars) { - for (int i = 0, max = chars.length(); i < max; i++) { - if (chars.charAt(i) == '%') { - return true; - } - } - return false; + protected final boolean isNoMorePattern() { + return this.next == null; } } diff --git a/spring-web/src/main/java/org/springframework/web/util/pattern/PathPattern.java b/spring-web/src/main/java/org/springframework/web/util/pattern/PathPattern.java index 94d74b1d7c8..179880163e1 100644 --- a/spring-web/src/main/java/org/springframework/web/util/pattern/PathPattern.java +++ b/spring-web/src/main/java/org/springframework/web/util/pattern/PathPattern.java @@ -16,11 +16,18 @@ package org.springframework.web.util.pattern; +import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; +import org.springframework.http.server.reactive.PathContainer; +import org.springframework.http.server.reactive.PathContainer.Element; +import org.springframework.http.server.reactive.PathContainer.Segment; +import org.springframework.http.server.reactive.PathContainer.Separator; import org.springframework.lang.Nullable; +import org.springframework.util.MultiValueMap; import org.springframework.util.PathMatcher; import org.springframework.util.StringUtils; @@ -63,9 +70,14 @@ import org.springframework.util.StringUtils; */ public class PathPattern implements Comparable { + private final static PathContainer EMPTY_PATH = PathContainer.parse("", StandardCharsets.UTF_8); + + /** The parser used to construct this pattern */ + private final PathPatternParser parser; + /** First path element in the parsed chain of path elements for this pattern */ @Nullable - private PathElement head; + private final PathElement head; /** The text of the parsed pattern */ private String patternString; @@ -109,10 +121,10 @@ public class PathPattern implements Comparable { private boolean catchAll = false; - PathPattern(String patternText, PathElement head, char separator, boolean caseSensitive, + PathPattern(String patternText, PathPatternParser parser, PathElement head, char separator, boolean caseSensitive, boolean allowOptionalTrailingSlash) { - this.patternString = patternText; + this.parser = parser; this.head = head; this.separator = separator; this.caseSensitive = caseSensitive; @@ -137,54 +149,48 @@ public class PathPattern implements Comparable { /** - * Return the original pattern string that was parsed to create this PathPattern. + * @return the original pattern string that was parsed to create this PathPattern. */ public String getPatternString() { return this.patternString; } - @Nullable - PathElement getHeadSection() { - return this.head; - } - - /** - * @param path the candidate path to attempt to match against this pattern + * @param pathContainer the candidate path container to attempt to match against this pattern * @return true if the path matches this pattern */ - public boolean matches(String path) { + public boolean matches(PathContainer pathContainer) { if (this.head == null) { - return !StringUtils.hasLength(path); + return !hasLength(pathContainer); } - else if (!StringUtils.hasLength(path)) { + else if (!hasLength(pathContainer)) { if (this.head instanceof WildcardTheRestPathElement || this.head instanceof CaptureTheRestPathElement) { - path = ""; // Will allow CaptureTheRest to bind the variable to empty + pathContainer = EMPTY_PATH; // Will allow CaptureTheRest to bind the variable to empty } else { return false; } } - MatchingContext matchingContext = new MatchingContext(path, false); + MatchingContext matchingContext = new MatchingContext(pathContainer, false); return this.head.matches(0, matchingContext); } - + /** * For a given path return the remaining piece that is not covered by this PathPattern. - * @param path a path that may or may not match this path pattern + * @param pathContainer a path that may or may not match this path pattern * @return a {@link PathRemainingMatchInfo} describing the match result, * or {@code null} if the path does not match this pattern */ @Nullable - public PathRemainingMatchInfo getPathRemaining(String path) { + public PathRemainingMatchInfo getPathRemaining(@Nullable PathContainer pathContainer) { if (this.head == null) { - return new PathRemainingMatchInfo(path); + return new PathRemainingMatchInfo(pathContainer); } - else if (!StringUtils.hasLength(path)) { + else if (!hasLength(pathContainer)) { return null; } - MatchingContext matchingContext = new MatchingContext(path, true); + MatchingContext matchingContext = new MatchingContext(pathContainer, true); matchingContext.setMatchAllowExtraPath(); boolean matches = this.head.matches(0, matchingContext); if (!matches) { @@ -192,11 +198,11 @@ public class PathPattern implements Comparable { } else { PathRemainingMatchInfo info; - if (matchingContext.remainingPathIndex == path.length()) { - info = new PathRemainingMatchInfo("", matchingContext.getExtractedVariables()); + if (matchingContext.remainingPathIndex == pathContainer.elements().size()) { + info = new PathRemainingMatchInfo(EMPTY_PATH, matchingContext.getExtractedVariables()); } else { - info = new PathRemainingMatchInfo(path.substring(matchingContext.remainingPathIndex), + info = new PathRemainingMatchInfo(PathContainer.subPath(pathContainer, matchingContext.remainingPathIndex), matchingContext.getExtractedVariables()); } return info; @@ -204,36 +210,36 @@ public class PathPattern implements Comparable { } /** - * @param path the path to check against the pattern + * @param pathContainer the path to check against the pattern * @return true if the pattern matches as much of the path as is supplied */ - public boolean matchStart(String path) { + public boolean matchStart(PathContainer pathContainer) { if (this.head == null) { - return !StringUtils.hasLength(path); + return !hasLength(pathContainer); } - else if (!StringUtils.hasLength(path)) { + else if (!hasLength(pathContainer)) { return true; } - MatchingContext matchingContext = new MatchingContext(path, false); + MatchingContext matchingContext = new MatchingContext(pathContainer, false); matchingContext.setMatchStartMatching(true); return this.head.matches(0, matchingContext); } /** - * @param path a path that matches this pattern from which to extract variables + * @param pathContainer a path that matches this pattern from which to extract variables * @return a map of extracted variables - an empty map if no variables extracted. * @throws IllegalStateException if the path does not match the pattern */ - public Map matchAndExtract(String path) { - MatchingContext matchingContext = new MatchingContext(path, true); + public Map matchAndExtract(PathContainer pathContainer) { + MatchingContext matchingContext = new MatchingContext(pathContainer, true); if (this.head != null && this.head.matches(0, matchingContext)) { return matchingContext.getExtractedVariables(); } - else if (!StringUtils.hasLength(path)) { + else if (!hasLength(pathContainer)) { return Collections.emptyMap(); } else { - throw new IllegalStateException("Pattern \"" + this + "\" is not a match for \"" + path + "\""); + throw new IllegalStateException("Pattern \"" + this + "\" is not a match for \"" + pathContainer.value() + "\""); } } @@ -367,48 +373,21 @@ public class PathPattern implements Comparable { return (lenDifference < 0) ? +1 : (lenDifference == 0 ? 0 : -1); } - int getScore() { - return this.score; - } - - boolean isCatchAll() { - return this.catchAll; - } - - /** - * The normalized length is trying to measure the 'active' part of the pattern. It is computed - * by assuming all capture variables have a normalized length of 1. Effectively this means changing - * your variable name lengths isn't going to change the length of the active part of the pattern. - * Useful when comparing two patterns. - */ - int getNormalizedLength() { - return this.normalizedLength; - } - - char getSeparator() { - return this.separator; - } - - int getCapturedVariableCount() { - return this.capturedVariableCount; - } - - /** * Combine this pattern with another. Currently does not produce a new PathPattern, just produces a new string. */ - public String combine(String pattern2string) { + public PathPattern combine(PathPattern pattern2string) { // If one of them is empty the result is the other. If both empty the result is "" if (!StringUtils.hasLength(this.patternString)) { - if (!StringUtils.hasLength(pattern2string)) { - return ""; + if (!StringUtils.hasLength(pattern2string.patternString)) { + return parser.parse(""); } else { return pattern2string; } } - else if (!StringUtils.hasLength(pattern2string)) { - return this.patternString; + else if (!StringUtils.hasLength(pattern2string.patternString)) { + return this; } // /* + /hotel => /hotel @@ -416,61 +395,40 @@ public class PathPattern implements Comparable { // However: // /usr + /user => /usr/user // /{foo} + /bar => /{foo}/bar - if (!this.patternString.equals(pattern2string) &&this. capturedVariableCount == 0 && matches(pattern2string)) { + if (!this.patternString.equals(pattern2string.patternString) && this.capturedVariableCount == 0 && + matches(PathContainer.parse(pattern2string.patternString, StandardCharsets.UTF_8))) { return pattern2string; } // /hotels/* + /booking => /hotels/booking // /hotels/* + booking => /hotels/booking if (this.endsWithSeparatorWildcard) { - return concat(this.patternString.substring(0, this.patternString.length() - 2), pattern2string); + return parser.parse(concat(this.patternString.substring(0, this.patternString.length() - 2), pattern2string.patternString)); } // /hotels + /booking => /hotels/booking // /hotels + booking => /hotels/booking int starDotPos1 = this.patternString.indexOf("*."); // Are there any file prefix/suffix things to consider? if (this.capturedVariableCount != 0 || starDotPos1 == -1 || this.separator == '.') { - return concat(this.patternString, pattern2string); + return parser.parse(concat(this.patternString, pattern2string.patternString)); } // /*.html + /hotel => /hotel.html // /*.html + /hotel.* => /hotel.html String firstExtension = this.patternString.substring(starDotPos1 + 1); // looking for the first extension - int dotPos2 = pattern2string.indexOf('.'); - String file2 = (dotPos2 == -1 ? pattern2string : pattern2string.substring(0, dotPos2)); - String secondExtension = (dotPos2 == -1 ? "" : pattern2string.substring(dotPos2)); + String p2string = pattern2string.patternString; + int dotPos2 = p2string.indexOf('.'); + String file2 = (dotPos2 == -1 ? p2string : p2string.substring(0, dotPos2)); + String secondExtension = (dotPos2 == -1 ? "" : p2string.substring(dotPos2)); boolean firstExtensionWild = (firstExtension.equals(".*") || firstExtension.equals("")); boolean secondExtensionWild = (secondExtension.equals(".*") || secondExtension.equals("")); if (!firstExtensionWild && !secondExtensionWild) { throw new IllegalArgumentException( "Cannot combine patterns: " + this.patternString + " and " + pattern2string); } - return file2 + (firstExtensionWild ? secondExtension : firstExtension); + return parser.parse(file2 + (firstExtensionWild ? secondExtension : firstExtension)); } - /** - * Join two paths together including a separator if necessary. - * Extraneous separators are removed (if the first path - * ends with one and the second path starts with one). - * @param path1 first path - * @param path2 second path - * @return joined path that may include separator if necessary - */ - private String concat(String path1, String path2) { - boolean path1EndsWithSeparator = (path1.charAt(path1.length() - 1) == this.separator); - boolean path2StartsWithSeparator = (path2.charAt(0) == this.separator); - if (path1EndsWithSeparator && path2StartsWithSeparator) { - return path1 + path2.substring(1); - } - else if (path1EndsWithSeparator || path2StartsWithSeparator) { - return path1 + path2; - } - else { - return path1 + this.separator + path2; - } - } - - public boolean equals(Object other) { if (!(other instanceof PathPattern)) { return false; @@ -489,16 +447,47 @@ public class PathPattern implements Comparable { return this.patternString; } - String toChainString() { - StringBuilder buf = new StringBuilder(); - PathElement pe = this.head; - while (pe != null) { - buf.append(pe.toString()).append(" "); - pe = pe.next; + /** + * Represents the result of a successful variable match. This holds the key that matched, the + * value that was found for that key and, if any, the parameters attached to that path element. + * For example: "/{var}" against "/foo;a=b" will return a PathMathResult with 'key=var', + * 'value=foo' and parameters 'a=b'. + */ + public static class PathMatchResult { + + private final String key; + + private final String value; + + private final MultiValueMap parameters; + + public PathMatchResult(String key, String value, MultiValueMap parameters) { + this.key = key; + this.value = value; + this.parameters = parameters; } - return buf.toString().trim(); - } + /** + * @return match result key + */ + public String key() { + return key; + } + + /** + * @return match result value + */ + public String value() { + return this.value; + } + + /** + * @return match result parameters (empty map if no parameters) + */ + public MultiValueMap parameters() { + return this.parameters; + } + } /** * A holder for the result of a {@link PathPattern#getPathRemaining(String)} call. Holds @@ -507,15 +496,15 @@ public class PathPattern implements Comparable { */ public static class PathRemainingMatchInfo { - private final String pathRemaining; + private final PathContainer pathRemaining; - private final Map matchingVariables; + private final Map matchingVariables; - PathRemainingMatchInfo(String pathRemaining) { + PathRemainingMatchInfo(@Nullable PathContainer pathRemaining) { this(pathRemaining, Collections.emptyMap()); } - PathRemainingMatchInfo(String pathRemaining, Map matchingVariables) { + PathRemainingMatchInfo(@Nullable PathContainer pathRemaining, Map matchingVariables) { this.pathRemaining = pathRemaining; this.matchingVariables = matchingVariables; } @@ -524,18 +513,71 @@ public class PathPattern implements Comparable { * Return the part of a path that was not matched by a pattern. */ public String getPathRemaining() { - return this.pathRemaining; + return this.pathRemaining == null ? null: this.pathRemaining.value(); } /** * Return variables that were bound in the part of the path that was successfully matched. * Will be an empty map if no variables were bound */ - public Map getMatchingVariables() { + public Map getMatchingVariables() { return this.matchingVariables; } } + int getScore() { + return this.score; + } + + boolean isCatchAll() { + return this.catchAll; + } + + /** + * The normalized length is trying to measure the 'active' part of the pattern. It is computed + * by assuming all capture variables have a normalized length of 1. Effectively this means changing + * your variable name lengths isn't going to change the length of the active part of the pattern. + * Useful when comparing two patterns. + */ + int getNormalizedLength() { + return this.normalizedLength; + } + + char getSeparator() { + return this.separator; + } + + int getCapturedVariableCount() { + return this.capturedVariableCount; + } + + String toChainString() { + StringBuilder buf = new StringBuilder(); + PathElement pe = this.head; + while (pe != null) { + buf.append(pe.toString()).append(" "); + pe = pe.next; + } + return buf.toString().trim(); + } + + /** + * @return string form of the pattern built from walking the path element chain + */ + String computePatternString() { + StringBuilder buf = new StringBuilder(); + PathElement pe = this.head; + while (pe != null) { + buf.append(pe.getChars()); + pe = pe.next; + } + return buf.toString(); + } + + @Nullable + PathElement getHeadSection() { + return this.head; + } /** * Encapsulates context when attempting a match. Includes some fixed state like the @@ -544,16 +586,16 @@ public class PathPattern implements Comparable { */ class MatchingContext { - // The candidate path to attempt a match against - char[] candidate; + final PathContainer candidate; - // The length of the candidate path - int candidateLength; + final List pathElements; + + final int pathLength; boolean isMatchStartMatching = false; @Nullable - private Map extractedVariables; + private Map extractedVariables; boolean extractingVariables; @@ -564,9 +606,10 @@ public class PathPattern implements Comparable { // points to the remaining path that wasn't consumed int remainingPathIndex; - public MatchingContext(String path, boolean extractVariables) { - candidate = path.toCharArray(); - candidateLength = candidate.length; + public MatchingContext(PathContainer pathContainer, boolean extractVariables) { + candidate = pathContainer; + pathElements = pathContainer.elements(); + pathLength = pathElements.size(); this.extractingVariables = extractVariables; } @@ -582,14 +625,14 @@ public class PathPattern implements Comparable { isMatchStartMatching = b; } - public void set(String key, String value) { + public void set(String key, String value, MultiValueMap parameters) { if (this.extractedVariables == null) { extractedVariables = new HashMap<>(); } - extractedVariables.put(key, value); + extractedVariables.put(key, new PathMatchResult(key, value, parameters)); } - public Map getExtractedVariables() { + public Map getExtractedVariables() { if (this.extractedVariables == null) { return Collections.emptyMap(); } @@ -599,20 +642,54 @@ public class PathPattern implements Comparable { } /** - * Scan ahead from the specified position for either the next separator - * character or the end of the candidate. - * @param pos the starting position for the scan - * @return the position of the next separator or the end of the candidate + * @param pathIndex possible index of a separator + * @return true if element at specified index is a separator */ - public int scanAhead(int pos) { - while (pos < candidateLength) { - if (candidate[pos] == separator) { - return pos; - } - pos++; + boolean isSeparator(int pathIndex) { + return pathElements.get(pathIndex) instanceof Separator; + } + + /** + * @param pathIndex path element index + * @return decoded value of the specified element + */ + String pathElementValue(int pathIndex) { + Element element = (pathIndex < pathLength) ? pathElements.get(pathIndex) : null; + if (element instanceof Segment) { + return ((Segment)element).valueDecoded(); } - return candidateLength; + return ""; + } + } + + /** + * Join two paths together including a separator if necessary. + * Extraneous separators are removed (if the first path + * ends with one and the second path starts with one). + * @param path1 first path + * @param path2 second path + * @return joined path that may include separator if necessary + */ + private String concat(String path1, String path2) { + boolean path1EndsWithSeparator = (path1.charAt(path1.length() - 1) == this.separator); + boolean path2StartsWithSeparator = (path2.charAt(0) == this.separator); + if (path1EndsWithSeparator && path2StartsWithSeparator) { + return path1 + path2.substring(1); } + else if (path1EndsWithSeparator || path2StartsWithSeparator) { + return path1 + path2; + } + else { + return path1 + this.separator + path2; + } + } + + /** + * @param container a path container + * @return true if the container is not null and has more than zero elements + */ + private boolean hasLength(PathContainer container) { + return container != null && container.elements().size() > 0; } } diff --git a/spring-web/src/main/java/org/springframework/web/util/pattern/RegexPathElement.java b/spring-web/src/main/java/org/springframework/web/util/pattern/RegexPathElement.java index 58f3ad01695..0ae3fdfb8f2 100644 --- a/spring-web/src/main/java/org/springframework/web/util/pattern/RegexPathElement.java +++ b/spring-web/src/main/java/org/springframework/web/util/pattern/RegexPathElement.java @@ -22,8 +22,8 @@ import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.springframework.web.util.UriUtils; import org.springframework.web.util.pattern.PathPattern.MatchingContext; +import org.springframework.http.server.reactive.PathContainer.Segment; /** * A regex path element. Used to represent any complicated element of the path. @@ -63,20 +63,17 @@ class RegexPathElement extends PathElement { public Pattern buildPattern(char[] regex, char[] completePattern) { StringBuilder patternBuilder = new StringBuilder(); String text = new String(regex); - StringBuilder encodedRegexBuilder = new StringBuilder(); Matcher matcher = GLOB_PATTERN.matcher(text); int end = 0; while (matcher.find()) { - patternBuilder.append(quote(text, end, matcher.start(), encodedRegexBuilder)); + patternBuilder.append(quote(text, end, matcher.start())); String match = matcher.group(); if ("?".equals(match)) { patternBuilder.append('.'); - encodedRegexBuilder.append('?'); } else if ("*".equals(match)) { patternBuilder.append(".*"); - encodedRegexBuilder.append('*'); int pos = matcher.start(); if (pos < 1 || text.charAt(pos-1) != '.') { // To be compatible with the AntPathMatcher comparator, @@ -85,7 +82,6 @@ class RegexPathElement extends PathElement { } } else if (match.startsWith("{") && match.endsWith("}")) { - encodedRegexBuilder.append(match); int colonIdx = match.indexOf(':'); if (colonIdx == -1) { patternBuilder.append(DEFAULT_VARIABLE_PATTERN); @@ -112,8 +108,7 @@ class RegexPathElement extends PathElement { end = matcher.end(); } - patternBuilder.append(quote(text, end, text.length(), encodedRegexBuilder)); - this.regex = encodedRegexBuilder.toString().toCharArray(); + patternBuilder.append(quote(text, end, text.length())); if (this.caseSensitive) { return Pattern.compile(patternBuilder.toString()); } @@ -126,54 +121,43 @@ class RegexPathElement extends PathElement { return this.variableNames; } - private String quote(String s, int start, int end, StringBuilder encodedRegexBuilder) { + private String quote(String s, int start, int end) { if (start == end) { return ""; } - String substring = s.substring(start, end); - String encodedSubString = UriUtils.encodePath(substring, StandardCharsets.UTF_8); - encodedRegexBuilder.append(encodedSubString); - return Pattern.quote(substring); + return Pattern.quote(s.substring(start, end)); } @Override - public boolean matches(int candidateIndex, MatchingContext matchingContext) { - int pos = matchingContext.scanAhead(candidateIndex); - - CharSequence textToMatch = null; - if (includesPercent(matchingContext.candidate, candidateIndex, pos)) { - textToMatch = decode(new SubSequence(matchingContext.candidate, candidateIndex, pos)); - } - else { - textToMatch = new SubSequence(matchingContext.candidate, candidateIndex, pos); - } + public boolean matches(int pathIndex, MatchingContext matchingContext) { + String textToMatch = matchingContext.pathElementValue(pathIndex); Matcher matcher = this.pattern.matcher(textToMatch); boolean matches = matcher.matches(); if (matches) { - if (this.next == null) { + if (isNoMorePattern()) { if (matchingContext.determineRemainingPath && - ((this.variableNames.size() == 0) ? true : pos > candidateIndex)) { - matchingContext.remainingPathIndex = pos; + ((this.variableNames.size() == 0) ? true : textToMatch.length() > 0)) { + matchingContext.remainingPathIndex = pathIndex + 1; matches = true; } else { // No more pattern, is there more data? // If pattern is capturing variables there must be some actual data to bind to them - matches = (pos == matchingContext.candidateLength && - ((this.variableNames.size() == 0) ? true : pos > candidateIndex)); + matches = (pathIndex + 1) >= matchingContext.pathLength && + ((this.variableNames.size() == 0) ? true : textToMatch.length() > 0); if (!matches && matchingContext.isAllowOptionalTrailingSlash()) { - matches = ((this.variableNames.size() == 0) ? true : pos > candidateIndex) && - (pos + 1) == matchingContext.candidateLength && - matchingContext.candidate[pos] == separator; + matches = ((this.variableNames.size() == 0) ? true : textToMatch.length() > 0) && + (pathIndex + 2) >= matchingContext.pathLength && + matchingContext.isSeparator(pathIndex + 1); } } } else { - if (matchingContext.isMatchStartMatching && pos == matchingContext.candidateLength) { + if (matchingContext.isMatchStartMatching && (pathIndex + 1 >= matchingContext.pathLength)) { return true; // no more data but matches up to this point } - matches = this.next.matches(pos, matchingContext); + matches = this.next.matches(pathIndex + 1, matchingContext); } } @@ -188,12 +172,15 @@ class RegexPathElement extends PathElement { for (int i = 1; i <= matcher.groupCount(); i++) { String name = this.variableNames.get(i - 1); String value = matcher.group(i); - matchingContext.set(name, value); + matchingContext.set(name, value, + (i == this.variableNames.size())? + ((Segment)matchingContext.pathElements.get(pathIndex)).parameters(): + NO_PARAMETERS); } } return matches; } - + @Override public int getNormalizedLength() { int varsLength = 0; @@ -222,4 +209,8 @@ class RegexPathElement extends PathElement { return "Regex(" + String.valueOf(this.regex) + ")"; } + @Override + public char[] getChars() { + return this.regex; + } } diff --git a/spring-web/src/main/java/org/springframework/web/util/pattern/SeparatorPathElement.java b/spring-web/src/main/java/org/springframework/web/util/pattern/SeparatorPathElement.java index 8634e3b5474..cd0cfa66c5f 100644 --- a/spring-web/src/main/java/org/springframework/web/util/pattern/SeparatorPathElement.java +++ b/spring-web/src/main/java/org/springframework/web/util/pattern/SeparatorPathElement.java @@ -38,28 +38,26 @@ class SeparatorPathElement extends PathElement { * must be the separator. */ @Override - public boolean matches(int candidateIndex, MatchingContext matchingContext) { - boolean matched = false; - if (candidateIndex < matchingContext.candidateLength && - matchingContext.candidate[candidateIndex] == separator) { - if (this.next == null) { + public boolean matches(int pathIndex, MatchingContext matchingContext) { + if (pathIndex < matchingContext.pathLength && matchingContext.isSeparator(pathIndex)) { + if (isNoMorePattern()) { if (matchingContext.determineRemainingPath) { - matchingContext.remainingPathIndex = candidateIndex + 1; - matched = true; + matchingContext.remainingPathIndex = pathIndex + 1; + return true; } else { - matched = (candidateIndex + 1 == matchingContext.candidateLength); + return (pathIndex + 1 == matchingContext.pathLength); } } else { - candidateIndex++; - if (matchingContext.isMatchStartMatching && candidateIndex == matchingContext.candidateLength) { + pathIndex++; + if (matchingContext.isMatchStartMatching && pathIndex == matchingContext.pathLength) { return true; // no more data but matches up to this point } - matched = this.next.matches(candidateIndex, matchingContext); + return this.next.matches(pathIndex, matchingContext); } } - return matched; + return false; } @Override @@ -67,9 +65,12 @@ class SeparatorPathElement extends PathElement { return 1; } - public String toString() { return "Separator(" + this.separator + ")"; } + + public char[] getChars() { + return new char[] {this.separator}; + } } diff --git a/spring-web/src/main/java/org/springframework/web/util/pattern/SingleCharWildcardedPathElement.java b/spring-web/src/main/java/org/springframework/web/util/pattern/SingleCharWildcardedPathElement.java index 800311ef294..bf1618d7ff6 100644 --- a/spring-web/src/main/java/org/springframework/web/util/pattern/SingleCharWildcardedPathElement.java +++ b/spring-web/src/main/java/org/springframework/web/util/pattern/SingleCharWildcardedPathElement.java @@ -16,6 +16,8 @@ package org.springframework.web.util.pattern; +import org.springframework.http.server.reactive.PathContainer.Element; +import org.springframework.http.server.reactive.PathContainer.Segment; import org.springframework.web.util.pattern.PathPattern.MatchingContext; /** @@ -56,68 +58,63 @@ class SingleCharWildcardedPathElement extends PathElement { @Override - public boolean matches(int candidateIndex, MatchingContext matchingContext) { - if (matchingContext.candidateLength < (candidateIndex + len)) { - return false; // there isn't enough data to match + public boolean matches(int pathIndex, MatchingContext matchingContext) { + if (pathIndex >= matchingContext.pathLength) { + // no more path left to match this element + return false; } - char[] candidate = matchingContext.candidate; + Element element = matchingContext.pathElements.get(pathIndex); + if (!(element instanceof Segment)) { + return false; + } + String value = ((Segment)element).valueDecoded(); + if (value.length() != len) { + // Not enough data to match this path element + return false; + } + + char[] data = ((Segment)element).valueDecodedChars(); if (this.caseSensitive) { - for (int i = 0; i candidateIndex && // and there is at least one character to match the *... - (nextPos + 1) == matchingContext.candidateLength && // and the nextPos is the end of the candidate... - matchingContext.candidate[nextPos] == separator); // and the final character is a separator + segmentData != null && segmentData.length() > 0 && // and there is at least one character to match the *... + (pathIndex + 1) == matchingContext.pathLength && // and the next path element is the end of the candidate... + matchingContext.isSeparator(pathIndex)); // and the final element is a separator } } } else { - if (matchingContext.isMatchStartMatching && nextPos == matchingContext.candidateLength) { + if (matchingContext.isMatchStartMatching && pathIndex == matchingContext.pathLength) { return true; // no more data but matches up to this point } // Within a path (e.g. /aa/*/bb) there must be at least one character to match the wildcard - if (nextPos == candidateIndex) { + if (segmentData == null || segmentData.length() == 0) { return false; } - return this.next.matches(nextPos, matchingContext); + return this.next.matches(pathIndex, matchingContext); } } @@ -90,4 +104,8 @@ class WildcardPathElement extends PathElement { return "Wildcard(*)"; } + @Override + public char[] getChars() { + return new char[] {'*'}; + } } diff --git a/spring-web/src/main/java/org/springframework/web/util/pattern/WildcardTheRestPathElement.java b/spring-web/src/main/java/org/springframework/web/util/pattern/WildcardTheRestPathElement.java index 1e80a4546b3..0295cbdc5d5 100644 --- a/spring-web/src/main/java/org/springframework/web/util/pattern/WildcardTheRestPathElement.java +++ b/spring-web/src/main/java/org/springframework/web/util/pattern/WildcardTheRestPathElement.java @@ -31,14 +31,13 @@ class WildcardTheRestPathElement extends PathElement { @Override - public boolean matches(int candidateIndex, PathPattern.MatchingContext matchingContext) { + public boolean matches(int pathIndex, PathPattern.MatchingContext matchingContext) { // If there is more data, it must start with the separator - if (candidateIndex < matchingContext.candidateLength && - matchingContext.candidate[candidateIndex] != separator) { + if (pathIndex < matchingContext.pathLength && !matchingContext.isSeparator(pathIndex)) { return false; } if (matchingContext.determineRemainingPath) { - matchingContext.remainingPathIndex = matchingContext.candidateLength; + matchingContext.remainingPathIndex = matchingContext.pathLength; } return true; } @@ -58,4 +57,8 @@ class WildcardTheRestPathElement extends PathElement { return "WildcardTheRest(" + this.separator + "**)"; } + @Override + public char[] getChars() { + return (this.separator+"**").toCharArray(); + } } diff --git a/spring-web/src/test/java/org/springframework/web/util/pattern/PathPatternMatcherTests.java b/spring-web/src/test/java/org/springframework/web/util/pattern/PathPatternMatcherTests.java index 6e55addfe95..0aa44c3c049 100644 --- a/spring-web/src/test/java/org/springframework/web/util/pattern/PathPatternMatcherTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/pattern/PathPatternMatcherTests.java @@ -16,6 +16,7 @@ package org.springframework.web.util.pattern; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -24,13 +25,17 @@ import java.util.List; import java.util.Map; import org.hamcrest.Matchers; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; - +import org.springframework.http.server.reactive.PathContainer; +import org.springframework.http.server.reactive.PathContainer.Element; import org.springframework.util.AntPathMatcher; import org.springframework.web.util.pattern.ParsingPathMatcher; import org.springframework.web.util.pattern.PathPattern; +import org.springframework.web.util.pattern.PathPattern.PathMatchResult; +import org.springframework.web.util.pattern.PathPattern.PathRemainingMatchInfo; import org.springframework.web.util.pattern.PathPatternParser; import static org.hamcrest.CoreMatchers.*; @@ -45,212 +50,242 @@ public class PathPatternMatcherTests { private char separator = PathPatternParser.DEFAULT_SEPARATOR; + @Test + public void pathContainer() { + assertEquals("[/][abc][/][def]",elementsToString(toPathContainer("/abc/def").elements())); + assertEquals("[abc][/][def]",elementsToString(toPathContainer("abc/def").elements())); + assertEquals("[abc][/][def][/]",elementsToString(toPathContainer("abc/def/").elements())); + assertEquals("[abc][/][/][def][/][/]",elementsToString(toPathContainer("abc//def//").elements())); + assertEquals("[/]",elementsToString(toPathContainer("/").elements())); + assertEquals("[/][/][/]",elementsToString(toPathContainer("///").elements())); + } + + @Test + public void matching_LiteralPathElement() { + checkMatches("foo", "foo"); + checkNoMatch("foo", "bar"); + checkNoMatch("foo", "/foo"); + checkNoMatch("/foo", "foo"); + checkMatches("/f", "/f"); + checkMatches("/foo", "/foo"); + checkNoMatch("/foo", "/food"); + checkNoMatch("/food", "/foo"); + checkMatches("/foo/", "/foo/"); + checkMatches("/foo/bar/woo", "/foo/bar/woo"); + checkMatches("foo/bar/woo", "foo/bar/woo"); + } @Test public void basicMatching() { checkMatches("", ""); checkMatches("", null); - checkNoMatch("/abc", null); + checkNoMatch("/abc", "/"); checkMatches("/", "/"); checkNoMatch("/", "/a"); - checkMatches("f", "f"); - checkMatches("/foo", "/foo"); - checkMatches("/foo/", "/foo/"); - checkMatches("/foo/bar", "/foo/bar"); - checkMatches("foo/bar", "foo/bar"); - checkMatches("/foo/bar/", "/foo/bar/"); checkMatches("foo/bar/", "foo/bar/"); - checkMatches("/foo/bar/woo", "/foo/bar/woo"); checkNoMatch("foo", "foobar"); checkMatches("/foo/bar", "/foo/bar"); checkNoMatch("/foo/bar", "/foo/baz"); } + private void assertMatches(PathPattern pp, String path) { + assertTrue(pp.matches(toPathContainer(path))); + } + + private void assertNoMatch(PathPattern pp, String path) { + assertFalse(pp.matches(toPathContainer(path))); + } + @Test public void optionalTrailingSeparators() { + PathPattern pp; // LiteralPathElement - PathPattern pp = parse("/resource"); - assertTrue(pp.matches("/resource")); - assertTrue(pp.matches("/resource/")); - assertFalse(pp.matches("/resource//")); + pp = parse("/resource"); + assertMatches(pp,"/resource"); + assertMatches(pp,"/resource"); + assertMatches(pp,"/resource/"); + assertNoMatch(pp,"/resource//"); pp = parse("/resource/"); - assertFalse(pp.matches("/resource")); - assertTrue(pp.matches("/resource/")); - assertFalse(pp.matches("/resource//")); + assertNoMatch(pp,"/resource"); + assertMatches(pp,"/resource/"); + assertNoMatch(pp,"/resource//"); + pp = parse("res?urce"); + assertNoMatch(pp,"resource//"); // SingleCharWildcardPathElement pp = parse("/res?urce"); - assertTrue(pp.matches("/resource")); - assertTrue(pp.matches("/resource/")); - assertFalse(pp.matches("/resource//")); + assertMatches(pp,"/resource"); + assertMatches(pp,"/resource/"); + assertNoMatch(pp,"/resource//"); pp = parse("/res?urce/"); - assertFalse(pp.matches("/resource")); - assertTrue(pp.matches("/resource/")); - assertFalse(pp.matches("/resource//")); + assertNoMatch(pp,"/resource"); + assertMatches(pp,"/resource/"); + assertNoMatch(pp,"/resource//"); // CaptureVariablePathElement pp = parse("/{var}"); - assertTrue(pp.matches("/resource")); - assertEquals("resource",pp.matchAndExtract("/resource").get("var")); - assertTrue(pp.matches("/resource/")); - assertEquals("resource",pp.matchAndExtract("/resource/").get("var")); - assertFalse(pp.matches("/resource//")); + assertMatches(pp,"/resource"); + assertEquals("resource",pp.matchAndExtract(toPathContainer("/resource")).get("var").value()); + assertMatches(pp,"/resource/"); + assertEquals("resource",pp.matchAndExtract(toPathContainer("/resource/")).get("var").value()); + assertNoMatch(pp,"/resource//"); pp = parse("/{var}/"); - assertFalse(pp.matches("/resource")); - assertTrue(pp.matches("/resource/")); - assertEquals("resource",pp.matchAndExtract("/resource/").get("var")); - assertFalse(pp.matches("/resource//")); + assertNoMatch(pp,"/resource"); + assertMatches(pp,"/resource/"); + assertEquals("resource",pp.matchAndExtract(toPathContainer("/resource/")).get("var").value()); + assertNoMatch(pp,"/resource//"); // CaptureTheRestPathElement pp = parse("/{*var}"); - assertTrue(pp.matches("/resource")); - assertEquals("/resource",pp.matchAndExtract("/resource").get("var")); - assertTrue(pp.matches("/resource/")); - assertEquals("/resource/",pp.matchAndExtract("/resource/").get("var")); - assertTrue(pp.matches("/resource//")); - assertEquals("/resource//",pp.matchAndExtract("/resource//").get("var")); - assertTrue(pp.matches("//resource//")); - assertEquals("//resource//",pp.matchAndExtract("//resource//").get("var")); + assertMatches(pp,"/resource"); + assertEquals("/resource",pp.matchAndExtract(toPathContainer("/resource")).get("var").value()); + assertMatches(pp,"/resource/"); + assertEquals("/resource/",pp.matchAndExtract(toPathContainer("/resource/")).get("var").value()); + assertMatches(pp,"/resource//"); + assertEquals("/resource//",pp.matchAndExtract(toPathContainer("/resource//")).get("var").value()); + assertMatches(pp,"//resource//"); + assertEquals("//resource//",pp.matchAndExtract(toPathContainer("//resource//")).get("var").value()); // WildcardTheRestPathElement pp = parse("/**"); - assertTrue(pp.matches("/resource")); - assertTrue(pp.matches("/resource/")); - assertTrue(pp.matches("/resource//")); - assertTrue(pp.matches("//resource//")); + assertMatches(pp,"/resource"); + assertMatches(pp,"/resource/"); + assertMatches(pp,"/resource//"); + assertMatches(pp,"//resource//"); // WildcardPathElement pp = parse("/*"); - assertTrue(pp.matches("/resource")); - assertTrue(pp.matches("/resource/")); - assertFalse(pp.matches("/resource//")); + assertMatches(pp,"/resource"); + assertMatches(pp,"/resource/"); + assertNoMatch(pp,"/resource//"); pp = parse("/*/"); - assertFalse(pp.matches("/resource")); - assertTrue(pp.matches("/resource/")); - assertFalse(pp.matches("/resource//")); + assertNoMatch(pp,"/resource"); + assertMatches(pp,"/resource/"); + assertNoMatch(pp,"/resource//"); // RegexPathElement pp = parse("/{var1}_{var2}"); - assertTrue(pp.matches("/res1_res2")); - assertEquals("res1",pp.matchAndExtract("/res1_res2").get("var1")); - assertEquals("res2",pp.matchAndExtract("/res1_res2").get("var2")); - assertTrue(pp.matches("/res1_res2/")); - assertEquals("res1",pp.matchAndExtract("/res1_res2/").get("var1")); - assertEquals("res2",pp.matchAndExtract("/res1_res2/").get("var2")); - assertFalse(pp.matches("/res1_res2//")); + assertMatches(pp,"/res1_res2"); + assertEquals("res1",pp.matchAndExtract(toPathContainer("/res1_res2")).get("var1").value()); + assertEquals("res2",pp.matchAndExtract(toPathContainer("/res1_res2")).get("var2").value()); + assertMatches(pp,"/res1_res2/"); + assertEquals("res1",pp.matchAndExtract(toPathContainer("/res1_res2/")).get("var1").value()); + assertEquals("res2",pp.matchAndExtract(toPathContainer("/res1_res2/")).get("var2").value()); + assertNoMatch(pp,"/res1_res2//"); pp = parse("/{var1}_{var2}/"); - assertFalse(pp.matches("/res1_res2")); - assertTrue(pp.matches("/res1_res2/")); - assertEquals("res1",pp.matchAndExtract("/res1_res2/").get("var1")); - assertEquals("res2",pp.matchAndExtract("/res1_res2/").get("var2")); - assertFalse(pp.matches("/res1_res2//")); + assertNoMatch(pp,"/res1_res2"); + assertMatches(pp,"/res1_res2/"); + assertEquals("res1",pp.matchAndExtract(toPathContainer("/res1_res2/")).get("var1").value()); + assertEquals("res2",pp.matchAndExtract(toPathContainer("/res1_res2/")).get("var2").value()); + assertNoMatch(pp,"/res1_res2//"); pp = parse("/{var1}*"); - assertTrue(pp.matches("/a")); - assertTrue(pp.matches("/a/")); - assertFalse(pp.matches("/")); // no characters for var1 - assertFalse(pp.matches("//")); // no characters for var1 + assertMatches(pp,"/a"); + assertMatches(pp,"/a/"); + assertNoMatch(pp,"/"); // no characters for var1 + assertNoMatch(pp,"//"); // no characters for var1 // Now with trailing matching turned OFF PathPatternParser parser = new PathPatternParser(); parser.setMatchOptionalTrailingSlash(false); // LiteralPathElement pp = parser.parse("/resource"); - assertTrue(pp.matches("/resource")); - assertFalse(pp.matches("/resource/")); - assertFalse(pp.matches("/resource//")); + assertMatches(pp,"/resource"); + assertNoMatch(pp,"/resource/"); + assertNoMatch(pp,"/resource//"); pp = parser.parse("/resource/"); - assertFalse(pp.matches("/resource")); - assertTrue(pp.matches("/resource/")); - assertFalse(pp.matches("/resource//")); + assertNoMatch(pp,"/resource"); + assertMatches(pp,"/resource/"); + assertNoMatch(pp,"/resource//"); // SingleCharWildcardPathElement pp = parser.parse("/res?urce"); - assertTrue(pp.matches("/resource")); - assertFalse(pp.matches("/resource/")); - assertFalse(pp.matches("/resource//")); + assertMatches(pp,"/resource"); + assertNoMatch(pp,"/resource/"); + assertNoMatch(pp,"/resource//"); pp = parser.parse("/res?urce/"); - assertFalse(pp.matches("/resource")); - assertTrue(pp.matches("/resource/")); - assertFalse(pp.matches("/resource//")); + assertNoMatch(pp,"/resource"); + assertMatches(pp,"/resource/"); + assertNoMatch(pp,"/resource//"); // CaptureVariablePathElement pp = parser.parse("/{var}"); - assertTrue(pp.matches("/resource")); - assertEquals("resource",pp.matchAndExtract("/resource").get("var")); - assertFalse(pp.matches("/resource/")); - assertFalse(pp.matches("/resource//")); + assertMatches(pp,"/resource"); + assertEquals("resource",pp.matchAndExtract(toPathContainer("/resource")).get("var").value()); + assertNoMatch(pp,"/resource/"); + assertNoMatch(pp,"/resource//"); pp = parser.parse("/{var}/"); - assertFalse(pp.matches("/resource")); - assertTrue(pp.matches("/resource/")); - assertEquals("resource",pp.matchAndExtract("/resource/").get("var")); - assertFalse(pp.matches("/resource//")); + assertNoMatch(pp,"/resource"); + assertMatches(pp,"/resource/"); + assertEquals("resource",pp.matchAndExtract(toPathContainer("/resource/")).get("var").value()); + assertNoMatch(pp,"/resource//"); // CaptureTheRestPathElement pp = parser.parse("/{*var}"); - assertTrue(pp.matches("/resource")); - assertEquals("/resource",pp.matchAndExtract("/resource").get("var")); - assertTrue(pp.matches("/resource/")); - assertEquals("/resource/",pp.matchAndExtract("/resource/").get("var")); - assertTrue(pp.matches("/resource//")); - assertEquals("/resource//",pp.matchAndExtract("/resource//").get("var")); - assertTrue(pp.matches("//resource//")); - assertEquals("//resource//",pp.matchAndExtract("//resource//").get("var")); + assertMatches(pp,"/resource"); + assertEquals("/resource",pp.matchAndExtract(toPathContainer("/resource")).get("var").value()); + assertMatches(pp,"/resource/"); + assertEquals("/resource/",pp.matchAndExtract(toPathContainer("/resource/")).get("var").value()); + assertMatches(pp,"/resource//"); + assertEquals("/resource//",pp.matchAndExtract(toPathContainer("/resource//")).get("var").value()); + assertMatches(pp,"//resource//"); + assertEquals("//resource//",pp.matchAndExtract(toPathContainer("//resource//")).get("var").value()); // WildcardTheRestPathElement pp = parser.parse("/**"); - assertTrue(pp.matches("/resource")); - assertTrue(pp.matches("/resource/")); - assertTrue(pp.matches("/resource//")); - assertTrue(pp.matches("//resource//")); + assertMatches(pp,"/resource"); + assertMatches(pp,"/resource/"); + assertMatches(pp,"/resource//"); + assertMatches(pp,"//resource//"); // WildcardPathElement pp = parser.parse("/*"); - assertTrue(pp.matches("/resource")); - assertFalse(pp.matches("/resource/")); - assertFalse(pp.matches("/resource//")); + assertMatches(pp,"/resource"); + assertNoMatch(pp,"/resource/"); + assertNoMatch(pp,"/resource//"); pp = parser.parse("/*/"); - assertFalse(pp.matches("/resource")); - assertTrue(pp.matches("/resource/")); - assertFalse(pp.matches("/resource//")); + assertNoMatch(pp,"/resource"); + assertMatches(pp,"/resource/"); + assertNoMatch(pp,"/resource//"); // RegexPathElement pp = parser.parse("/{var1}_{var2}"); - assertTrue(pp.matches("/res1_res2")); - assertEquals("res1",pp.matchAndExtract("/res1_res2").get("var1")); - assertEquals("res2",pp.matchAndExtract("/res1_res2").get("var2")); - assertFalse(pp.matches("/res1_res2/")); - assertFalse(pp.matches("/res1_res2//")); + assertMatches(pp,"/res1_res2"); + assertEquals("res1",pp.matchAndExtract(toPathContainer("/res1_res2")).get("var1").value()); + assertEquals("res2",pp.matchAndExtract(toPathContainer("/res1_res2")).get("var2").value()); + assertNoMatch(pp,"/res1_res2/"); + assertNoMatch(pp,"/res1_res2//"); pp = parser.parse("/{var1}_{var2}/"); - assertFalse(pp.matches("/res1_res2")); - assertTrue(pp.matches("/res1_res2/")); - assertEquals("res1",pp.matchAndExtract("/res1_res2/").get("var1")); - assertEquals("res2",pp.matchAndExtract("/res1_res2/").get("var2")); - assertFalse(pp.matches("/res1_res2//")); + assertNoMatch(pp,"/res1_res2"); + assertMatches(pp,"/res1_res2/"); + assertEquals("res1",pp.matchAndExtract(toPathContainer("/res1_res2/")).get("var1").value()); + assertEquals("res2",pp.matchAndExtract(toPathContainer("/res1_res2/")).get("var2").value()); + assertNoMatch(pp,"/res1_res2//"); pp = parser.parse("/{var1}*"); - assertTrue(pp.matches("/a")); - assertFalse(pp.matches("/a/")); - assertFalse(pp.matches("/")); // no characters for var1 - assertFalse(pp.matches("//")); // no characters for var1 + assertMatches(pp,"/a"); + assertNoMatch(pp,"/a/"); + assertNoMatch(pp,"/"); // no characters for var1 + assertNoMatch(pp,"//"); // no characters for var1 } @Test public void pathRemainderBasicCases_spr15336() { // Cover all PathElement kinds - assertEquals("/bar", parse("/foo").getPathRemaining("/foo/bar").getPathRemaining()); - assertEquals("/", parse("/foo").getPathRemaining("/foo/").getPathRemaining()); - assertEquals("/bar",parse("/foo*").getPathRemaining("/foo/bar").getPathRemaining()); - assertEquals("/bar", parse("/*").getPathRemaining("/foo/bar").getPathRemaining()); - assertEquals("/bar", parse("/{foo}").getPathRemaining("/foo/bar").getPathRemaining()); - assertNull(parse("/foo").getPathRemaining("/bar/baz")); - assertEquals("",parse("/**").getPathRemaining("/foo/bar").getPathRemaining()); - assertEquals("",parse("/{*bar}").getPathRemaining("/foo/bar").getPathRemaining()); - assertEquals("/bar",parse("/a?b/d?e").getPathRemaining("/aab/dde/bar").getPathRemaining()); - assertEquals("/bar",parse("/{abc}abc").getPathRemaining("/xyzabc/bar").getPathRemaining()); - assertEquals("/bar",parse("/*y*").getPathRemaining("/xyzxyz/bar").getPathRemaining()); - assertEquals("",parse("/").getPathRemaining("/").getPathRemaining()); - assertEquals("a",parse("/").getPathRemaining("/a").getPathRemaining()); - assertEquals("a/",parse("/").getPathRemaining("/a/").getPathRemaining()); - assertEquals("/bar",parse("/a{abc}").getPathRemaining("/a/bar").getPathRemaining()); + assertEquals("/bar", getPathRemaining("/foo","/foo/bar").getPathRemaining()); + assertEquals("/", getPathRemaining("/foo","/foo/").getPathRemaining()); + assertEquals("/bar",getPathRemaining("/foo*","/foo/bar").getPathRemaining()); + assertEquals("/bar", getPathRemaining("/*","/foo/bar").getPathRemaining()); + assertEquals("/bar", getPathRemaining("/{foo}","/foo/bar").getPathRemaining()); + assertNull(getPathRemaining("/foo","/bar/baz")); + assertEquals("",getPathRemaining("/**","/foo/bar").getPathRemaining()); + assertEquals("",getPathRemaining("/{*bar}","/foo/bar").getPathRemaining()); + assertEquals("/bar",getPathRemaining("/a?b/d?e","/aab/dde/bar").getPathRemaining()); + assertEquals("/bar",getPathRemaining("/{abc}abc","/xyzabc/bar").getPathRemaining()); + assertEquals("/bar",getPathRemaining("/*y*","/xyzxyz/bar").getPathRemaining()); + assertEquals("",getPathRemaining("/","/").getPathRemaining()); + assertEquals("a",getPathRemaining("/","/a").getPathRemaining()); + assertEquals("a/",getPathRemaining("/","/a/").getPathRemaining()); + assertEquals("/bar",getPathRemaining("/a{abc}","/a/bar").getPathRemaining()); + assertEquals("/bar", getPathRemaining("/foo//","/foo///bar").getPathRemaining()); } @Test @@ -258,9 +293,6 @@ public class PathPatternMatcherTests { checkCapture("{var}","f%20o","var","f o"); checkCapture("{var1}/{var2}","f%20o/f%7Co","var1","f o","var2","f|o"); checkCapture("{var1}/{var2}","f%20o/f%7co","var1","f o","var2","f|o"); // lower case encoding - // constraints - // - constraint is expressed in non encoded form - // - returned values are decoded checkCapture("{var:foo}","foo","var","foo"); checkCapture("{var:f o}","f%20o","var","f o"); // constraint is expressed in non encoded form checkCapture("{var:f.o}","f%20o","var","f o"); @@ -271,73 +303,11 @@ public class PathPatternMatcherTests { public void encodingAndBoundVariablesCaptureTheRestPathElement() { checkCapture("/{*var}","/f%20o","var","/f o"); checkCapture("{var1}/{*var2}","f%20o/f%7Co","var1","f o","var2","/f|o"); - // constraints - decoding happens for constraint checking but returned value is undecoded checkCapture("/{*var}","/foo","var","/foo"); - checkCapture("/{*var}","/f%20o","var","/f o"); // constraint is expressed in non encoded form + checkCapture("/{*var}","/f%20o","var","/f o"); checkCapture("/{*var}","/f%20o","var","/f o"); checkCapture("/{*var}","/f%7co","var","/f|o"); } - - @Test - public void encodingWithCaseSensitivity() { - // Concern here is that regardless of case sensitivity, %7c == %7C (for example) - // Need to test all path elements that might have literal components - - PathPatternParser ppp = new PathPatternParser(); - ppp.setCaseSensitive(true); - - // LiteralPathElement - PathPattern pp = ppp.parse("/this is a |"); - assertTrue(pp.matches("/this%20is%20a%20%7C")); - assertTrue(pp.matches("/this%20is%20a%20%7c")); - assertFalse(pp.matches("/thIs%20is%20a%20%7c")); - assertFalse(pp.matches("/thIs%20is%20a%20%7C")); - assertEquals("Separator(/) Literal(this%20is%20a%20%7C)",pp.toChainString()); - - // RegexPathElement - pp = ppp.parse("/{foo}this is a |"); - assertTrue(pp.matches("/xxxthis%20is%20a%20%7C")); - assertTrue(pp.matches("/xxxthis%20is%20a%20%7c")); - assertFalse(pp.matches("/xxxXhis%20is%20a%20%7C")); - assertFalse(pp.matches("/xxxXhis%20is%20a%20%7c")); - assertEquals("Separator(/) Regex({foo}this%20is%20a%20%7C)",pp.toChainString()); - - // SingleCharWildcardedPathElement - pp = ppp.parse("/th?s is a |"); - assertTrue(pp.matches("/this%20is%20a%20%7C")); - assertTrue(pp.matches("/this%20is%20a%20%7c")); - assertFalse(pp.matches("/xhis%20is%20a%20%7C")); - assertFalse(pp.matches("/xhis%20is%20a%20%7c")); - assertEquals("Separator(/) SingleCharWildcarded(th?s%20is%20a%20%7C)",pp.toChainString()); - - ppp = new PathPatternParser(); - ppp.setCaseSensitive(false); - - // LiteralPathElement - pp = ppp.parse("/this is a |"); - assertTrue(pp.matches("/this%20is%20a%20%7C")); - assertTrue(pp.matches("/this%20is%20a%20%7c")); - assertTrue(pp.matches("/thIs%20is%20a%20%7C")); - assertTrue(pp.matches("/tHis%20is%20a%20%7c")); - // For case insensitive matches we make all the chars lower case - assertEquals("Separator(/) Literal(this%20is%20a%20%7c)",pp.toChainString()); - - // RegexPathElement - pp = ppp.parse("/{foo}this is a |"); - assertTrue(pp.matches("/xxxthis%20is%20a%20%7C")); - assertTrue(pp.matches("/xxxthis%20is%20a%20%7c")); - assertTrue(pp.matches("/xxxThis%20is%20a%20%7C")); - assertTrue(pp.matches("/xxxThis%20is%20a%20%7c")); - assertEquals("Separator(/) Regex({foo}this%20is%20a%20%7C)",pp.toChainString()); - - // SingleCharWildcardedPathElement - pp = ppp.parse("/th?s is a |"); - assertTrue(pp.matches("/this%20is%20a%20%7C")); - assertTrue(pp.matches("/this%20is%20a%20%7c")); - assertTrue(pp.matches("/This%20is%20a%20%7C")); - assertTrue(pp.matches("/This%20is%20a%20%7c")); - assertEquals("Separator(/) SingleCharWildcarded(th?s%20is%20a%20%7c)",pp.toChainString()); - } @Test public void encodingAndBoundVariablesRegexPathElement() { @@ -347,45 +317,42 @@ public class PathPatternMatcherTests { checkCapture("/{var1}_ _{var2:f\\|o}","/f%20o_%20_f%7co","var1","f o","var2","f|o"); checkCapture("/{var1:f o}_ _{var2:f\\|o}","/f%20o_%20_f%7co","var1","f o","var2","f|o"); } - - @Test - public void encodedPaths() { - checkMatches("/foo bar", "/foo%20bar"); - checkMatches("/foo*bar", "/fooboobar"); - checkMatches("/f?o","/f%7co"); - } @Test public void pathRemainingCornerCases_spr15336() { // No match when the literal path element is a longer form of the segment in the pattern - assertNull(parse("/foo").getPathRemaining("/footastic/bar")); - assertNull(parse("/f?o").getPathRemaining("/footastic/bar")); - assertNull(parse("/f*o*p").getPathRemaining("/flooptastic/bar")); - assertNull(parse("/{abc}abc").getPathRemaining("/xyzabcbar/bar")); + assertNull(parse("/foo").getPathRemaining(toPathContainer("/footastic/bar"))); + assertNull(parse("/f?o").getPathRemaining(toPathContainer("/footastic/bar"))); + assertNull(parse("/f*o*p").getPathRemaining(toPathContainer("/flooptastic/bar"))); + assertNull(parse("/{abc}abc").getPathRemaining(toPathContainer("/xyzabcbar/bar"))); // With a /** on the end have to check if there is any more data post // 'the match' it starts with a separator - assertNull(parse("/resource/**").getPathRemaining("/resourceX")); - assertEquals("",parse("/resource/**").getPathRemaining("/resource").getPathRemaining()); + assertNull(parse("/resource/**").getPathRemaining(toPathContainer("/resourceX"))); + assertEquals("",parse("/resource/**").getPathRemaining(toPathContainer("/resource")).getPathRemaining()); // Similar to above for the capture-the-rest variant - assertNull(parse("/resource/{*foo}").getPathRemaining("/resourceX")); - assertEquals("",parse("/resource/{*foo}").getPathRemaining("/resource").getPathRemaining()); + assertNull(parse("/resource/{*foo}").getPathRemaining(toPathContainer("/resourceX"))); + assertEquals("",parse("/resource/{*foo}").getPathRemaining(toPathContainer("/resource")).getPathRemaining()); - PathPattern.PathRemainingMatchInfo pri = parse("/aaa/{bbb}/c?d/e*f/*/g").getPathRemaining("/aaa/b/ccd/ef/x/g/i"); + PathPattern.PathRemainingMatchInfo pri = parse("/aaa/{bbb}/c?d/e*f/*/g").getPathRemaining(toPathContainer("/aaa/b/ccd/ef/x/g/i")); assertEquals("/i",pri.getPathRemaining()); - assertEquals("b",pri.getMatchingVariables().get("bbb")); + assertEquals("b",pri.getMatchingVariables().get("bbb").value()); - pri = parse("/{aaa}_{bbb}/e*f/{x}/g").getPathRemaining("/aa_bb/ef/x/g/i"); + pri = parse("/aaa/{bbb}/c?d/e*f/*/g/").getPathRemaining(toPathContainer("/aaa/b/ccd/ef/x/g/i")); + assertEquals("i",pri.getPathRemaining()); + assertEquals("b",pri.getMatchingVariables().get("bbb").value()); + + pri = parse("/{aaa}_{bbb}/e*f/{x}/g").getPathRemaining(toPathContainer("/aa_bb/ef/x/g/i")); assertEquals("/i",pri.getPathRemaining()); - assertEquals("aa",pri.getMatchingVariables().get("aaa")); - assertEquals("bb",pri.getMatchingVariables().get("bbb")); - assertEquals("x",pri.getMatchingVariables().get("x")); + assertEquals("aa",pri.getMatchingVariables().get("aaa").value()); + assertEquals("bb",pri.getMatchingVariables().get("bbb").value()); + assertEquals("x",pri.getMatchingVariables().get("x").value()); - assertNull(parse("/a/b").getPathRemaining("")); + assertNull(parse("/a/b").getPathRemaining(toPathContainer(""))); assertNull(parse("/a/b").getPathRemaining(null)); - assertEquals("/a/b",parse("").getPathRemaining("/a/b").getPathRemaining()); - assertEquals("",parse("").getPathRemaining("").getPathRemaining()); + assertEquals("/a/b",parse("").getPathRemaining(toPathContainer("/a/b")).getPathRemaining()); + assertEquals("",parse("").getPathRemaining(toPathContainer("")).getPathRemaining()); assertNull(parse("").getPathRemaining(null).getPathRemaining()); } @@ -430,7 +397,7 @@ public class PathPatternMatcherTests { public void multipleSeparatorsInPattern() { PathPattern pp = parse("a//b//c"); assertEquals("Literal(a) Separator(/) Separator(/) Literal(b) Separator(/) Separator(/) Literal(c)",pp.toChainString()); - assertTrue(pp.matches("a//b//c")); + assertMatches(pp,"a//b//c"); assertEquals("Literal(a) Separator(/) WildcardTheRest(/**)",parse("a//**").toChainString()); checkMatches("///abc", "///abc"); checkNoMatch("///abc", "/abc"); @@ -450,6 +417,7 @@ public class PathPatternMatcherTests { checkNoMatch("/abc", "////abc"); checkMatches("////abc", "////abc"); checkNoMatch("/", "//"); + checkNoMatch("/abc//def", "/abc/def"); checkNoMatch("/abc//def///ghi", "/abc/def/ghi"); checkMatches("/abc//def///ghi", "/abc//def///ghi"); } @@ -471,7 +439,6 @@ public class PathPatternMatcherTests { checkMatches("/f*/bar", "/foo/bar"); checkMatches("/*/bar", "/foo/bar"); checkMatches("a/*","a/"); - checkMatches("/*","/"); checkMatches("/*/bar", "/foo/bar"); checkNoMatch("/*/bar", "/foo/baz"); @@ -480,11 +447,11 @@ public class PathPatternMatcherTests { checkMatches("/a*b*c*d/bar", "/abcd/bar"); checkMatches("*a*", "testa"); checkMatches("a/*", "a/"); - checkNoMatch("a/*", "a//"); // trailing slash, so is allowed + checkNoMatch("a/*", "a//"); // no data for * checkMatches("a/*", "a/a/"); // trailing slash, so is allowed PathPatternParser ppp = new PathPatternParser(); ppp.setMatchOptionalTrailingSlash(false); - assertFalse(ppp.parse("a/*").matches("a//")); + assertFalse(ppp.parse("a/*").matches(toPathContainer("a//"))); checkMatches("a/*", "a/a"); checkMatches("a/*", "a/a/"); // trailing slash is optional checkMatches("/resource/**", "/resource"); @@ -577,38 +544,45 @@ public class PathPatternMatcherTests { @Test public void pathRemainingEnhancements_spr15419() { + PathPattern pp; + PathPattern.PathRemainingMatchInfo pri; // It would be nice to partially match a path and get any bound variables in one step - PathPattern pp = parse("/{this}/{one}/{here}"); - PathPattern.PathRemainingMatchInfo pri = pp.getPathRemaining("/foo/bar/goo/boo"); + pp = parse("/{this}/{one}/{here}"); + pri = getPathRemaining(pp, "/foo/bar/goo/boo"); assertEquals("/boo",pri.getPathRemaining()); - assertEquals("foo",pri.getMatchingVariables().get("this")); - assertEquals("bar",pri.getMatchingVariables().get("one")); - assertEquals("goo",pri.getMatchingVariables().get("here")); + assertEquals("foo",pri.getMatchingVariables().get("this").value()); + assertEquals("bar",pri.getMatchingVariables().get("one").value()); + assertEquals("goo",pri.getMatchingVariables().get("here").value()); pp = parse("/aaa/{foo}"); - pri = pp.getPathRemaining("/aaa/bbb"); + pri = getPathRemaining(pp, "/aaa/bbb"); assertEquals("",pri.getPathRemaining()); - assertEquals("bbb",pri.getMatchingVariables().get("foo")); + assertEquals("bbb",pri.getMatchingVariables().get("foo").value()); pp = parse("/aaa/bbb"); - pri = pp.getPathRemaining("/aaa/bbb"); + pri = getPathRemaining(pp, "/aaa/bbb"); assertEquals("",pri.getPathRemaining()); assertEquals(0,pri.getMatchingVariables().size()); pp = parse("/*/{foo}/b*"); - pri = pp.getPathRemaining("/foo"); + pri = getPathRemaining(pp, "/foo"); assertNull(pri); - pri = pp.getPathRemaining("/abc/def/bhi"); + pri = getPathRemaining(pp, "/abc/def/bhi"); assertEquals("",pri.getPathRemaining()); - assertEquals("def",pri.getMatchingVariables().get("foo")); + assertEquals("def",pri.getMatchingVariables().get("foo").value()); - pri = pp.getPathRemaining("/abc/def/bhi/jkl"); + pri = getPathRemaining(pp, "/abc/def/bhi/jkl"); assertEquals("/jkl",pri.getPathRemaining()); - assertEquals("def",pri.getMatchingVariables().get("foo")); + assertEquals("def",pri.getMatchingVariables().get("foo").value()); } @Test public void matchStart() { + PathPatternParser ppp = new PathPatternParser(); + ppp.setMatchOptionalTrailingSlash(false); + PathPattern pp = ppp.parse("test"); + assertFalse(pp.matchStart(PathContainer.parse("test/",StandardCharsets.UTF_8))); + checkStartNoMatch("test/*/","test//"); checkStartMatches("test/*","test/abc"); checkStartMatches("test/*/def","test/abc/def"); @@ -715,93 +689,94 @@ public class PathPatternMatcherTests { PathPatternParser pp = new PathPatternParser(); pp.setCaseSensitive(false); PathPattern p = pp.parse("abc"); - assertTrue(p.matches("AbC")); - assertFalse(p.matches("def")); + assertMatches(p,"AbC"); + assertNoMatch(p,"def"); p = pp.parse("fOo"); - assertTrue(p.matches("FoO")); + assertMatches(p,"FoO"); p = pp.parse("/fOo/bAr"); - assertTrue(p.matches("/FoO/BaR")); + assertMatches(p,"/FoO/BaR"); pp = new PathPatternParser(); pp.setCaseSensitive(true); p = pp.parse("abc"); - assertFalse(p.matches("AbC")); + assertNoMatch(p,"AbC"); p = pp.parse("fOo"); - assertFalse(p.matches("FoO")); + assertNoMatch(p,"FoO"); p = pp.parse("/fOo/bAr"); - assertFalse(p.matches("/FoO/BaR")); + assertNoMatch(p,"/FoO/BaR"); p = pp.parse("/fOO/bAr"); - assertTrue(p.matches("/fOO/bAr")); + assertMatches(p,"/fOO/bAr"); pp = new PathPatternParser(); pp.setCaseSensitive(false); p = pp.parse("{foo:[A-Z]*}"); - assertTrue(p.matches("abc")); - assertTrue(p.matches("ABC")); + assertMatches(p,"abc"); + assertMatches(p,"ABC"); pp = new PathPatternParser(); pp.setCaseSensitive(true); p = pp.parse("{foo:[A-Z]*}"); - assertFalse(p.matches("abc")); - assertTrue(p.matches("ABC")); + assertNoMatch(p,"abc"); + assertMatches(p,"ABC"); pp = new PathPatternParser(); pp.setCaseSensitive(false); p = pp.parse("ab?"); - assertTrue(p.matches("AbC")); + assertMatches(p,"AbC"); p = pp.parse("fO?"); - assertTrue(p.matches("FoO")); + assertMatches(p,"FoO"); p = pp.parse("/fO?/bA?"); - assertTrue(p.matches("/FoO/BaR")); - assertFalse(p.matches("/bAr/fOo")); + assertMatches(p,"/FoO/BaR"); + assertNoMatch(p,"/bAr/fOo"); pp = new PathPatternParser(); pp.setCaseSensitive(true); p = pp.parse("ab?"); - assertFalse(p.matches("AbC")); + assertNoMatch(p,"AbC"); p = pp.parse("fO?"); - assertFalse(p.matches("FoO")); + assertNoMatch(p,"FoO"); p = pp.parse("/fO?/bA?"); - assertFalse(p.matches("/FoO/BaR")); + assertNoMatch(p,"/FoO/BaR"); p = pp.parse("/fO?/bA?"); - assertTrue(p.matches("/fOO/bAr")); + assertMatches(p,"/fOO/bAr"); pp = new PathPatternParser(); pp.setCaseSensitive(false); p = pp.parse("{abc:[A-Z]*}_{def:[A-Z]*}"); - assertTrue(p.matches("abc_abc")); - assertTrue(p.matches("ABC_aBc")); + assertMatches(p,"abc_abc"); + assertMatches(p,"ABC_aBc"); pp = new PathPatternParser(); pp.setCaseSensitive(true); p = pp.parse("{abc:[A-Z]*}_{def:[A-Z]*}"); - assertFalse(p.matches("abc_abc")); - assertTrue(p.matches("ABC_ABC")); + assertNoMatch(p,"abc_abc"); + assertMatches(p,"ABC_ABC"); pp = new PathPatternParser(); pp.setCaseSensitive(false); p = pp.parse("*?a?*"); - assertTrue(p.matches("bab")); - assertTrue(p.matches("bAb")); + assertMatches(p,"bab"); + assertMatches(p,"bAb"); pp = new PathPatternParser(); pp.setCaseSensitive(true); p = pp.parse("*?A?*"); - assertFalse(p.matches("bab")); - assertTrue(p.matches("bAb")); + assertNoMatch(p,"bab"); + assertMatches(p,"bAb"); } + @Ignore @Test public void alternativeDelimiter() { try { this.separator = '.'; // test exact matching - checkMatches("test", "test"); - checkMatches(".test", ".test"); - checkNoMatch(".test/jpg", "test/jpg"); - checkNoMatch("test", ".test"); - checkNoMatch(".test", "test"); +// checkMatches("test", "test"); +// checkMatches(".test", ".test"); +// checkNoMatch(".test/jpg", "test/jpg"); +// checkNoMatch("test", ".test"); +// checkNoMatch(".test", "test"); // test matching with ?'s checkMatches("t?st", "test"); @@ -893,52 +868,51 @@ public class PathPatternMatcherTests { @Test public void extractUriTemplateVariables_spr15264() { - PathPattern pp = new PathPatternParser().parse("/{foo}"); - assertTrue(pp.matches("/abc")); - assertFalse(pp.matches("/")); - assertFalse(pp.matches("//")); + PathPattern pp; + pp = new PathPatternParser().parse("/{foo}"); + assertMatches(pp,"/abc"); + assertNoMatch(pp,"/"); + assertNoMatch(pp,"//"); checkCapture("/{foo}", "/abc", "foo", "abc"); pp = new PathPatternParser().parse("/{foo}/{bar}"); - assertTrue(pp.matches("/abc/def")); - assertFalse(pp.matches("/def")); - assertFalse(pp.matches("/")); - assertFalse(pp.matches("//def")); - assertFalse(pp.matches("//")); - + assertMatches(pp,"/abc/def"); + assertNoMatch(pp,"/def"); + assertNoMatch(pp,"/"); + assertNoMatch(pp,"//def"); + assertNoMatch(pp,"//"); pp = parse("/{foo}/boo"); - assertTrue(pp.matches("/abc/boo")); - assertTrue(pp.matches("/a/boo")); - assertFalse(pp.matches("/boo")); - assertFalse(pp.matches("//boo")); + assertMatches(pp,"/abc/boo"); + assertMatches(pp,"/a/boo"); + assertNoMatch(pp,"/boo"); + assertNoMatch(pp,"//boo"); - pp = parse("/{foo}*"); - assertTrue(pp.matches("/abc")); - assertFalse(pp.matches("/")); + assertMatches(pp,"/abc"); + assertNoMatch(pp,"/"); checkCapture("/{word:[a-z]*}", "/abc", "word", "abc"); pp = parse("/{word:[a-z]*}"); - assertFalse(pp.matches("/1")); - assertTrue(pp.matches("/a")); - assertFalse(pp.matches("/")); + assertNoMatch(pp,"/1"); + assertMatches(pp,"/a"); + assertNoMatch(pp,"/"); // Two captures mean we use a RegexPathElement pp = new PathPatternParser().parse("/{foo}{bar}"); - assertTrue(pp.matches("/abcdef")); - assertFalse(pp.matches("/")); - assertFalse(pp.matches("//")); + assertMatches(pp,"/abcdef"); + assertNoMatch(pp,"/"); + assertNoMatch(pp,"//"); checkCapture("/{foo:[a-z][a-z]}{bar:[a-z]}", "/abc", "foo", "ab", "bar", "c"); // Only patterns not capturing variables cannot match against just / PathPatternParser ppp = new PathPatternParser(); ppp.setMatchOptionalTrailingSlash(true); pp = ppp.parse("/****"); - assertTrue(pp.matches("/abcdef")); - assertTrue(pp.matches("/")); - assertTrue(pp.matches("/")); - assertTrue(pp.matches("//")); + assertMatches(pp,"/abcdef"); + assertMatches(pp,"/"); + assertMatches(pp,"/"); + assertMatches(pp,"//"); // Confirming AntPathMatcher behaviour: assertFalse(new AntPathMatcher().match("/{foo}", "/")); @@ -954,11 +928,14 @@ public class PathPatternMatcherTests { @Test public void extractUriTemplateVariables() throws Exception { + assertMatches(parse("{hotel}"),"1"); + assertMatches(parse("/hotels/{hotel}"),"/hotels/1"); checkCapture("/hotels/{hotel}", "/hotels/1", "hotel", "1"); checkCapture("/h?tels/{hotel}", "/hotels/1", "hotel", "1"); checkCapture("/hotels/{hotel}/bookings/{booking}", "/hotels/1/bookings/2", "hotel", "1", "booking", "2"); checkCapture("/*/hotels/*/{hotel}", "/foo/hotels/bar/1", "hotel", "1"); checkCapture("/{page}.html", "/42.html", "page", "42"); + checkNoMatch("/{var}","/"); checkCapture("/{page}.*", "/42.html", "page", "42"); checkCapture("/A-{B}-C", "/A-b-C", "B", "b"); checkCapture("/{name}.{extension}", "/test.html", "name", "test", "extension", "html"); @@ -985,7 +962,7 @@ public class PathPatternMatcherTests { checkCapture("/foo/{bar}/boo/{baz}", "/foo/plum/boo/apple", "bar", "plum", "baz", "apple"); checkCapture("/{bla}.*", "/testing.html", "bla", "testing"); - Map extracted = checkCapture("/abc", "/abc"); + Map extracted = checkCapture("/abc", "/abc"); assertEquals(0, extracted.size()); checkCapture("/{bla}/foo","/a/foo"); } @@ -996,14 +973,14 @@ public class PathPatternMatcherTests { PathPattern p = null; p = pp.parse("{symbolicName:[\\w\\.]+}-{version:[\\w\\.]+}.jar"); - Map result = p.matchAndExtract("com.example-1.0.0.jar"); - assertEquals("com.example", result.get("symbolicName")); - assertEquals("1.0.0", result.get("version")); + Map result = matchAndExtract(p, "com.example-1.0.0.jar"); + assertEquals("com.example", result.get("symbolicName").value()); + assertEquals("1.0.0", result.get("version").value()); p = pp.parse("{symbolicName:[\\w\\.]+}-sources-{version:[\\w\\.]+}.jar"); - result = p.matchAndExtract("com.example-sources-1.0.0.jar"); - assertEquals("com.example", result.get("symbolicName")); - assertEquals("1.0.0", result.get("version")); + result = matchAndExtract(p, "com.example-sources-1.0.0.jar"); + assertEquals("com.example", result.get("symbolicName").value()); + assertEquals("1.0.0", result.get("version").value()); } @Test @@ -1011,31 +988,31 @@ public class PathPatternMatcherTests { PathPatternParser pp = new PathPatternParser(); PathPattern p = pp.parse("{symbolicName:[\\p{L}\\.]+}-sources-{version:[\\p{N}\\.]+}.jar"); - Map result = p.matchAndExtract("com.example-sources-1.0.0.jar"); - assertEquals("com.example", result.get("symbolicName")); - assertEquals("1.0.0", result.get("version")); + Map result = p.matchAndExtract(toPathContainer("com.example-sources-1.0.0.jar")); + assertEquals("com.example", result.get("symbolicName").value()); + assertEquals("1.0.0", result.get("version").value()); p = pp.parse("{symbolicName:[\\w\\.]+}-sources-{version:[\\d\\.]+}-{year:\\d{4}}{month:\\d{2}}{day:\\d{2}}.jar"); - result = p.matchAndExtract("com.example-sources-1.0.0-20100220.jar"); - assertEquals("com.example", result.get("symbolicName")); - assertEquals("1.0.0", result.get("version")); - assertEquals("2010", result.get("year")); - assertEquals("02", result.get("month")); - assertEquals("20", result.get("day")); + result = matchAndExtract(p,"com.example-sources-1.0.0-20100220.jar"); + assertEquals("com.example", result.get("symbolicName").value()); + assertEquals("1.0.0", result.get("version").value()); + assertEquals("2010", result.get("year").value()); + assertEquals("02", result.get("month").value()); + assertEquals("20", result.get("day").value()); p = pp.parse("{symbolicName:[\\p{L}\\.]+}-sources-{version:[\\p{N}\\.\\{\\}]+}.jar"); - result = p.matchAndExtract("com.example-sources-1.0.0.{12}.jar"); - assertEquals("com.example", result.get("symbolicName")); - assertEquals("1.0.0.{12}", result.get("version")); + result = matchAndExtract(p, "com.example-sources-1.0.0.{12}.jar"); + assertEquals("com.example", result.get("symbolicName").value()); + assertEquals("1.0.0.{12}", result.get("version").value()); } @Test public void extractUriTemplateVarsRegexCapturingGroups() { - PathPatternParser pp = new PathPatternParser(); - PathPattern pathMatcher = pp.parse("/web/{id:foo(bar)?}_{goo}"); + PathPatternParser ppp = new PathPatternParser(); + PathPattern pathPattern = ppp.parse("/web/{id:foo(bar)?}_{goo}"); exception.expect(IllegalArgumentException.class); exception.expectMessage(containsString("The number of capturing groups in the pattern")); - pathMatcher.matchAndExtract("/web/foobar_goo"); + matchAndExtract(pathPattern,"/web/foobar_goo"); } @Rule @@ -1160,12 +1137,12 @@ public class PathPatternMatcherTests { PathPatternParser parser = new PathPatternParser(); PathPattern p1 = parser.parse("/{foo}"); PathPattern p2 = parser.parse("/{foo}.*"); - Map r1 = p1.matchAndExtract("/file.txt"); - Map r2 = p2.matchAndExtract("/file.txt"); + Map r1 = matchAndExtract(p1, "/file.txt"); + Map r2 = matchAndExtract(p2, "/file.txt"); // works fine - assertEquals(r1.get("foo"), "file.txt"); - assertEquals(r2.get("foo"), "file"); + assertEquals("file.txt", r1.get("foo").value()); + assertEquals("file", r2.get("foo").value()); // This produces 2 (see comments in https://jira.spring.io/browse/SPR-14544 ) // Comparator patternComparator = new AntPathMatcher().getPatternComparator(""); @@ -1283,58 +1260,105 @@ public class PathPatternMatcherTests { PathPatternParser pp = new PathPatternParser(); pp.setCaseSensitive(false); PathPattern p = pp.parse("/group/{groupName}/members"); - assertTrue(p.matches("/group/sales/members")); - assertTrue(p.matches("/Group/Sales/Members")); - assertTrue(p.matches("/group/Sales/members")); + assertMatches(p,"/group/sales/members"); + assertMatches(p,"/Group/Sales/Members"); + assertMatches(p,"/group/Sales/members"); + } + + + @Test + public void parameters() { + // CaptureVariablePathElement + Map result = matchAndExtract("/abc/{var}","/abc/one;two=three;four=five"); + assertEquals("one",result.get("var").value()); + assertEquals("[three]",result.get("var").parameters().get("two").toString()); + assertEquals("[five]",result.get("var").parameters().get("four").toString()); + // RegexPathElement + result = matchAndExtract("/abc/{var1}_{var2}","/abc/123_456;a=b;c=d"); + assertEquals("123",result.get("var1").value()); + assertEquals("456",result.get("var2").value()); + // vars associated with second variable + assertNull(result.get("var1").parameters().get("a")); + assertNull(result.get("var1").parameters().get("c")); + assertEquals("[b]",result.get("var2").parameters().get("a").toString()); + assertEquals("[d]",result.get("var2").parameters().get("c").toString()); + // CaptureTheRestPathElement + result = matchAndExtract("/{*var}","/abc/123_456;a=b;c=d"); + assertEquals("/abc/123_456",result.get("var").value()); + assertEquals("[b]",result.get("var").parameters().get("a").toString()); + assertEquals("[d]",result.get("var").parameters().get("c").toString()); + result = matchAndExtract("/{*var}","/abc/123_456;a=b;c=d/789;a=e;f=g"); + assertEquals("/abc/123_456/789",result.get("var").value()); + assertEquals("[b, e]",result.get("var").parameters().get("a").toString()); + assertEquals("[d]",result.get("var").parameters().get("c").toString()); + assertEquals("[g]",result.get("var").parameters().get("f").toString()); + + result = matchAndExtract("/abc/{var}","/abc/one"); + assertEquals("one",result.get("var").value()); + assertEquals(0,result.get("var").parameters().size()); } + // --- + private Map matchAndExtract(String pattern, String path) { + return parse(pattern).matchAndExtract(PathPatternMatcherTests.toPathContainer(path)); + } + private PathPattern parse(String path) { PathPatternParser pp = new PathPatternParser(); pp.setMatchOptionalTrailingSlash(true); return pp.parse(path); } - + + public static PathContainer toPathContainer(String path) { + if (path == null) { + return null; + } + return PathContainer.parse(path, StandardCharsets.UTF_8); + } + private void checkMatches(String uriTemplate, String path) { PathPatternParser parser = new PathPatternParser(this.separator); parser.setMatchOptionalTrailingSlash(true); PathPattern p = parser.parse(uriTemplate); - assertTrue(p.matches(path)); + PathContainer pc = toPathContainer(path); + assertTrue(p.matches(pc)); } private void checkStartNoMatch(String uriTemplate, String path) { PathPatternParser p = new PathPatternParser(); p.setMatchOptionalTrailingSlash(true); PathPattern pattern = p.parse(uriTemplate); - assertFalse(pattern.matchStart(path)); + assertFalse(pattern.matchStart(toPathContainer(path))); } private void checkStartMatches(String uriTemplate, String path) { PathPatternParser p = new PathPatternParser(); p.setMatchOptionalTrailingSlash(true); PathPattern pattern = p.parse(uriTemplate); - assertTrue(pattern.matchStart(path)); + assertTrue(pattern.matchStart(toPathContainer(path))); } private void checkNoMatch(String uriTemplate, String path) { PathPatternParser p = new PathPatternParser(); PathPattern pattern = p.parse(uriTemplate); - assertFalse(pattern.matches(path)); + PathContainer PathContainer = toPathContainer(path); + assertFalse(pattern.matches(PathContainer)); } - private Map checkCapture(String uriTemplate, String path, String... keyValues) { + private Map checkCapture(String uriTemplate, String path, String... keyValues) { PathPatternParser parser = new PathPatternParser(); PathPattern pattern = parser.parse(uriTemplate); - Map matchResults = pattern.matchAndExtract(path); + Map matchResults = pattern.matchAndExtract(toPathContainer(path)); Map expectedKeyValues = new HashMap<>(); if (keyValues != null) { for (int i = 0; i < keyValues.length; i += 2) { expectedKeyValues.put(keyValues[i], keyValues[i + 1]); } } - Map capturedVariables = matchResults; + Map capturedVariables = matchResults; for (Map.Entry me : expectedKeyValues.entrySet()) { - String value = capturedVariables.get(me.getKey()); + String value = capturedVariables.get(me.getKey()).value(); if (value == null) { fail("Did not find key '" + me.getKey() + "' in captured variables: " + capturedVariables); @@ -1361,9 +1385,30 @@ public class PathPatternMatcherTests { public String combine(String string1, String string2) { PathPattern pattern1 = pp.parse(string1); - return pattern1.combine(string2); + PathPattern pattern2 = pp.parse(string2); + return pattern1.combine(pattern2).getPatternString(); } } + private PathRemainingMatchInfo getPathRemaining(String pattern, String path) { + return parse(pattern).getPathRemaining(toPathContainer(path)); + } + + private PathRemainingMatchInfo getPathRemaining(PathPattern pattern, String path) { + return pattern.getPathRemaining(toPathContainer(path)); + } + + private Map matchAndExtract(PathPattern p, String path) { + return p.matchAndExtract(toPathContainer(path)); + } + + private String elementsToString(List elements) { + StringBuilder s = new StringBuilder(); + for (Element element: elements) { + s.append("[").append(element.value()).append("]"); + } + return s.toString(); + } + } diff --git a/spring-web/src/test/java/org/springframework/web/util/pattern/PathPatternParserTests.java b/spring-web/src/test/java/org/springframework/web/util/pattern/PathPatternParserTests.java index 31f13e43b71..8ae5441b49f 100644 --- a/spring-web/src/test/java/org/springframework/web/util/pattern/PathPatternParserTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/pattern/PathPatternParserTests.java @@ -22,6 +22,8 @@ import java.util.List; import java.util.Map; import org.junit.Test; +import org.springframework.http.server.reactive.PathContainer; +import org.springframework.web.util.pattern.PathPattern.PathMatchResult; import org.springframework.web.util.pattern.PatternParseException.PatternMessage; import static org.junit.Assert.*; @@ -35,7 +37,6 @@ public class PathPatternParserTests { private PathPattern pathPattern; - @Test public void basicPatterns() { checkStructure("/"); @@ -76,8 +77,8 @@ public class PathPatternParserTests { @Test public void captureTheRestPatterns() { - checkError("/{*foobar}x{abc}", 10, PatternMessage.NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST); - pathPattern = checkStructure("{*foobar}"); + pathPattern = parse("{*foobar}"); + assertEquals("/{*foobar}", pathPattern.computePatternString()); assertPathElements(pathPattern, CaptureTheRestPathElement.class); pathPattern = checkStructure("/{*foobar}"); assertPathElements(pathPattern, CaptureTheRestPathElement.class); @@ -125,34 +126,34 @@ public class PathPatternParserTests { pathPattern = checkStructure("/{var:\\\\}"); assertEquals(CaptureVariablePathElement.class.getName(), pathPattern.getHeadSection().next.getClass().getName()); - assertTrue(pathPattern.matches("/\\")); + assertMatches(pathPattern,"/\\"); pathPattern = checkStructure("/{var:\\/}"); assertEquals(CaptureVariablePathElement.class.getName(), pathPattern.getHeadSection().next.getClass().getName()); - assertFalse(pathPattern.matches("/aaa")); + assertNoMatch(pathPattern,"/aaa"); - pathPattern = checkStructure("/{var:a{1,2}}", 1); + pathPattern = checkStructure("/{var:a{1,2}}"); assertEquals(CaptureVariablePathElement.class.getName(), pathPattern.getHeadSection().next.getClass().getName()); - pathPattern = checkStructure("/{var:[^\\/]*}", 1); + pathPattern = checkStructure("/{var:[^\\/]*}"); assertEquals(CaptureVariablePathElement.class.getName(), pathPattern.getHeadSection().next.getClass().getName()); - Map result = pathPattern.matchAndExtract("/foo"); - assertEquals("foo", result.get("var")); + Map result = matchAndExtract(pathPattern,"/foo"); + assertEquals("foo", result.get("var").value()); - pathPattern = checkStructure("/{var:\\[*}", 1); + pathPattern = checkStructure("/{var:\\[*}"); assertEquals(CaptureVariablePathElement.class.getName(), pathPattern.getHeadSection().next.getClass().getName()); - result = pathPattern.matchAndExtract("/[[["); - assertEquals("[[[", result.get("var")); + result = matchAndExtract(pathPattern,"/[[["); + assertEquals("[[[", result.get("var").value()); - pathPattern = checkStructure("/{var:[\\{]*}", 1); + pathPattern = checkStructure("/{var:[\\{]*}"); assertEquals(CaptureVariablePathElement.class.getName(), pathPattern.getHeadSection().next.getClass().getName()); - result = pathPattern.matchAndExtract("/{{{"); - assertEquals("{{{", result.get("var")); + result = matchAndExtract(pathPattern,"/{{{"); + assertEquals("{{{", result.get("var").value()); - pathPattern = checkStructure("/{var:[\\}]*}", 1); + pathPattern = checkStructure("/{var:[\\}]*}"); assertEquals(CaptureVariablePathElement.class.getName(), pathPattern.getHeadSection().next.getClass().getName()); - result = pathPattern.matchAndExtract("/}}}"); - assertEquals("}}}", result.get("var")); + result = matchAndExtract(pathPattern,"/}}}"); + assertEquals("}}}", result.get("var").value()); pathPattern = checkStructure("*"); assertEquals(WildcardPathElement.class.getName(), pathPattern.getHeadSection().getClass().getName()); @@ -170,7 +171,6 @@ public class PathPatternParserTests { pathPattern = checkStructure("{symbolicName:[\\p{L}\\.]+}-sources-{version:[\\p{N}\\.]+}.jar"); assertEquals(RegexPathElement.class.getName(), pathPattern.getHeadSection().getClass().getName()); - } @Test @@ -182,63 +182,9 @@ public class PathPatternParserTests { checkStructure("/{foo}/{bar}/{wibble}"); } - /** - * During a parse some elements of the path are encoded for use when matching an encoded path. - * The patterns a developer writes are not encoded, hence we decode them when turning them - * into PathPattern objects. The encoding is visible through the toChainString() method. - */ - @Test - public void encodingDuringParse() throws Exception { - PathPattern pp; - - // CaptureTheRest - pp = parse("/{*var}"); - assertEquals("CaptureTheRest(/{*var})",pp.toChainString()); - - // CaptureVariable - pp = parse("/{var}"); - assertEquals("Separator(/) CaptureVariable({var})",pp.toChainString()); - - // Literal - pp = parse("/foo bar/b_oo"); - assertEquals("Separator(/) Literal(foo%20bar) Separator(/) Literal(b_oo)",pp.toChainString()); - pp = parse("foo:bar"); - assertEquals("Literal(foo:bar)",pp.toChainString()); - - // Regex - pp = parse("{foo}_{bar}"); - assertEquals("Regex({foo}_{bar})",pp.toChainString()); - pp = parse("{foo}_ _{bar}"); - assertEquals("Regex({foo}_%20_{bar})",pp.toChainString()); - - // Separator - pp = parse("/"); - assertEquals("Separator(/)",pp.toChainString()); - - // SingleCharWildcarded - pp = parse("/foo?bar"); - assertEquals("Separator(/) SingleCharWildcarded(foo?bar)",pp.toChainString()); - pp = parse("/f o?bar"); - assertEquals("Separator(/) SingleCharWildcarded(f%20o?bar)",pp.toChainString()); - - // Wildcard - pp = parse("/foo*bar"); - assertEquals("Separator(/) Regex(foo*bar)",pp.toChainString()); - pp = parse("f oo:*bar"); - assertEquals("Regex(f%20oo:*bar)",pp.toChainString()); - pp = parse("/f oo:*bar"); - assertEquals("Separator(/) Regex(f%20oo:*bar)",pp.toChainString()); - pp = parse("/f|!oo:*bar"); - assertEquals("Separator(/) Regex(f%7C!oo:*bar)",pp.toChainString()); - - // WildcardTheRest - pp = parse("/**"); - assertEquals("WildcardTheRest(/**)",pp.toChainString()); - } - @Test - public void encodingWithConstraints() { - // Constraint regex expressions are not URL encoded + public void noEncoding() { + // Check no encoding of expressions or constraints PathPattern pp = parse("/{var:f o}"); assertEquals("Separator(/) CaptureVariable({var:f o})",pp.toChainString()); @@ -246,7 +192,7 @@ public class PathPatternParserTests { assertEquals("Separator(/) Regex({var:f o}_)",pp.toChainString()); pp = parse("{foo:f o}_ _{bar:b\\|o}"); - assertEquals("Regex({foo:f o}_%20_{bar:b\\|o})",pp.toChainString()); + assertEquals("Regex({foo:f o}_ _{bar:b\\|o})",pp.toChainString()); } @Test @@ -292,14 +238,14 @@ public class PathPatternParserTests { checkError("/foobar/{abc:..}_{abc:..}", 8, PatternMessage.ILLEGAL_DOUBLE_CAPTURE); PathPattern pp = parse("/{abc:foo(bar)}"); try { - pp.matchAndExtract("/foo"); + pp.matchAndExtract(toPSC("/foo")); fail("Should have raised exception"); } catch (IllegalArgumentException iae) { assertEquals("No capture groups allowed in the constraint regex: foo(bar)", iae.getMessage()); } try { - pp.matchAndExtract("/foobar"); + pp.matchAndExtract(toPSC("/foobar")); fail("Should have raised exception"); } catch (IllegalArgumentException iae) { @@ -365,12 +311,12 @@ public class PathPatternParserTests { public void multipleSeparatorPatterns() { pathPattern = checkStructure("///aaa"); assertEquals(6, pathPattern.getNormalizedLength()); - assertPathElements(pathPattern, SeparatorPathElement.class, SeparatorPathElement.class, - SeparatorPathElement.class, LiteralPathElement.class); + assertPathElements(pathPattern, SeparatorPathElement.class, SeparatorPathElement.class, SeparatorPathElement.class, + LiteralPathElement.class); pathPattern = checkStructure("///aaa////aaa/b"); assertEquals(15, pathPattern.getNormalizedLength()); - assertPathElements(pathPattern, SeparatorPathElement.class, SeparatorPathElement.class, - SeparatorPathElement.class, LiteralPathElement.class, SeparatorPathElement.class, + assertPathElements(pathPattern, SeparatorPathElement.class, SeparatorPathElement.class, SeparatorPathElement.class, + LiteralPathElement.class, SeparatorPathElement.class, SeparatorPathElement.class, SeparatorPathElement.class, SeparatorPathElement.class, LiteralPathElement.class, SeparatorPathElement.class, LiteralPathElement.class); pathPattern = checkStructure("/////**"); @@ -464,36 +410,18 @@ public class PathPatternParserTests { assertEquals(p2, patterns.get(1)); } - private PathPattern parse(String pattern) { PathPatternParser patternParser = new PathPatternParser(); return patternParser.parse(pattern); } /** - * Verify the parsed chain of sections matches the original pattern and the separator count - * that has been determined is correct. + * Verify the pattern string computed for a parsed pattern matches the original pattern text */ private PathPattern checkStructure(String pattern) { - int count = 0; - for (int i = 0; i < pattern.length(); i++) { - if (pattern.charAt(i) == '/') { -// if (peekDoubleWildcard(pattern,i)) { -// // it is /** -// i+=2; -// } else { - count++; -// } - } - } - return checkStructure(pattern, count); - } - - private PathPattern checkStructure(String pattern, int expectedSeparatorCount) { - pathPattern = parse(pattern); - assertEquals(pattern, pathPattern.getPatternString()); - // assertEquals(expectedSeparatorCount, pathPattern.getSeparatorCount()); - return pathPattern; + PathPattern pp = parse(pattern); + assertEquals(pattern, pp.computePatternString()); + return pp; } private void checkError(String pattern, int expectedPos, PatternMessage expectedMessage, String... expectedInserts) { @@ -531,4 +459,20 @@ public class PathPatternParserTests { return capturedVariableCount + wildcardCount * 100; } + private void assertMatches(PathPattern pp, String path) { + assertTrue(pp.matches(PathPatternMatcherTests.toPathContainer(path))); + } + + private void assertNoMatch(PathPattern pp, String path) { + assertFalse(pp.matches(PathPatternMatcherTests.toPathContainer(path))); + } + + private Map matchAndExtract(PathPattern pp, String path) { + return pp.matchAndExtract(PathPatternMatcherTests.toPathContainer(path)); + } + + private PathContainer toPSC(String path) { + return PathPatternMatcherTests.toPathContainer(path); + } + }