提交 584b290d 编写于 作者: A Andy Clement

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
上级 e49d7971
......@@ -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);
}
}
\ No newline at end of file
......@@ -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);
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册