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
This commit is contained in:
parent
e49d797104
commit
584b290dff
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -147,6 +147,42 @@ public class PathPattern implements Comparable<PathPattern> {
|
|||
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<PathPattern> {
|
|||
|
||||
private Map<String, String> 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<PathPattern> {
|
|||
this.extractingVariables = extractVariables;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public void setMatchAllowExtraPath() {
|
||||
determineRemaining = true;
|
||||
}
|
||||
|
||||
public void setMatchStartMatching(boolean b) {
|
||||
isMatchStartMatching = b;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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++;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue