From 584b290dff472d93abed673e17f4f8cc1b04be37 Mon Sep 17 00:00:00 2001 From: Andy Clement Date: Wed, 5 Apr 2017 22:53:25 -0700 Subject: [PATCH] Introduce method to allow a pattern to partially consume a path With this change there is a new getPathRemaining() method on PathPattern objects. It is called with a path and returns the path remaining once the path pattern in question has matched as much as it can of that path. For example if the pattern is /fo* and the path is /foo/bar then getPathRemaining will return /bar. This allows for a set of pathpatterns to work together in sequence to match a complete entire path. Issue: SPR-15336 --- .../patterns/CaptureTheRestPathElement.java | 13 ++--- .../patterns/CaptureVariablePathElement.java | 14 +++-- .../patterns/InternalPathPatternParser.java | 12 ++--- .../web/util/patterns/LiteralPathElement.java | 12 +++-- .../web/util/patterns/PathElement.java | 17 +++++- .../web/util/patterns/PathPattern.java | 52 ++++++++++++++++++- .../web/util/patterns/RegexPathElement.java | 19 ++++--- .../util/patterns/SeparatorPathElement.java | 13 +++-- .../SingleCharWildcardedPathElement.java | 12 +++-- .../util/patterns/WildcardPathElement.java | 12 +++-- .../patterns/WildcardTheRestPathElement.java | 8 +-- .../patterns/PathPatternMatcherTests.java | 49 +++++++++++++++++ 12 files changed, 191 insertions(+), 42 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/util/patterns/CaptureTheRestPathElement.java b/spring-web/src/main/java/org/springframework/web/util/patterns/CaptureTheRestPathElement.java index 9e4fad18a31..ef6e48e3dfa 100644 --- a/spring-web/src/main/java/org/springframework/web/util/patterns/CaptureTheRestPathElement.java +++ b/spring-web/src/main/java/org/springframework/web/util/patterns/CaptureTheRestPathElement.java @@ -28,17 +28,14 @@ class CaptureTheRestPathElement extends PathElement { private String variableName; - private char separator; - /** - * @param pos + * @param pos position of the path element within the path pattern text * @param captureDescriptor a character array containing contents like '{' '*' 'a' 'b' '}' - * @param separator the separator ahead of this construct + * @param separator the separator used in the path pattern */ CaptureTheRestPathElement(int pos, char[] captureDescriptor, char separator) { - super(pos); + super(pos, separator); variableName = new String(captureDescriptor, 2, captureDescriptor.length - 3); - this.separator = separator; } @Override @@ -56,6 +53,10 @@ class CaptureTheRestPathElement extends PathElement { matchingContext.candidate[candidateIndex + 1] == separator) { candidateIndex++; } + if (matchingContext.determineRemaining) { + matchingContext.remainingPathIndex = matchingContext.candidateLength; + return true; + } if (matchingContext.extractingVariables) { matchingContext.set(variableName, new String(matchingContext.candidate, candidateIndex, matchingContext.candidateLength - candidateIndex)); diff --git a/spring-web/src/main/java/org/springframework/web/util/patterns/CaptureVariablePathElement.java b/spring-web/src/main/java/org/springframework/web/util/patterns/CaptureVariablePathElement.java index 0a0cb08e66c..6028f28dfdf 100644 --- a/spring-web/src/main/java/org/springframework/web/util/patterns/CaptureVariablePathElement.java +++ b/spring-web/src/main/java/org/springframework/web/util/patterns/CaptureVariablePathElement.java @@ -36,8 +36,8 @@ class CaptureVariablePathElement extends PathElement { * @param pos the position in the pattern of this capture element * @param captureDescriptor is of the form {AAAAA[:pattern]} */ - CaptureVariablePathElement(int pos, char[] captureDescriptor, boolean caseSensitive) { - super(pos); + CaptureVariablePathElement(int pos, char[] captureDescriptor, boolean caseSensitive, char separator) { + super(pos, separator); int colon = -1; for (int i = 0; i < captureDescriptor.length; i++) { if (captureDescriptor[i] == ':') { @@ -80,8 +80,14 @@ class CaptureVariablePathElement extends PathElement { } boolean match = false; if (next == null) { - // Needs to be at least one character #SPR15264 - match = (nextPos == matchingContext.candidateLength && nextPos > candidateIndex); + if (matchingContext.determineRemaining && nextPos > candidateIndex) { + matchingContext.remainingPathIndex = nextPos; + match = true; + } + else { + // Needs to be at least one character #SPR15264 + match = (nextPos == matchingContext.candidateLength && nextPos > candidateIndex); + } } else { if (matchingContext.isMatchStartMatching && nextPos == matchingContext.candidateLength) { diff --git a/spring-web/src/main/java/org/springframework/web/util/patterns/InternalPathPatternParser.java b/spring-web/src/main/java/org/springframework/web/util/patterns/InternalPathPatternParser.java index 147eb7e9933..07415178e17 100644 --- a/spring-web/src/main/java/org/springframework/web/util/patterns/InternalPathPatternParser.java +++ b/spring-web/src/main/java/org/springframework/web/util/patterns/InternalPathPatternParser.java @@ -318,7 +318,7 @@ public class InternalPathPatternParser { else { // It is a full capture of this element (possibly with constraint), for example: /foo/{abc}/ try { - newPE = new CaptureVariablePathElement(pathElementStart, pathElementText, caseSensitive); + newPE = new CaptureVariablePathElement(pathElementStart, pathElementText, caseSensitive, separator); } catch (PatternSyntaxException pse) { throw new PatternParseException(pse, findRegexStart(pathPatternData, pathElementStart) @@ -333,7 +333,7 @@ public class InternalPathPatternParser { PatternMessage.CAPTURE_ALL_IS_STANDALONE_CONSTRUCT); } RegexPathElement newRegexSection = new RegexPathElement(pathElementStart, pathElementText, - caseSensitive, pathPatternData); + caseSensitive, pathPatternData, separator); for (String variableName : newRegexSection.getVariableNames()) { recordCapturedVariable(pathElementStart, variableName); } @@ -343,18 +343,18 @@ public class InternalPathPatternParser { else { if (wildcard) { if (pos - 1 == pathElementStart) { - newPE = new WildcardPathElement(pathElementStart); + newPE = new WildcardPathElement(pathElementStart, separator); } else { - newPE = new RegexPathElement(pathElementStart, pathElementText, caseSensitive, pathPatternData); + newPE = new RegexPathElement(pathElementStart, pathElementText, caseSensitive, pathPatternData, separator); } } else if (singleCharWildcardCount != 0) { newPE = new SingleCharWildcardedPathElement(pathElementStart, pathElementText, - singleCharWildcardCount, caseSensitive); + singleCharWildcardCount, caseSensitive, separator); } else { - newPE = new LiteralPathElement(pathElementStart, pathElementText, caseSensitive); + newPE = new LiteralPathElement(pathElementStart, pathElementText, caseSensitive, separator); } } return newPE; diff --git a/spring-web/src/main/java/org/springframework/web/util/patterns/LiteralPathElement.java b/spring-web/src/main/java/org/springframework/web/util/patterns/LiteralPathElement.java index 147cb269f65..1bc9e6feaa4 100644 --- a/spring-web/src/main/java/org/springframework/web/util/patterns/LiteralPathElement.java +++ b/spring-web/src/main/java/org/springframework/web/util/patterns/LiteralPathElement.java @@ -32,8 +32,8 @@ class LiteralPathElement extends PathElement { private boolean caseSensitive; - public LiteralPathElement(int pos, char[] literalText, boolean caseSensitive) { - super(pos); + public LiteralPathElement(int pos, char[] literalText, boolean caseSensitive, char separator) { + super(pos, separator); this.len = literalText.length; this.caseSensitive = caseSensitive; if (caseSensitive) { @@ -69,7 +69,13 @@ class LiteralPathElement extends PathElement { } } if (next == null) { - return candidateIndex == matchingContext.candidateLength; + if (matchingContext.determineRemaining && nextIfExistsIsSeparator(candidateIndex, matchingContext)) { + matchingContext.remainingPathIndex = candidateIndex; + return true; + } + else { + return candidateIndex == matchingContext.candidateLength; + } } else { if (matchingContext.isMatchStartMatching && candidateIndex == matchingContext.candidateLength) { diff --git a/spring-web/src/main/java/org/springframework/web/util/patterns/PathElement.java b/spring-web/src/main/java/org/springframework/web/util/patterns/PathElement.java index 4cb84cd7597..fc308b3f7f3 100644 --- a/spring-web/src/main/java/org/springframework/web/util/patterns/PathElement.java +++ b/spring-web/src/main/java/org/springframework/web/util/patterns/PathElement.java @@ -45,13 +45,20 @@ abstract class PathElement { * The previous path element in the chain */ protected PathElement prev; + + /** + * The separator used in this path pattern + */ + protected char separator; /** * Create a new path element. * @param pos the position where this path element starts in the pattern data + * @param separator the separator in use in the path pattern */ - PathElement(int pos) { + PathElement(int pos, char separator) { this.pos = pos; + this.separator = separator; } /** @@ -88,4 +95,12 @@ abstract class PathElement { public int getScore() { return 0; } + + /** + * @return 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] == separator); + } } \ No newline at end of file diff --git a/spring-web/src/main/java/org/springframework/web/util/patterns/PathPattern.java b/spring-web/src/main/java/org/springframework/web/util/patterns/PathPattern.java index 6bf7fca5e6b..68fa32ec6c0 100644 --- a/spring-web/src/main/java/org/springframework/web/util/patterns/PathPattern.java +++ b/spring-web/src/main/java/org/springframework/web/util/patterns/PathPattern.java @@ -147,6 +147,42 @@ public class PathPattern implements Comparable { return 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 + * @return the remaining path after as much has been consumed as possible by this pattern, + * result can be the empty string if the path is entirely consumed or it will be null + * if the path does not match + */ + public String getPathRemaining(String path) { + if (head == null) { + if (path == null) { + return path; + } + else { + return hasLength(path)?path:""; + } + } + else if (!hasLength(path)) { + return null; + } + MatchingContext matchingContext = new MatchingContext(path, false); + matchingContext.setMatchAllowExtraPath(); + boolean matches = head.matches(0, matchingContext); + if (!matches) { + return null; + } + else { + if (matchingContext.remainingPathIndex == path.length()) { + return ""; + } + else { + return path.substring(matchingContext.remainingPathIndex); + } + } + } + /** * @param path the path to check against the pattern * @return true if the pattern matches as much of the path as is supplied @@ -384,7 +420,14 @@ public class PathPattern implements Comparable { private Map extractedVariables; - public boolean extractingVariables; + boolean extractingVariables; + + boolean determineRemaining = false; + + // if determineRemaining is true, this is set to the position in + // the candidate where the pattern finished matching - i.e. it + // points to the remaining path that wasn't consumed + int remainingPathIndex; public MatchingContext(String path, boolean extractVariables) { candidate = path.toCharArray(); @@ -392,6 +435,13 @@ public class PathPattern implements Comparable { this.extractingVariables = extractVariables; } + /** + * + */ + public void setMatchAllowExtraPath() { + determineRemaining = true; + } + public void setMatchStartMatching(boolean b) { isMatchStartMatching = b; } diff --git a/spring-web/src/main/java/org/springframework/web/util/patterns/RegexPathElement.java b/spring-web/src/main/java/org/springframework/web/util/patterns/RegexPathElement.java index 810120a158c..e1c35d71bc5 100644 --- a/spring-web/src/main/java/org/springframework/web/util/patterns/RegexPathElement.java +++ b/spring-web/src/main/java/org/springframework/web/util/patterns/RegexPathElement.java @@ -48,8 +48,8 @@ class RegexPathElement extends PathElement { private int wildcardCount; - RegexPathElement(int pos, char[] regex, boolean caseSensitive, char[] completePattern) { - super(pos); + RegexPathElement(int pos, char[] regex, boolean caseSensitive, char[] completePattern, char separator) { + super(pos, separator); this.regex = regex; this.caseSensitive = caseSensitive; buildPattern(regex, completePattern); @@ -124,10 +124,17 @@ class RegexPathElement extends PathElement { boolean matches = m.matches(); if (matches) { if (next == null) { - // No more pattern, is there more data? - // If pattern is capturing variables there must be some actual data to bind to them - matches = (p == matchingContext.candidateLength && - ((this.variableNames.size() == 0) ? true : p > candidateIndex)); + if (matchingContext.determineRemaining && + ((this.variableNames.size() == 0) ? true : p > candidateIndex)) { + matchingContext.remainingPathIndex = p; + 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 = (p == matchingContext.candidateLength && + ((this.variableNames.size() == 0) ? true : p > candidateIndex)); + } } else { if (matchingContext.isMatchStartMatching && p == matchingContext.candidateLength) { diff --git a/spring-web/src/main/java/org/springframework/web/util/patterns/SeparatorPathElement.java b/spring-web/src/main/java/org/springframework/web/util/patterns/SeparatorPathElement.java index 25aaea68b16..f981cd1604a 100644 --- a/spring-web/src/main/java/org/springframework/web/util/patterns/SeparatorPathElement.java +++ b/spring-web/src/main/java/org/springframework/web/util/patterns/SeparatorPathElement.java @@ -28,11 +28,8 @@ import org.springframework.web.util.patterns.PathPattern.MatchingContext; */ class SeparatorPathElement extends PathElement { - private char separator; - SeparatorPathElement(int pos, char separator) { - super(pos); - this.separator = separator; + super(pos, separator); } /** @@ -51,7 +48,13 @@ class SeparatorPathElement extends PathElement { candidateIndex++; } if (next == null) { - matched = ((candidateIndex + 1) == matchingContext.candidateLength); + if (matchingContext.determineRemaining) { + matchingContext.remainingPathIndex = candidateIndex + 1; + matched = true; + } + else { + matched = ((candidateIndex + 1) == matchingContext.candidateLength); + } } else { candidateIndex++; diff --git a/spring-web/src/main/java/org/springframework/web/util/patterns/SingleCharWildcardedPathElement.java b/spring-web/src/main/java/org/springframework/web/util/patterns/SingleCharWildcardedPathElement.java index fbb35ec0fd3..aff8b74cbad 100644 --- a/spring-web/src/main/java/org/springframework/web/util/patterns/SingleCharWildcardedPathElement.java +++ b/spring-web/src/main/java/org/springframework/web/util/patterns/SingleCharWildcardedPathElement.java @@ -35,8 +35,8 @@ class SingleCharWildcardedPathElement extends PathElement { private boolean caseSensitive; - public SingleCharWildcardedPathElement(int pos, char[] literalText, int questionMarkCount, boolean caseSensitive) { - super(pos); + public SingleCharWildcardedPathElement(int pos, char[] literalText, int questionMarkCount, boolean caseSensitive, char separator) { + super(pos, separator); this.len = literalText.length; this.questionMarkCount = questionMarkCount; this.caseSensitive = caseSensitive; @@ -76,7 +76,13 @@ class SingleCharWildcardedPathElement extends PathElement { } } if (next == null) { - return candidateIndex == matchingContext.candidateLength; + if (matchingContext.determineRemaining && nextIfExistsIsSeparator(candidateIndex, matchingContext)) { + matchingContext.remainingPathIndex = candidateIndex; + return true; + } + else { + return candidateIndex == matchingContext.candidateLength; + } } else { if (matchingContext.isMatchStartMatching && candidateIndex == matchingContext.candidateLength) { diff --git a/spring-web/src/main/java/org/springframework/web/util/patterns/WildcardPathElement.java b/spring-web/src/main/java/org/springframework/web/util/patterns/WildcardPathElement.java index 6489b88d55e..a4cf8838cc4 100644 --- a/spring-web/src/main/java/org/springframework/web/util/patterns/WildcardPathElement.java +++ b/spring-web/src/main/java/org/springframework/web/util/patterns/WildcardPathElement.java @@ -27,8 +27,8 @@ import org.springframework.web.util.patterns.PathPattern.MatchingContext; */ class WildcardPathElement extends PathElement { - public WildcardPathElement(int pos) { - super(pos); + public WildcardPathElement(int pos, char separator) { + super(pos, separator); } /** @@ -40,7 +40,13 @@ class WildcardPathElement extends PathElement { public boolean matches(int candidateIndex, MatchingContext matchingContext) { int nextPos = matchingContext.scanAhead(candidateIndex); if (next == null) { - return (nextPos == matchingContext.candidateLength); + if (matchingContext.determineRemaining) { + matchingContext.remainingPathIndex = nextPos; + return true; + } + else { + return (nextPos == matchingContext.candidateLength); + } } else { if (matchingContext.isMatchStartMatching && nextPos == matchingContext.candidateLength) { diff --git a/spring-web/src/main/java/org/springframework/web/util/patterns/WildcardTheRestPathElement.java b/spring-web/src/main/java/org/springframework/web/util/patterns/WildcardTheRestPathElement.java index e8ca8cd76ec..36df46349b1 100644 --- a/spring-web/src/main/java/org/springframework/web/util/patterns/WildcardTheRestPathElement.java +++ b/spring-web/src/main/java/org/springframework/web/util/patterns/WildcardTheRestPathElement.java @@ -27,11 +27,8 @@ import org.springframework.web.util.patterns.PathPattern.MatchingContext; */ class WildcardTheRestPathElement extends PathElement { - private char separator; - WildcardTheRestPathElement(int pos, char separator) { - super(pos); - this.separator = separator; + super(pos, separator); } @Override @@ -41,6 +38,9 @@ class WildcardTheRestPathElement extends PathElement { matchingContext.candidate[candidateIndex] != separator) { return false; } + if (matchingContext.determineRemaining) { + matchingContext.remainingPathIndex = matchingContext.candidateLength; + } return true; } diff --git a/spring-web/src/test/java/org/springframework/web/util/patterns/PathPatternMatcherTests.java b/spring-web/src/test/java/org/springframework/web/util/patterns/PathPatternMatcherTests.java index 3ea0f60e726..a239feb6c51 100644 --- a/spring-web/src/test/java/org/springframework/web/util/patterns/PathPatternMatcherTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/patterns/PathPatternMatcherTests.java @@ -38,6 +38,55 @@ import static org.junit.Assert.*; */ public class PathPatternMatcherTests { + @Test + public void pathRemainderBasicCases_spr15336() { + // getPathRemaining: Given some pattern and some path, return the bit of the path + // that was left over after the pattern part was matched. + + // Cover all PathElement kinds: + assertEquals("/bar", parse("/foo").getPathRemaining("/foo/bar")); + assertEquals("/", parse("/foo").getPathRemaining("/foo/")); + assertEquals("/bar",parse("/foo*").getPathRemaining("/foo/bar")); + assertEquals("/bar", parse("/*").getPathRemaining("/foo/bar")); + assertEquals("/bar", parse("/{foo}").getPathRemaining("/foo/bar")); + assertNull(parse("/foo").getPathRemaining("/bar/baz")); + assertEquals("",parse("/**").getPathRemaining("/foo/bar")); + assertEquals("",parse("/{*bar}").getPathRemaining("/foo/bar")); + assertEquals("/bar",parse("/a?b/d?e").getPathRemaining("/aab/dde/bar")); + assertEquals("/bar",parse("/{abc}abc").getPathRemaining("/xyzabc/bar")); + assertEquals("/bar",parse("/*y*").getPathRemaining("/xyzxyz/bar")); + assertEquals("",parse("/").getPathRemaining("/")); + assertEquals("a",parse("/").getPathRemaining("/a")); + assertEquals("a/",parse("/").getPathRemaining("/a/")); + assertEquals("/bar",parse("/a{abc}").getPathRemaining("/a/bar")); + } + + @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")); + + // 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")); + + // Similar to above for the capture-the-rest variant + assertNull(parse("/resource/{*foo}").getPathRemaining("/resourceX")); + assertEquals("",parse("/resource/{*foo}").getPathRemaining("/resource")); + + assertEquals("/i",parse("/aaa/{bbb}/c?d/e*f/*/g").getPathRemaining("/aaa/b/ccd/ef/x/g/i")); + + assertNull(parse("/a/b").getPathRemaining("")); + assertNull(parse("/a/b").getPathRemaining(null)); + assertEquals("/a/b",parse("").getPathRemaining("/a/b")); + assertEquals("",parse("").getPathRemaining("")); + assertNull(parse("").getPathRemaining(null)); + } + @Test public void basicMatching() { checkMatches(null, null);