Rework matching to use PathContainer
This commit is contained in:
parent
fc3fcf05fd
commit
26448a0ebc
|
@ -16,8 +16,15 @@
|
||||||
|
|
||||||
package org.springframework.web.util.pattern;
|
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 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
|
* A path element representing capturing the rest of a path. In the pattern
|
||||||
* '/foo/{*foobar}' the /{*foobar} is represented as a {@link CaptureTheRestPathElement}.
|
* '/foo/{*foobar}' the /{*foobar} is represented as a {@link CaptureTheRestPathElement}.
|
||||||
|
@ -42,26 +49,53 @@ class CaptureTheRestPathElement extends PathElement {
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@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
|
// No need to handle 'match start' checking as this captures everything
|
||||||
// anyway and cannot be followed by anything else
|
// anyway and cannot be followed by anything else
|
||||||
// assert next == null
|
// assert next == null
|
||||||
|
|
||||||
// If there is more data, it must start with the separator
|
// If there is more data, it must start with the separator
|
||||||
if (candidateIndex < matchingContext.candidateLength &&
|
if (pathIndex < matchingContext.pathLength && !matchingContext.isSeparator(pathIndex)) {
|
||||||
matchingContext.candidate[candidateIndex] != separator) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (matchingContext.determineRemainingPath) {
|
if (matchingContext.determineRemainingPath) {
|
||||||
matchingContext.remainingPathIndex = matchingContext.candidateLength;
|
matchingContext.remainingPathIndex = matchingContext.pathLength;
|
||||||
}
|
}
|
||||||
if (matchingContext.extractingVariables) {
|
if (matchingContext.extractingVariables) {
|
||||||
matchingContext.set(variableName, decode(new String(matchingContext.candidate, candidateIndex,
|
// Collect the parameters from all the remaining segments
|
||||||
matchingContext.candidateLength - candidateIndex)));
|
MultiValueMap<String,String> parametersCollector = null;
|
||||||
|
for (int i = pathIndex; i < matchingContext.pathLength; i++) {
|
||||||
|
Element element = matchingContext.pathElements.get(i);
|
||||||
|
if (element instanceof Segment) {
|
||||||
|
MultiValueMap<String, String> 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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String pathToString(int fromSegment, List<Element> 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
|
@Override
|
||||||
public int getNormalizedLength() {
|
public int getNormalizedLength() {
|
||||||
return 1;
|
return 1;
|
||||||
|
@ -82,4 +116,8 @@ class CaptureTheRestPathElement extends PathElement {
|
||||||
return "CaptureTheRest(/{*" + this.variableName + "})";
|
return "CaptureTheRest(/{*" + this.variableName + "})";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public char[] getChars() {
|
||||||
|
return ("/{*"+this.variableName+"}").toCharArray();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ import java.util.regex.Pattern;
|
||||||
|
|
||||||
import org.springframework.lang.Nullable;
|
import org.springframework.lang.Nullable;
|
||||||
import org.springframework.web.util.UriUtils;
|
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
|
* 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
|
@Override
|
||||||
public boolean matches(int candidateIndex, PathPattern.MatchingContext matchingContext) {
|
public boolean matches(int pathIndex, PathPattern.MatchingContext matchingContext) {
|
||||||
int nextPos = matchingContext.scanAhead(candidateIndex);
|
if (pathIndex >= matchingContext.pathLength) {
|
||||||
// There must be at least one character to capture:
|
// no more path left to match this element
|
||||||
if (nextPos == candidateIndex) {
|
return false;
|
||||||
|
}
|
||||||
|
String candidateCapture = matchingContext.pathElementValue(pathIndex);
|
||||||
|
if (candidateCapture.length() == 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
String substringForDecoding = null;
|
|
||||||
CharSequence candidateCapture = null;
|
|
||||||
if (this.constraintPattern != null) {
|
if (this.constraintPattern != null) {
|
||||||
// TODO possible optimization - only regex match if rest of pattern matches? Benefit likely to vary pattern to pattern
|
// 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);
|
Matcher matcher = constraintPattern.matcher(candidateCapture);
|
||||||
if (matcher.groupCount() != 0) {
|
if (matcher.groupCount() != 0) {
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
|
@ -101,34 +96,33 @@ class CaptureVariablePathElement extends PathElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean match = false;
|
boolean match = false;
|
||||||
if (this.next == null) {
|
pathIndex++;
|
||||||
if (matchingContext.determineRemainingPath && nextPos > candidateIndex) {
|
if (isNoMorePattern()) {
|
||||||
matchingContext.remainingPathIndex = nextPos;
|
if (matchingContext.determineRemainingPath) {
|
||||||
|
matchingContext.remainingPathIndex = pathIndex;
|
||||||
match = true;
|
match = true;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Needs to be at least one character #SPR15264
|
// Needs to be at least one character #SPR15264
|
||||||
match = (nextPos == matchingContext.candidateLength && nextPos > candidateIndex);
|
match = (pathIndex == matchingContext.pathLength);
|
||||||
if (!match && matchingContext.isAllowOptionalTrailingSlash()) {
|
if (!match && matchingContext.isAllowOptionalTrailingSlash()) {
|
||||||
match = (nextPos > candidateIndex) &&
|
match = //(nextPos > candidateIndex) &&
|
||||||
(nextPos + 1) == matchingContext.candidateLength &&
|
(pathIndex + 1) == matchingContext.pathLength &&
|
||||||
matchingContext.candidate[nextPos] == separator;
|
matchingContext.isSeparator(pathIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (matchingContext.isMatchStartMatching && nextPos == matchingContext.candidateLength) {
|
if (matchingContext.isMatchStartMatching && pathIndex == matchingContext.pathLength) {
|
||||||
match = true; // no more data but matches up to this point
|
match = true; // no more data but matches up to this point
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
match = this.next.matches(nextPos, matchingContext);
|
match = this.next.matches(pathIndex, matchingContext);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (match && matchingContext.extractingVariables) {
|
if (match && matchingContext.extractingVariables) {
|
||||||
matchingContext.set(this.variableName,
|
matchingContext.set(this.variableName, candidateCapture, ((Segment)matchingContext.pathElements.get(pathIndex-1)).parameters());
|
||||||
candidateCapture != null ? candidateCapture.toString():
|
|
||||||
decode(new String(matchingContext.candidate, candidateIndex, nextPos - candidateIndex)));
|
|
||||||
}
|
}
|
||||||
return match;
|
return match;
|
||||||
}
|
}
|
||||||
|
@ -163,4 +157,15 @@ class CaptureVariablePathElement extends PathElement {
|
||||||
(this.constraintPattern != null ? ":" + this.constraintPattern.pattern() : "") + "})";
|
(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();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,8 @@ import org.springframework.web.util.pattern.PatternParseException.PatternMessage
|
||||||
*/
|
*/
|
||||||
class InternalPathPatternParser {
|
class InternalPathPatternParser {
|
||||||
|
|
||||||
|
private PathPatternParser parser;
|
||||||
|
|
||||||
// The expected path separator to split path elements during parsing
|
// The expected path separator to split path elements during parsing
|
||||||
char separator = PathPatternParser.DEFAULT_SEPARATOR;
|
char separator = PathPatternParser.DEFAULT_SEPARATOR;
|
||||||
|
|
||||||
|
@ -99,6 +101,9 @@ class InternalPathPatternParser {
|
||||||
this.separator = separator;
|
this.separator = separator;
|
||||||
this.caseSensitive = caseSensitive;
|
this.caseSensitive = caseSensitive;
|
||||||
this.matchOptionalTrailingSlash = matchOptionalTrailingSlash;
|
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
|
* @throws PatternParseException in case of parse errors
|
||||||
*/
|
*/
|
||||||
public PathPattern parse(String pathPattern) throws PatternParseException {
|
public PathPattern parse(String pathPattern) throws PatternParseException {
|
||||||
|
if (pathPattern == null) {
|
||||||
|
pathPattern = "";
|
||||||
|
}
|
||||||
this.pathPatternData = pathPattern.toCharArray();
|
this.pathPatternData = pathPattern.toCharArray();
|
||||||
this.pathPatternLength = pathPatternData.length;
|
this.pathPatternLength = pathPatternData.length;
|
||||||
this.headPE = null;
|
this.headPE = null;
|
||||||
|
@ -205,7 +213,7 @@ class InternalPathPatternParser {
|
||||||
pushPathElement(createPathElement());
|
pushPathElement(createPathElement());
|
||||||
}
|
}
|
||||||
return new PathPattern(
|
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();
|
resetPathElementState();
|
||||||
}
|
}
|
||||||
|
|
||||||
private char[] getPathElementText(boolean encodeElement) {
|
private char[] getPathElementText() {
|
||||||
char[] pathElementText = new char[this.pos - this.pathElementStart];
|
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,
|
System.arraycopy(this.pathPatternData, this.pathElementStart, pathElementText, 0,
|
||||||
this.pos - this.pathElementStart);
|
this.pos - this.pathElementStart);
|
||||||
}
|
|
||||||
return pathElementText;
|
return pathElementText;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -342,12 +343,12 @@ class InternalPathPatternParser {
|
||||||
this.pathPatternData[this.pos - 1] == '}') {
|
this.pathPatternData[this.pos - 1] == '}') {
|
||||||
if (this.isCaptureTheRestVariable) {
|
if (this.isCaptureTheRestVariable) {
|
||||||
// It is {*....}
|
// It is {*....}
|
||||||
newPE = new CaptureTheRestPathElement(pathElementStart, getPathElementText(false), separator);
|
newPE = new CaptureTheRestPathElement(pathElementStart, getPathElementText(), separator);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// It is a full capture of this element (possibly with constraint), for example: /foo/{abc}/
|
// It is a full capture of this element (possibly with constraint), for example: /foo/{abc}/
|
||||||
try {
|
try {
|
||||||
newPE = new CaptureVariablePathElement(this.pathElementStart, getPathElementText(false),
|
newPE = new CaptureVariablePathElement(this.pathElementStart, getPathElementText(),
|
||||||
this.caseSensitive, this.separator);
|
this.caseSensitive, this.separator);
|
||||||
}
|
}
|
||||||
catch (PatternSyntaxException pse) {
|
catch (PatternSyntaxException pse) {
|
||||||
|
@ -365,7 +366,7 @@ class InternalPathPatternParser {
|
||||||
PatternMessage.CAPTURE_ALL_IS_STANDALONE_CONSTRUCT);
|
PatternMessage.CAPTURE_ALL_IS_STANDALONE_CONSTRUCT);
|
||||||
}
|
}
|
||||||
RegexPathElement newRegexSection = new RegexPathElement(this.pathElementStart,
|
RegexPathElement newRegexSection = new RegexPathElement(this.pathElementStart,
|
||||||
getPathElementText(false), this.caseSensitive,
|
getPathElementText(), this.caseSensitive,
|
||||||
this.pathPatternData, this.separator);
|
this.pathPatternData, this.separator);
|
||||||
for (String variableName : newRegexSection.getVariableNames()) {
|
for (String variableName : newRegexSection.getVariableNames()) {
|
||||||
recordCapturedVariable(this.pathElementStart, variableName);
|
recordCapturedVariable(this.pathElementStart, variableName);
|
||||||
|
@ -379,16 +380,16 @@ class InternalPathPatternParser {
|
||||||
newPE = new WildcardPathElement(this.pathElementStart, this.separator);
|
newPE = new WildcardPathElement(this.pathElementStart, this.separator);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
newPE = new RegexPathElement(this.pathElementStart, getPathElementText(false),
|
newPE = new RegexPathElement(this.pathElementStart, getPathElementText(),
|
||||||
this.caseSensitive, this.pathPatternData, this.separator);
|
this.caseSensitive, this.pathPatternData, this.separator);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (this.singleCharWildcardCount != 0) {
|
else if (this.singleCharWildcardCount != 0) {
|
||||||
newPE = new SingleCharWildcardedPathElement(this.pathElementStart, getPathElementText(true),
|
newPE = new SingleCharWildcardedPathElement(this.pathElementStart, getPathElementText(),
|
||||||
this.singleCharWildcardCount, this.caseSensitive, this.separator);
|
this.singleCharWildcardCount, this.caseSensitive, this.separator);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
newPE = new LiteralPathElement(this.pathElementStart, getPathElementText(true),
|
newPE = new LiteralPathElement(this.pathElementStart, getPathElementText(),
|
||||||
this.caseSensitive, this.separator);
|
this.caseSensitive, this.separator);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,8 @@
|
||||||
|
|
||||||
package org.springframework.web.util.pattern;
|
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;
|
import org.springframework.web.util.pattern.PathPattern.MatchingContext;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -49,52 +51,60 @@ class LiteralPathElement extends PathElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean matches(int candidateIndex, MatchingContext matchingContext) {
|
public boolean matches(int pathIndex, MatchingContext matchingContext) {
|
||||||
if ((candidateIndex + text.length) > matchingContext.candidateLength) {
|
if (pathIndex >= matchingContext.pathLength) {
|
||||||
return false; // not enough data, cannot be a match
|
// no more path left to match this element
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
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 (data[i] != this.text[i]) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
for (int i = 0; i < len; i++) {
|
for (int i = 0; i < len; i++) {
|
||||||
// TODO revisit performance if doing a lot of case insensitive matching
|
// 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;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.next == null) {
|
pathIndex++;
|
||||||
if (matchingContext.determineRemainingPath && nextIfExistsIsSeparator(candidateIndex, matchingContext)) {
|
if (isNoMorePattern()) {
|
||||||
matchingContext.remainingPathIndex = candidateIndex;
|
if (matchingContext.determineRemainingPath) {
|
||||||
|
matchingContext.remainingPathIndex = pathIndex;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (candidateIndex == matchingContext.candidateLength) {
|
if (pathIndex == matchingContext.pathLength) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return (matchingContext.isAllowOptionalTrailingSlash() &&
|
return (matchingContext.isAllowOptionalTrailingSlash() &&
|
||||||
(candidateIndex + 1) == matchingContext.candidateLength &&
|
(pathIndex + 1) == matchingContext.pathLength &&
|
||||||
matchingContext.candidate[candidateIndex] == separator);
|
matchingContext.isSeparator(pathIndex));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (matchingContext.isMatchStartMatching && candidateIndex == matchingContext.candidateLength) {
|
if (matchingContext.isMatchStartMatching && pathIndex == matchingContext.pathLength) {
|
||||||
return true; // no more data but everything matched so far
|
return true; // no more data but everything matched so far
|
||||||
}
|
}
|
||||||
return this.next.matches(candidateIndex, matchingContext);
|
return this.next.matches(pathIndex, matchingContext);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,4 +118,8 @@ class LiteralPathElement extends PathElement {
|
||||||
return "Literal(" + String.valueOf(this.text) + ")";
|
return "Literal(" + String.valueOf(this.text) + ")";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public char[] getChars() {
|
||||||
|
return this.text;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,13 +16,18 @@
|
||||||
|
|
||||||
package org.springframework.web.util.pattern;
|
package org.springframework.web.util.pattern;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.ConcurrentMap;
|
import java.util.concurrent.ConcurrentMap;
|
||||||
|
|
||||||
|
import org.springframework.http.server.reactive.PathContainer;
|
||||||
import org.springframework.lang.Nullable;
|
import org.springframework.lang.Nullable;
|
||||||
import org.springframework.util.PathMatcher;
|
import org.springframework.util.PathMatcher;
|
||||||
|
import org.springframework.web.util.pattern.PathPattern.PathMatchResult;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link PathMatcher} implementation for path patterns parsed
|
* {@link PathMatcher} implementation for path patterns parsed
|
||||||
|
@ -56,13 +61,13 @@ public class ParsingPathMatcher implements PathMatcher {
|
||||||
@Override
|
@Override
|
||||||
public boolean match(String pattern, String path) {
|
public boolean match(String pattern, String path) {
|
||||||
PathPattern pathPattern = getPathPattern(pattern);
|
PathPattern pathPattern = getPathPattern(pattern);
|
||||||
return pathPattern.matches(path);
|
return pathPattern.matches(PathContainer.parse(path, StandardCharsets.UTF_8));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean matchStart(String pattern, String path) {
|
public boolean matchStart(String pattern, String path) {
|
||||||
PathPattern pathPattern = getPathPattern(pattern);
|
PathPattern pathPattern = getPathPattern(pattern);
|
||||||
return pathPattern.matchStart(path);
|
return pathPattern.matchStart(PathContainer.parse(path, StandardCharsets.UTF_8));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -74,7 +79,19 @@ public class ParsingPathMatcher implements PathMatcher {
|
||||||
@Override
|
@Override
|
||||||
public Map<String, String> extractUriTemplateVariables(String pattern, String path) {
|
public Map<String, String> extractUriTemplateVariables(String pattern, String path) {
|
||||||
PathPattern pathPattern = getPathPattern(pattern);
|
PathPattern pathPattern = getPathPattern(pattern);
|
||||||
return pathPattern.matchAndExtract(path);
|
Map<String, PathMatchResult> results = pathPattern.matchAndExtract(PathContainer.parse(path, StandardCharsets.UTF_8));
|
||||||
|
// Collapse PathMatchResults to simple value results (path parameters are lost in this translation)
|
||||||
|
Map<String, String> boundVariables = null;
|
||||||
|
if (results.size() == 0) {
|
||||||
|
boundVariables = Collections.emptyMap();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
boundVariables = new LinkedHashMap<>();
|
||||||
|
for (Map.Entry<String,PathMatchResult> entries: results.entrySet()) {
|
||||||
|
boundVariables.put(entries.getKey(), entries.getValue().value());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return boundVariables;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -85,7 +102,7 @@ public class ParsingPathMatcher implements PathMatcher {
|
||||||
@Override
|
@Override
|
||||||
public String combine(String pattern1, String pattern2) {
|
public String combine(String pattern1, String pattern2) {
|
||||||
PathPattern pathPattern = getPathPattern(pattern1);
|
PathPattern pathPattern = getPathPattern(pattern1);
|
||||||
return pathPattern.combine(pattern2);
|
return pathPattern.combine(getPathPattern(pattern2)).getPatternString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private PathPattern getPathPattern(String pattern) {
|
private PathPattern getPathPattern(String pattern) {
|
||||||
|
|
|
@ -19,6 +19,8 @@ package org.springframework.web.util.pattern;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
import org.springframework.lang.Nullable;
|
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.UriUtils;
|
||||||
import org.springframework.web.util.pattern.PathPattern.MatchingContext;
|
import org.springframework.web.util.pattern.PathPattern.MatchingContext;
|
||||||
|
|
||||||
|
@ -35,6 +37,7 @@ abstract class PathElement {
|
||||||
|
|
||||||
protected static final int CAPTURE_VARIABLE_WEIGHT = 1;
|
protected static final int CAPTURE_VARIABLE_WEIGHT = 1;
|
||||||
|
|
||||||
|
protected final static MultiValueMap<String,String> NO_PARAMETERS = new LinkedMultiValueMap<>();
|
||||||
|
|
||||||
// Position in the pattern where this path element starts
|
// Position in the pattern where this path element starts
|
||||||
protected final int pos;
|
protected final int pos;
|
||||||
|
@ -75,6 +78,8 @@ abstract class PathElement {
|
||||||
*/
|
*/
|
||||||
public abstract int getNormalizedLength();
|
public abstract int getNormalizedLength();
|
||||||
|
|
||||||
|
public abstract char[] getChars();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the number of variables captured by the path element.
|
* 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.
|
* @return true if the there are no more PathElements in the pattern
|
||||||
*/
|
*/
|
||||||
protected boolean nextIfExistsIsSeparator(int nextIndex, MatchingContext matchingContext) {
|
protected final boolean isNoMorePattern() {
|
||||||
return (nextIndex >= matchingContext.candidateLength ||
|
return this.next == null;
|
||||||
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
|
|
||||||
*/
|
|
||||||
protected boolean includesPercent(CharSequence chars) {
|
|
||||||
for (int i = 0, max = chars.length(); i < max; i++) {
|
|
||||||
if (chars.charAt(i) == '%') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,11 +16,18 @@
|
||||||
|
|
||||||
package org.springframework.web.util.pattern;
|
package org.springframework.web.util.pattern;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
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.lang.Nullable;
|
||||||
|
import org.springframework.util.MultiValueMap;
|
||||||
import org.springframework.util.PathMatcher;
|
import org.springframework.util.PathMatcher;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
@ -63,9 +70,14 @@ import org.springframework.util.StringUtils;
|
||||||
*/
|
*/
|
||||||
public class PathPattern implements Comparable<PathPattern> {
|
public class PathPattern implements Comparable<PathPattern> {
|
||||||
|
|
||||||
|
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 */
|
/** First path element in the parsed chain of path elements for this pattern */
|
||||||
@Nullable
|
@Nullable
|
||||||
private PathElement head;
|
private final PathElement head;
|
||||||
|
|
||||||
/** The text of the parsed pattern */
|
/** The text of the parsed pattern */
|
||||||
private String patternString;
|
private String patternString;
|
||||||
|
@ -109,10 +121,10 @@ public class PathPattern implements Comparable<PathPattern> {
|
||||||
private boolean catchAll = false;
|
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) {
|
boolean allowOptionalTrailingSlash) {
|
||||||
|
|
||||||
this.patternString = patternText;
|
this.patternString = patternText;
|
||||||
|
this.parser = parser;
|
||||||
this.head = head;
|
this.head = head;
|
||||||
this.separator = separator;
|
this.separator = separator;
|
||||||
this.caseSensitive = caseSensitive;
|
this.caseSensitive = caseSensitive;
|
||||||
|
@ -137,54 +149,48 @@ public class PathPattern implements Comparable<PathPattern> {
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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() {
|
public String getPatternString() {
|
||||||
return this.patternString;
|
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
|
* @return true if the path matches this pattern
|
||||||
*/
|
*/
|
||||||
public boolean matches(String path) {
|
public boolean matches(PathContainer pathContainer) {
|
||||||
if (this.head == null) {
|
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) {
|
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 {
|
else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MatchingContext matchingContext = new MatchingContext(path, false);
|
MatchingContext matchingContext = new MatchingContext(pathContainer, false);
|
||||||
return this.head.matches(0, matchingContext);
|
return this.head.matches(0, matchingContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For a given path return the remaining piece that is not covered by this PathPattern.
|
* 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,
|
* @return a {@link PathRemainingMatchInfo} describing the match result,
|
||||||
* or {@code null} if the path does not match this pattern
|
* or {@code null} if the path does not match this pattern
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
public PathRemainingMatchInfo getPathRemaining(String path) {
|
public PathRemainingMatchInfo getPathRemaining(@Nullable PathContainer pathContainer) {
|
||||||
if (this.head == null) {
|
if (this.head == null) {
|
||||||
return new PathRemainingMatchInfo(path);
|
return new PathRemainingMatchInfo(pathContainer);
|
||||||
}
|
}
|
||||||
else if (!StringUtils.hasLength(path)) {
|
else if (!hasLength(pathContainer)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
MatchingContext matchingContext = new MatchingContext(path, true);
|
MatchingContext matchingContext = new MatchingContext(pathContainer, true);
|
||||||
matchingContext.setMatchAllowExtraPath();
|
matchingContext.setMatchAllowExtraPath();
|
||||||
boolean matches = this.head.matches(0, matchingContext);
|
boolean matches = this.head.matches(0, matchingContext);
|
||||||
if (!matches) {
|
if (!matches) {
|
||||||
|
@ -192,11 +198,11 @@ public class PathPattern implements Comparable<PathPattern> {
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
PathRemainingMatchInfo info;
|
PathRemainingMatchInfo info;
|
||||||
if (matchingContext.remainingPathIndex == path.length()) {
|
if (matchingContext.remainingPathIndex == pathContainer.elements().size()) {
|
||||||
info = new PathRemainingMatchInfo("", matchingContext.getExtractedVariables());
|
info = new PathRemainingMatchInfo(EMPTY_PATH, matchingContext.getExtractedVariables());
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
info = new PathRemainingMatchInfo(path.substring(matchingContext.remainingPathIndex),
|
info = new PathRemainingMatchInfo(PathContainer.subPath(pathContainer, matchingContext.remainingPathIndex),
|
||||||
matchingContext.getExtractedVariables());
|
matchingContext.getExtractedVariables());
|
||||||
}
|
}
|
||||||
return info;
|
return info;
|
||||||
|
@ -204,36 +210,36 @@ public class PathPattern implements Comparable<PathPattern> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @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
|
* @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) {
|
if (this.head == null) {
|
||||||
return !StringUtils.hasLength(path);
|
return !hasLength(pathContainer);
|
||||||
}
|
}
|
||||||
else if (!StringUtils.hasLength(path)) {
|
else if (!hasLength(pathContainer)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
MatchingContext matchingContext = new MatchingContext(path, false);
|
MatchingContext matchingContext = new MatchingContext(pathContainer, false);
|
||||||
matchingContext.setMatchStartMatching(true);
|
matchingContext.setMatchStartMatching(true);
|
||||||
return this.head.matches(0, matchingContext);
|
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.
|
* @return a map of extracted variables - an empty map if no variables extracted.
|
||||||
* @throws IllegalStateException if the path does not match the pattern
|
* @throws IllegalStateException if the path does not match the pattern
|
||||||
*/
|
*/
|
||||||
public Map<String, String> matchAndExtract(String path) {
|
public Map<String, PathMatchResult> matchAndExtract(PathContainer pathContainer) {
|
||||||
MatchingContext matchingContext = new MatchingContext(path, true);
|
MatchingContext matchingContext = new MatchingContext(pathContainer, true);
|
||||||
if (this.head != null && this.head.matches(0, matchingContext)) {
|
if (this.head != null && this.head.matches(0, matchingContext)) {
|
||||||
return matchingContext.getExtractedVariables();
|
return matchingContext.getExtractedVariables();
|
||||||
}
|
}
|
||||||
else if (!StringUtils.hasLength(path)) {
|
else if (!hasLength(pathContainer)) {
|
||||||
return Collections.emptyMap();
|
return Collections.emptyMap();
|
||||||
}
|
}
|
||||||
else {
|
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,6 +373,158 @@ public class PathPattern implements Comparable<PathPattern> {
|
||||||
return (lenDifference < 0) ? +1 : (lenDifference == 0 ? 0 : -1);
|
return (lenDifference < 0) ? +1 : (lenDifference == 0 ? 0 : -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combine this pattern with another. Currently does not produce a new PathPattern, just produces a new string.
|
||||||
|
*/
|
||||||
|
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.patternString)) {
|
||||||
|
return parser.parse("");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return pattern2string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (!StringUtils.hasLength(pattern2string.patternString)) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// /* + /hotel => /hotel
|
||||||
|
// /*.* + /*.html => /*.html
|
||||||
|
// However:
|
||||||
|
// /usr + /user => /usr/user
|
||||||
|
// /{foo} + /bar => /{foo}/bar
|
||||||
|
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 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 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
|
||||||
|
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 parser.parse(file2 + (firstExtensionWild ? secondExtension : firstExtension));
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean equals(Object other) {
|
||||||
|
if (!(other instanceof PathPattern)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
PathPattern otherPattern = (PathPattern) other;
|
||||||
|
return (this.patternString.equals(otherPattern.getPatternString()) &&
|
||||||
|
this.separator == otherPattern.getSeparator() &&
|
||||||
|
this.caseSensitive == otherPattern.caseSensitive);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int hashCode() {
|
||||||
|
return (this.patternString.hashCode() + this.separator) * 17 + (this.caseSensitive ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toString() {
|
||||||
|
return this.patternString;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<String,String> parameters;
|
||||||
|
|
||||||
|
public PathMatchResult(String key, String value, MultiValueMap<String, String> parameters) {
|
||||||
|
this.key = key;
|
||||||
|
this.value = value;
|
||||||
|
this.parameters = parameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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<String,String> parameters() {
|
||||||
|
return this.parameters;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A holder for the result of a {@link PathPattern#getPathRemaining(String)} call. Holds
|
||||||
|
* information on the path left after the first part has successfully matched a pattern
|
||||||
|
* and any variables bound in that first part that matched.
|
||||||
|
*/
|
||||||
|
public static class PathRemainingMatchInfo {
|
||||||
|
|
||||||
|
private final PathContainer pathRemaining;
|
||||||
|
|
||||||
|
private final Map<String, PathMatchResult> matchingVariables;
|
||||||
|
|
||||||
|
PathRemainingMatchInfo(@Nullable PathContainer pathRemaining) {
|
||||||
|
this(pathRemaining, Collections.emptyMap());
|
||||||
|
}
|
||||||
|
|
||||||
|
PathRemainingMatchInfo(@Nullable PathContainer pathRemaining, Map<String, PathMatchResult> matchingVariables) {
|
||||||
|
this.pathRemaining = pathRemaining;
|
||||||
|
this.matchingVariables = matchingVariables;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the part of a path that was not matched by a pattern.
|
||||||
|
*/
|
||||||
|
public String getPathRemaining() {
|
||||||
|
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<String, PathMatchResult> getMatchingVariables() {
|
||||||
|
return this.matchingVariables;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int getScore() {
|
int getScore() {
|
||||||
return this.score;
|
return this.score;
|
||||||
}
|
}
|
||||||
|
@ -393,59 +551,115 @@ public class PathPattern implements Comparable<PathPattern> {
|
||||||
return this.capturedVariableCount;
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Combine this pattern with another. Currently does not produce a new PathPattern, just produces a new string.
|
* @return string form of the pattern built from walking the path element chain
|
||||||
*/
|
*/
|
||||||
public String combine(String pattern2string) {
|
String computePatternString() {
|
||||||
// If one of them is empty the result is the other. If both empty the result is ""
|
StringBuilder buf = new StringBuilder();
|
||||||
if (!StringUtils.hasLength(this.patternString)) {
|
PathElement pe = this.head;
|
||||||
if (!StringUtils.hasLength(pattern2string)) {
|
while (pe != null) {
|
||||||
return "";
|
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
|
||||||
|
* candidate currently being considered for a match but also some accumulators for
|
||||||
|
* extracted variables.
|
||||||
|
*/
|
||||||
|
class MatchingContext {
|
||||||
|
|
||||||
|
final PathContainer candidate;
|
||||||
|
|
||||||
|
final List<Element> pathElements;
|
||||||
|
|
||||||
|
final int pathLength;
|
||||||
|
|
||||||
|
boolean isMatchStartMatching = false;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private Map<String, PathMatchResult> extractedVariables;
|
||||||
|
|
||||||
|
boolean extractingVariables;
|
||||||
|
|
||||||
|
boolean determineRemainingPath = 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(PathContainer pathContainer, boolean extractVariables) {
|
||||||
|
candidate = pathContainer;
|
||||||
|
pathElements = pathContainer.elements();
|
||||||
|
pathLength = pathElements.size();
|
||||||
|
this.extractingVariables = extractVariables;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMatchAllowExtraPath() {
|
||||||
|
determineRemainingPath = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAllowOptionalTrailingSlash() {
|
||||||
|
return allowOptionalTrailingSlash;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMatchStartMatching(boolean b) {
|
||||||
|
isMatchStartMatching = b;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void set(String key, String value, MultiValueMap<String,String> parameters) {
|
||||||
|
if (this.extractedVariables == null) {
|
||||||
|
extractedVariables = new HashMap<>();
|
||||||
|
}
|
||||||
|
extractedVariables.put(key, new PathMatchResult(key, value, parameters));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, PathMatchResult> getExtractedVariables() {
|
||||||
|
if (this.extractedVariables == null) {
|
||||||
|
return Collections.emptyMap();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return pattern2string;
|
return this.extractedVariables;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (!StringUtils.hasLength(pattern2string)) {
|
|
||||||
return this.patternString;
|
|
||||||
}
|
|
||||||
|
|
||||||
// /* + /hotel => /hotel
|
/**
|
||||||
// /*.* + /*.html => /*.html
|
* @param pathIndex possible index of a separator
|
||||||
// However:
|
* @return true if element at specified index is a separator
|
||||||
// /usr + /user => /usr/user
|
*/
|
||||||
// /{foo} + /bar => /{foo}/bar
|
boolean isSeparator(int pathIndex) {
|
||||||
if (!this.patternString.equals(pattern2string) &&this. capturedVariableCount == 0 && matches(pattern2string)) {
|
return pathElements.get(pathIndex) instanceof Separator;
|
||||||
return pattern2string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// /hotels/* + /booking => /hotels/booking
|
/**
|
||||||
// /hotels/* + booking => /hotels/booking
|
* @param pathIndex path element index
|
||||||
if (this.endsWithSeparatorWildcard) {
|
* @return decoded value of the specified element
|
||||||
return concat(this.patternString.substring(0, this.patternString.length() - 2), pattern2string);
|
*/
|
||||||
|
String pathElementValue(int pathIndex) {
|
||||||
|
Element element = (pathIndex < pathLength) ? pathElements.get(pathIndex) : null;
|
||||||
|
if (element instanceof Segment) {
|
||||||
|
return ((Segment)element).valueDecoded();
|
||||||
}
|
}
|
||||||
|
return "";
|
||||||
// /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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// /*.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));
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -470,149 +684,12 @@ public class PathPattern implements Comparable<PathPattern> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public boolean equals(Object other) {
|
|
||||||
if (!(other instanceof PathPattern)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
PathPattern otherPattern = (PathPattern) other;
|
|
||||||
return (this.patternString.equals(otherPattern.getPatternString()) &&
|
|
||||||
this.separator == otherPattern.getSeparator() &&
|
|
||||||
this.caseSensitive == otherPattern.caseSensitive);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int hashCode() {
|
|
||||||
return (this.patternString.hashCode() + this.separator) * 17 + (this.caseSensitive ? 1 : 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String toString() {
|
|
||||||
return this.patternString;
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A holder for the result of a {@link PathPattern#getPathRemaining(String)} call. Holds
|
* @param container a path container
|
||||||
* information on the path left after the first part has successfully matched a pattern
|
* @return true if the container is not null and has more than zero elements
|
||||||
* and any variables bound in that first part that matched.
|
|
||||||
*/
|
*/
|
||||||
public static class PathRemainingMatchInfo {
|
private boolean hasLength(PathContainer container) {
|
||||||
|
return container != null && container.elements().size() > 0;
|
||||||
private final String pathRemaining;
|
|
||||||
|
|
||||||
private final Map<String, String> matchingVariables;
|
|
||||||
|
|
||||||
PathRemainingMatchInfo(String pathRemaining) {
|
|
||||||
this(pathRemaining, Collections.emptyMap());
|
|
||||||
}
|
|
||||||
|
|
||||||
PathRemainingMatchInfo(String pathRemaining, Map<String, String> matchingVariables) {
|
|
||||||
this.pathRemaining = pathRemaining;
|
|
||||||
this.matchingVariables = matchingVariables;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the part of a path that was not matched by a pattern.
|
|
||||||
*/
|
|
||||||
public String getPathRemaining() {
|
|
||||||
return this.pathRemaining;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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<String, String> getMatchingVariables() {
|
|
||||||
return this.matchingVariables;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encapsulates context when attempting a match. Includes some fixed state like the
|
|
||||||
* candidate currently being considered for a match but also some accumulators for
|
|
||||||
* extracted variables.
|
|
||||||
*/
|
|
||||||
class MatchingContext {
|
|
||||||
|
|
||||||
// The candidate path to attempt a match against
|
|
||||||
char[] candidate;
|
|
||||||
|
|
||||||
// The length of the candidate path
|
|
||||||
int candidateLength;
|
|
||||||
|
|
||||||
boolean isMatchStartMatching = false;
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private Map<String, String> extractedVariables;
|
|
||||||
|
|
||||||
boolean extractingVariables;
|
|
||||||
|
|
||||||
boolean determineRemainingPath = 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();
|
|
||||||
candidateLength = candidate.length;
|
|
||||||
this.extractingVariables = extractVariables;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMatchAllowExtraPath() {
|
|
||||||
determineRemainingPath = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isAllowOptionalTrailingSlash() {
|
|
||||||
return allowOptionalTrailingSlash;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMatchStartMatching(boolean b) {
|
|
||||||
isMatchStartMatching = b;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void set(String key, String value) {
|
|
||||||
if (this.extractedVariables == null) {
|
|
||||||
extractedVariables = new HashMap<>();
|
|
||||||
}
|
|
||||||
extractedVariables.put(key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Map<String, String> getExtractedVariables() {
|
|
||||||
if (this.extractedVariables == null) {
|
|
||||||
return Collections.emptyMap();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return this.extractedVariables;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
*/
|
|
||||||
public int scanAhead(int pos) {
|
|
||||||
while (pos < candidateLength) {
|
|
||||||
if (candidate[pos] == separator) {
|
|
||||||
return pos;
|
|
||||||
}
|
|
||||||
pos++;
|
|
||||||
}
|
|
||||||
return candidateLength;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,8 +22,8 @@ import java.util.List;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import org.springframework.web.util.UriUtils;
|
|
||||||
import org.springframework.web.util.pattern.PathPattern.MatchingContext;
|
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.
|
* 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) {
|
public Pattern buildPattern(char[] regex, char[] completePattern) {
|
||||||
StringBuilder patternBuilder = new StringBuilder();
|
StringBuilder patternBuilder = new StringBuilder();
|
||||||
String text = new String(regex);
|
String text = new String(regex);
|
||||||
StringBuilder encodedRegexBuilder = new StringBuilder();
|
|
||||||
Matcher matcher = GLOB_PATTERN.matcher(text);
|
Matcher matcher = GLOB_PATTERN.matcher(text);
|
||||||
int end = 0;
|
int end = 0;
|
||||||
|
|
||||||
while (matcher.find()) {
|
while (matcher.find()) {
|
||||||
patternBuilder.append(quote(text, end, matcher.start(), encodedRegexBuilder));
|
patternBuilder.append(quote(text, end, matcher.start()));
|
||||||
String match = matcher.group();
|
String match = matcher.group();
|
||||||
if ("?".equals(match)) {
|
if ("?".equals(match)) {
|
||||||
patternBuilder.append('.');
|
patternBuilder.append('.');
|
||||||
encodedRegexBuilder.append('?');
|
|
||||||
}
|
}
|
||||||
else if ("*".equals(match)) {
|
else if ("*".equals(match)) {
|
||||||
patternBuilder.append(".*");
|
patternBuilder.append(".*");
|
||||||
encodedRegexBuilder.append('*');
|
|
||||||
int pos = matcher.start();
|
int pos = matcher.start();
|
||||||
if (pos < 1 || text.charAt(pos-1) != '.') {
|
if (pos < 1 || text.charAt(pos-1) != '.') {
|
||||||
// To be compatible with the AntPathMatcher comparator,
|
// To be compatible with the AntPathMatcher comparator,
|
||||||
|
@ -85,7 +82,6 @@ class RegexPathElement extends PathElement {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (match.startsWith("{") && match.endsWith("}")) {
|
else if (match.startsWith("{") && match.endsWith("}")) {
|
||||||
encodedRegexBuilder.append(match);
|
|
||||||
int colonIdx = match.indexOf(':');
|
int colonIdx = match.indexOf(':');
|
||||||
if (colonIdx == -1) {
|
if (colonIdx == -1) {
|
||||||
patternBuilder.append(DEFAULT_VARIABLE_PATTERN);
|
patternBuilder.append(DEFAULT_VARIABLE_PATTERN);
|
||||||
|
@ -112,8 +108,7 @@ class RegexPathElement extends PathElement {
|
||||||
end = matcher.end();
|
end = matcher.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
patternBuilder.append(quote(text, end, text.length(), encodedRegexBuilder));
|
patternBuilder.append(quote(text, end, text.length()));
|
||||||
this.regex = encodedRegexBuilder.toString().toCharArray();
|
|
||||||
if (this.caseSensitive) {
|
if (this.caseSensitive) {
|
||||||
return Pattern.compile(patternBuilder.toString());
|
return Pattern.compile(patternBuilder.toString());
|
||||||
}
|
}
|
||||||
|
@ -126,54 +121,43 @@ class RegexPathElement extends PathElement {
|
||||||
return this.variableNames;
|
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) {
|
if (start == end) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
String substring = s.substring(start, end);
|
return Pattern.quote(s.substring(start, end));
|
||||||
String encodedSubString = UriUtils.encodePath(substring, StandardCharsets.UTF_8);
|
|
||||||
encodedRegexBuilder.append(encodedSubString);
|
|
||||||
return Pattern.quote(substring);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean matches(int candidateIndex, MatchingContext matchingContext) {
|
public boolean matches(int pathIndex, MatchingContext matchingContext) {
|
||||||
int pos = matchingContext.scanAhead(candidateIndex);
|
String textToMatch = matchingContext.pathElementValue(pathIndex);
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
Matcher matcher = this.pattern.matcher(textToMatch);
|
Matcher matcher = this.pattern.matcher(textToMatch);
|
||||||
boolean matches = matcher.matches();
|
boolean matches = matcher.matches();
|
||||||
|
|
||||||
if (matches) {
|
if (matches) {
|
||||||
if (this.next == null) {
|
if (isNoMorePattern()) {
|
||||||
if (matchingContext.determineRemainingPath &&
|
if (matchingContext.determineRemainingPath &&
|
||||||
((this.variableNames.size() == 0) ? true : pos > candidateIndex)) {
|
((this.variableNames.size() == 0) ? true : textToMatch.length() > 0)) {
|
||||||
matchingContext.remainingPathIndex = pos;
|
matchingContext.remainingPathIndex = pathIndex + 1;
|
||||||
matches = true;
|
matches = true;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// No more pattern, is there more data?
|
// No more pattern, is there more data?
|
||||||
// If pattern is capturing variables there must be some actual data to bind to them
|
// If pattern is capturing variables there must be some actual data to bind to them
|
||||||
matches = (pos == matchingContext.candidateLength &&
|
matches = (pathIndex + 1) >= matchingContext.pathLength &&
|
||||||
((this.variableNames.size() == 0) ? true : pos > candidateIndex));
|
((this.variableNames.size() == 0) ? true : textToMatch.length() > 0);
|
||||||
if (!matches && matchingContext.isAllowOptionalTrailingSlash()) {
|
if (!matches && matchingContext.isAllowOptionalTrailingSlash()) {
|
||||||
matches = ((this.variableNames.size() == 0) ? true : pos > candidateIndex) &&
|
matches = ((this.variableNames.size() == 0) ? true : textToMatch.length() > 0) &&
|
||||||
(pos + 1) == matchingContext.candidateLength &&
|
(pathIndex + 2) >= matchingContext.pathLength &&
|
||||||
matchingContext.candidate[pos] == separator;
|
matchingContext.isSeparator(pathIndex + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
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
|
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,7 +172,10 @@ class RegexPathElement extends PathElement {
|
||||||
for (int i = 1; i <= matcher.groupCount(); i++) {
|
for (int i = 1; i <= matcher.groupCount(); i++) {
|
||||||
String name = this.variableNames.get(i - 1);
|
String name = this.variableNames.get(i - 1);
|
||||||
String value = matcher.group(i);
|
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;
|
return matches;
|
||||||
|
@ -222,4 +209,8 @@ class RegexPathElement extends PathElement {
|
||||||
return "Regex(" + String.valueOf(this.regex) + ")";
|
return "Regex(" + String.valueOf(this.regex) + ")";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public char[] getChars() {
|
||||||
|
return this.regex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,28 +38,26 @@ class SeparatorPathElement extends PathElement {
|
||||||
* must be the separator.
|
* must be the separator.
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public boolean matches(int candidateIndex, MatchingContext matchingContext) {
|
public boolean matches(int pathIndex, MatchingContext matchingContext) {
|
||||||
boolean matched = false;
|
if (pathIndex < matchingContext.pathLength && matchingContext.isSeparator(pathIndex)) {
|
||||||
if (candidateIndex < matchingContext.candidateLength &&
|
if (isNoMorePattern()) {
|
||||||
matchingContext.candidate[candidateIndex] == separator) {
|
|
||||||
if (this.next == null) {
|
|
||||||
if (matchingContext.determineRemainingPath) {
|
if (matchingContext.determineRemainingPath) {
|
||||||
matchingContext.remainingPathIndex = candidateIndex + 1;
|
matchingContext.remainingPathIndex = pathIndex + 1;
|
||||||
matched = true;
|
return true;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
matched = (candidateIndex + 1 == matchingContext.candidateLength);
|
return (pathIndex + 1 == matchingContext.pathLength);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
candidateIndex++;
|
pathIndex++;
|
||||||
if (matchingContext.isMatchStartMatching && candidateIndex == matchingContext.candidateLength) {
|
if (matchingContext.isMatchStartMatching && pathIndex == matchingContext.pathLength) {
|
||||||
return true; // no more data but matches up to this point
|
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
|
@Override
|
||||||
|
@ -67,9 +65,12 @@ class SeparatorPathElement extends PathElement {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "Separator(" + this.separator + ")";
|
return "Separator(" + this.separator + ")";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public char[] getChars() {
|
||||||
|
return new char[] {this.separator};
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,8 @@
|
||||||
|
|
||||||
package org.springframework.web.util.pattern;
|
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;
|
import org.springframework.web.util.pattern.PathPattern.MatchingContext;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -56,68 +58,63 @@ class SingleCharWildcardedPathElement extends PathElement {
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean matches(int candidateIndex, MatchingContext matchingContext) {
|
public boolean matches(int pathIndex, MatchingContext matchingContext) {
|
||||||
if (matchingContext.candidateLength < (candidateIndex + len)) {
|
if (pathIndex >= matchingContext.pathLength) {
|
||||||
return false; // there isn't enough data to match
|
// no more path left to match this element
|
||||||
}
|
|
||||||
|
|
||||||
char[] candidate = matchingContext.candidate;
|
|
||||||
if (this.caseSensitive) {
|
|
||||||
for (int i = 0; i <this.len; i++) {
|
|
||||||
char t = this.text[i];
|
|
||||||
if (t == '?') {
|
|
||||||
if (candidate[candidateIndex] == '%') {
|
|
||||||
// encoded value, skip next two as well!
|
|
||||||
candidateIndex += 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (candidate[candidateIndex] != t) {
|
|
||||||
// TODO unfortunate performance hit here on comparison when encoded data is the less likely case
|
|
||||||
if (i < 3 || matchingContext.candidate[candidateIndex-2] != '%' ||
|
|
||||||
Character.toUpperCase(matchingContext.candidate[candidateIndex]) != this.text[i]) {
|
|
||||||
return false;
|
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++) {
|
||||||
|
char ch = this.text[i];
|
||||||
|
if ((ch != '?') && (ch != data[i])) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
candidateIndex++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
for (int i = 0; i < this.len; i++) {
|
for (int i = 0; i < len; i++) {
|
||||||
char t = this.text[i];
|
char ch = this.text[i];
|
||||||
if (t == '?') {
|
// TODO revisit performance if doing a lot of case insensitive matching
|
||||||
if (candidate[candidateIndex] == '%') {
|
if ((ch != '?') && (ch != Character.toLowerCase(data[i]))) {
|
||||||
// encoded value, skip next two as well!
|
|
||||||
candidateIndex += 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (Character.toLowerCase(candidate[candidateIndex]) != t) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
candidateIndex++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.next == null) {
|
pathIndex++;
|
||||||
if (matchingContext.determineRemainingPath && nextIfExistsIsSeparator(candidateIndex, matchingContext)) {
|
if (isNoMorePattern()) {
|
||||||
matchingContext.remainingPathIndex = candidateIndex;
|
if (matchingContext.determineRemainingPath) {
|
||||||
|
matchingContext.remainingPathIndex = pathIndex;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (candidateIndex == matchingContext.candidateLength) {
|
if (pathIndex == matchingContext.pathLength) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return (matchingContext.isAllowOptionalTrailingSlash() &&
|
return (matchingContext.isAllowOptionalTrailingSlash() &&
|
||||||
(candidateIndex + 1) == matchingContext.candidateLength &&
|
(pathIndex + 1) == matchingContext.pathLength &&
|
||||||
matchingContext.candidate[candidateIndex] == separator);
|
matchingContext.isSeparator(pathIndex));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (matchingContext.isMatchStartMatching && candidateIndex == matchingContext.candidateLength) {
|
if (matchingContext.isMatchStartMatching && pathIndex == matchingContext.pathLength) {
|
||||||
return true; // no more data but matches up to this point
|
return true; // no more data but everything matched so far
|
||||||
}
|
}
|
||||||
return this.next.matches(candidateIndex, matchingContext);
|
return this.next.matches(pathIndex, matchingContext);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,4 +133,9 @@ class SingleCharWildcardedPathElement extends PathElement {
|
||||||
return "SingleCharWildcarded(" + String.valueOf(this.text) + ")";
|
return "SingleCharWildcarded(" + String.valueOf(this.text) + ")";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public char[] getChars() {
|
||||||
|
return this.text;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,8 @@
|
||||||
package org.springframework.web.util.pattern;
|
package org.springframework.web.util.pattern;
|
||||||
|
|
||||||
import org.springframework.web.util.pattern.PathPattern.MatchingContext;
|
import org.springframework.web.util.pattern.PathPattern.MatchingContext;
|
||||||
|
import org.springframework.http.server.reactive.PathContainer.Element;
|
||||||
|
import org.springframework.http.server.reactive.PathContainer.Segment;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A wildcard path element. In the pattern '/foo/*/goo' the * is
|
* A wildcard path element. In the pattern '/foo/*/goo' the * is
|
||||||
|
@ -39,34 +41,46 @@ class WildcardPathElement extends PathElement {
|
||||||
* candidate.
|
* candidate.
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public boolean matches(int candidateIndex, MatchingContext matchingContext) {
|
public boolean matches(int pathIndex, MatchingContext matchingContext) {
|
||||||
int nextPos = matchingContext.scanAhead(candidateIndex);
|
String segmentData = null;
|
||||||
if (this.next == null) {
|
// Assert if it exists it is a segment
|
||||||
|
if (pathIndex < matchingContext.pathLength) {
|
||||||
|
Element element = matchingContext.pathElements.get(pathIndex);
|
||||||
|
if (!(element instanceof Segment)) {
|
||||||
|
// Should not match a separator
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
segmentData = ((Segment)element).valueDecoded();
|
||||||
|
pathIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNoMorePattern()) {
|
||||||
if (matchingContext.determineRemainingPath) {
|
if (matchingContext.determineRemainingPath) {
|
||||||
matchingContext.remainingPathIndex = nextPos;
|
matchingContext.remainingPathIndex = pathIndex;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (nextPos == matchingContext.candidateLength) {
|
if (pathIndex == matchingContext.pathLength) {
|
||||||
|
// and the path data has run out too
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return (matchingContext.isAllowOptionalTrailingSlash() && // if optional slash is on...
|
return (matchingContext.isAllowOptionalTrailingSlash() && // if optional slash is on...
|
||||||
nextPos > candidateIndex && // and there is at least one character to match the *...
|
segmentData != null && segmentData.length() > 0 && // and there is at least one character to match the *...
|
||||||
(nextPos + 1) == matchingContext.candidateLength && // and the nextPos is the end of the candidate...
|
(pathIndex + 1) == matchingContext.pathLength && // and the next path element is the end of the candidate...
|
||||||
matchingContext.candidate[nextPos] == separator); // and the final character is a separator
|
matchingContext.isSeparator(pathIndex)); // and the final element is a separator
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (matchingContext.isMatchStartMatching && nextPos == matchingContext.candidateLength) {
|
if (matchingContext.isMatchStartMatching && pathIndex == matchingContext.pathLength) {
|
||||||
return true; // no more data but matches up to this point
|
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
|
// 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 false;
|
||||||
}
|
}
|
||||||
return this.next.matches(nextPos, matchingContext);
|
return this.next.matches(pathIndex, matchingContext);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,4 +104,8 @@ class WildcardPathElement extends PathElement {
|
||||||
return "Wildcard(*)";
|
return "Wildcard(*)";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public char[] getChars() {
|
||||||
|
return new char[] {'*'};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,14 +31,13 @@ class WildcardTheRestPathElement extends PathElement {
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@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 there is more data, it must start with the separator
|
||||||
if (candidateIndex < matchingContext.candidateLength &&
|
if (pathIndex < matchingContext.pathLength && !matchingContext.isSeparator(pathIndex)) {
|
||||||
matchingContext.candidate[candidateIndex] != separator) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (matchingContext.determineRemainingPath) {
|
if (matchingContext.determineRemainingPath) {
|
||||||
matchingContext.remainingPathIndex = matchingContext.candidateLength;
|
matchingContext.remainingPathIndex = matchingContext.pathLength;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -58,4 +57,8 @@ class WildcardTheRestPathElement extends PathElement {
|
||||||
return "WildcardTheRest(" + this.separator + "**)";
|
return "WildcardTheRest(" + this.separator + "**)";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public char[] getChars() {
|
||||||
|
return (this.separator+"**").toCharArray();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -22,6 +22,8 @@ import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import org.junit.Test;
|
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 org.springframework.web.util.pattern.PatternParseException.PatternMessage;
|
||||||
|
|
||||||
import static org.junit.Assert.*;
|
import static org.junit.Assert.*;
|
||||||
|
@ -35,7 +37,6 @@ public class PathPatternParserTests {
|
||||||
|
|
||||||
private PathPattern pathPattern;
|
private PathPattern pathPattern;
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void basicPatterns() {
|
public void basicPatterns() {
|
||||||
checkStructure("/");
|
checkStructure("/");
|
||||||
|
@ -76,8 +77,8 @@ public class PathPatternParserTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void captureTheRestPatterns() {
|
public void captureTheRestPatterns() {
|
||||||
checkError("/{*foobar}x{abc}", 10, PatternMessage.NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST);
|
pathPattern = parse("{*foobar}");
|
||||||
pathPattern = checkStructure("{*foobar}");
|
assertEquals("/{*foobar}", pathPattern.computePatternString());
|
||||||
assertPathElements(pathPattern, CaptureTheRestPathElement.class);
|
assertPathElements(pathPattern, CaptureTheRestPathElement.class);
|
||||||
pathPattern = checkStructure("/{*foobar}");
|
pathPattern = checkStructure("/{*foobar}");
|
||||||
assertPathElements(pathPattern, CaptureTheRestPathElement.class);
|
assertPathElements(pathPattern, CaptureTheRestPathElement.class);
|
||||||
|
@ -125,34 +126,34 @@ public class PathPatternParserTests {
|
||||||
|
|
||||||
pathPattern = checkStructure("/{var:\\\\}");
|
pathPattern = checkStructure("/{var:\\\\}");
|
||||||
assertEquals(CaptureVariablePathElement.class.getName(), pathPattern.getHeadSection().next.getClass().getName());
|
assertEquals(CaptureVariablePathElement.class.getName(), pathPattern.getHeadSection().next.getClass().getName());
|
||||||
assertTrue(pathPattern.matches("/\\"));
|
assertMatches(pathPattern,"/\\");
|
||||||
|
|
||||||
pathPattern = checkStructure("/{var:\\/}");
|
pathPattern = checkStructure("/{var:\\/}");
|
||||||
assertEquals(CaptureVariablePathElement.class.getName(), pathPattern.getHeadSection().next.getClass().getName());
|
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());
|
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());
|
assertEquals(CaptureVariablePathElement.class.getName(), pathPattern.getHeadSection().next.getClass().getName());
|
||||||
Map<String, String> result = pathPattern.matchAndExtract("/foo");
|
Map<String, PathMatchResult> result = matchAndExtract(pathPattern,"/foo");
|
||||||
assertEquals("foo", result.get("var"));
|
assertEquals("foo", result.get("var").value());
|
||||||
|
|
||||||
pathPattern = checkStructure("/{var:\\[*}", 1);
|
pathPattern = checkStructure("/{var:\\[*}");
|
||||||
assertEquals(CaptureVariablePathElement.class.getName(), pathPattern.getHeadSection().next.getClass().getName());
|
assertEquals(CaptureVariablePathElement.class.getName(), pathPattern.getHeadSection().next.getClass().getName());
|
||||||
result = pathPattern.matchAndExtract("/[[[");
|
result = matchAndExtract(pathPattern,"/[[[");
|
||||||
assertEquals("[[[", result.get("var"));
|
assertEquals("[[[", result.get("var").value());
|
||||||
|
|
||||||
pathPattern = checkStructure("/{var:[\\{]*}", 1);
|
pathPattern = checkStructure("/{var:[\\{]*}");
|
||||||
assertEquals(CaptureVariablePathElement.class.getName(), pathPattern.getHeadSection().next.getClass().getName());
|
assertEquals(CaptureVariablePathElement.class.getName(), pathPattern.getHeadSection().next.getClass().getName());
|
||||||
result = pathPattern.matchAndExtract("/{{{");
|
result = matchAndExtract(pathPattern,"/{{{");
|
||||||
assertEquals("{{{", result.get("var"));
|
assertEquals("{{{", result.get("var").value());
|
||||||
|
|
||||||
pathPattern = checkStructure("/{var:[\\}]*}", 1);
|
pathPattern = checkStructure("/{var:[\\}]*}");
|
||||||
assertEquals(CaptureVariablePathElement.class.getName(), pathPattern.getHeadSection().next.getClass().getName());
|
assertEquals(CaptureVariablePathElement.class.getName(), pathPattern.getHeadSection().next.getClass().getName());
|
||||||
result = pathPattern.matchAndExtract("/}}}");
|
result = matchAndExtract(pathPattern,"/}}}");
|
||||||
assertEquals("}}}", result.get("var"));
|
assertEquals("}}}", result.get("var").value());
|
||||||
|
|
||||||
pathPattern = checkStructure("*");
|
pathPattern = checkStructure("*");
|
||||||
assertEquals(WildcardPathElement.class.getName(), pathPattern.getHeadSection().getClass().getName());
|
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");
|
pathPattern = checkStructure("{symbolicName:[\\p{L}\\.]+}-sources-{version:[\\p{N}\\.]+}.jar");
|
||||||
assertEquals(RegexPathElement.class.getName(), pathPattern.getHeadSection().getClass().getName());
|
assertEquals(RegexPathElement.class.getName(), pathPattern.getHeadSection().getClass().getName());
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -182,63 +182,9 @@ public class PathPatternParserTests {
|
||||||
checkStructure("/{foo}/{bar}/{wibble}");
|
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
|
@Test
|
||||||
public void encodingDuringParse() throws Exception {
|
public void noEncoding() {
|
||||||
PathPattern pp;
|
// Check no encoding of expressions or constraints
|
||||||
|
|
||||||
// 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
|
|
||||||
PathPattern pp = parse("/{var:f o}");
|
PathPattern pp = parse("/{var:f o}");
|
||||||
assertEquals("Separator(/) CaptureVariable({var:f o})",pp.toChainString());
|
assertEquals("Separator(/) CaptureVariable({var:f o})",pp.toChainString());
|
||||||
|
|
||||||
|
@ -246,7 +192,7 @@ public class PathPatternParserTests {
|
||||||
assertEquals("Separator(/) Regex({var:f o}_)",pp.toChainString());
|
assertEquals("Separator(/) Regex({var:f o}_)",pp.toChainString());
|
||||||
|
|
||||||
pp = parse("{foo:f o}_ _{bar:b\\|o}");
|
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
|
@Test
|
||||||
|
@ -292,14 +238,14 @@ public class PathPatternParserTests {
|
||||||
checkError("/foobar/{abc:..}_{abc:..}", 8, PatternMessage.ILLEGAL_DOUBLE_CAPTURE);
|
checkError("/foobar/{abc:..}_{abc:..}", 8, PatternMessage.ILLEGAL_DOUBLE_CAPTURE);
|
||||||
PathPattern pp = parse("/{abc:foo(bar)}");
|
PathPattern pp = parse("/{abc:foo(bar)}");
|
||||||
try {
|
try {
|
||||||
pp.matchAndExtract("/foo");
|
pp.matchAndExtract(toPSC("/foo"));
|
||||||
fail("Should have raised exception");
|
fail("Should have raised exception");
|
||||||
}
|
}
|
||||||
catch (IllegalArgumentException iae) {
|
catch (IllegalArgumentException iae) {
|
||||||
assertEquals("No capture groups allowed in the constraint regex: foo(bar)", iae.getMessage());
|
assertEquals("No capture groups allowed in the constraint regex: foo(bar)", iae.getMessage());
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
pp.matchAndExtract("/foobar");
|
pp.matchAndExtract(toPSC("/foobar"));
|
||||||
fail("Should have raised exception");
|
fail("Should have raised exception");
|
||||||
}
|
}
|
||||||
catch (IllegalArgumentException iae) {
|
catch (IllegalArgumentException iae) {
|
||||||
|
@ -365,12 +311,12 @@ public class PathPatternParserTests {
|
||||||
public void multipleSeparatorPatterns() {
|
public void multipleSeparatorPatterns() {
|
||||||
pathPattern = checkStructure("///aaa");
|
pathPattern = checkStructure("///aaa");
|
||||||
assertEquals(6, pathPattern.getNormalizedLength());
|
assertEquals(6, pathPattern.getNormalizedLength());
|
||||||
assertPathElements(pathPattern, SeparatorPathElement.class, SeparatorPathElement.class,
|
assertPathElements(pathPattern, SeparatorPathElement.class, SeparatorPathElement.class, SeparatorPathElement.class,
|
||||||
SeparatorPathElement.class, LiteralPathElement.class);
|
LiteralPathElement.class);
|
||||||
pathPattern = checkStructure("///aaa////aaa/b");
|
pathPattern = checkStructure("///aaa////aaa/b");
|
||||||
assertEquals(15, pathPattern.getNormalizedLength());
|
assertEquals(15, pathPattern.getNormalizedLength());
|
||||||
assertPathElements(pathPattern, SeparatorPathElement.class, SeparatorPathElement.class,
|
assertPathElements(pathPattern, SeparatorPathElement.class, SeparatorPathElement.class, SeparatorPathElement.class,
|
||||||
SeparatorPathElement.class, LiteralPathElement.class, SeparatorPathElement.class,
|
LiteralPathElement.class, SeparatorPathElement.class,
|
||||||
SeparatorPathElement.class, SeparatorPathElement.class, SeparatorPathElement.class,
|
SeparatorPathElement.class, SeparatorPathElement.class, SeparatorPathElement.class,
|
||||||
LiteralPathElement.class, SeparatorPathElement.class, LiteralPathElement.class);
|
LiteralPathElement.class, SeparatorPathElement.class, LiteralPathElement.class);
|
||||||
pathPattern = checkStructure("/////**");
|
pathPattern = checkStructure("/////**");
|
||||||
|
@ -464,36 +410,18 @@ public class PathPatternParserTests {
|
||||||
assertEquals(p2, patterns.get(1));
|
assertEquals(p2, patterns.get(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private PathPattern parse(String pattern) {
|
private PathPattern parse(String pattern) {
|
||||||
PathPatternParser patternParser = new PathPatternParser();
|
PathPatternParser patternParser = new PathPatternParser();
|
||||||
return patternParser.parse(pattern);
|
return patternParser.parse(pattern);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verify the parsed chain of sections matches the original pattern and the separator count
|
* Verify the pattern string computed for a parsed pattern matches the original pattern text
|
||||||
* that has been determined is correct.
|
|
||||||
*/
|
*/
|
||||||
private PathPattern checkStructure(String pattern) {
|
private PathPattern checkStructure(String pattern) {
|
||||||
int count = 0;
|
PathPattern pp = parse(pattern);
|
||||||
for (int i = 0; i < pattern.length(); i++) {
|
assertEquals(pattern, pp.computePatternString());
|
||||||
if (pattern.charAt(i) == '/') {
|
return pp;
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkError(String pattern, int expectedPos, PatternMessage expectedMessage, String... expectedInserts) {
|
private void checkError(String pattern, int expectedPos, PatternMessage expectedMessage, String... expectedInserts) {
|
||||||
|
@ -531,4 +459,20 @@ public class PathPatternParserTests {
|
||||||
return capturedVariableCount + wildcardCount * 100;
|
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<String, PathMatchResult> matchAndExtract(PathPattern pp, String path) {
|
||||||
|
return pp.matchAndExtract(PathPatternMatcherTests.toPathContainer(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
private PathContainer toPSC(String path) {
|
||||||
|
return PathPatternMatcherTests.toPathContainer(path);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue