Introduce PathPatternParser for optimized path matching

This commit introduces a PathPatternParser which parses request pattern
strings into PathPattern objects which can then be used to fast
match incoming string paths. The parser and matching supports the syntax
as described in SPR-14544. The code is optimized around the common usages
of request patterns and is designed to create very little transient
garbage when matching.

Issue: SPR-14544
This commit is contained in:
Andy Clement
2016-10-11 15:14:08 -07:00
committed by Brian Clozel
parent 6f029392c7
commit f58ffad939
44 changed files with 3880 additions and 73 deletions

View File

@@ -21,9 +21,9 @@ import java.util.LinkedHashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.util.PathMatcher;
import org.springframework.web.util.ParsingPathMatcher;
import org.springframework.web.util.UrlPathHelper;
/**
@@ -40,7 +40,7 @@ public class UrlBasedCorsConfigurationSource implements CorsConfigurationSource
private final Map<String, CorsConfiguration> corsConfigurations = new LinkedHashMap<>();
private PathMatcher pathMatcher = new AntPathMatcher();
private PathMatcher pathMatcher = new ParsingPathMatcher();
private UrlPathHelper urlPathHelper = new UrlPathHelper();

View File

@@ -0,0 +1,115 @@
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.util;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import org.springframework.util.PathMatcher;
import org.springframework.web.util.patterns.PathPattern;
import org.springframework.web.util.patterns.PathPatternParser;
import org.springframework.web.util.patterns.PatternComparatorConsideringPath;
/**
*
* @author Andy Clement
* @since 5.0
*/
public class ParsingPathMatcher implements PathMatcher {
Map<String,PathPattern> cache = new HashMap<>();
PathPatternParser parser;
public ParsingPathMatcher() {
parser = new PathPatternParser();
}
@Override
public boolean match(String pattern, String path) {
PathPattern p = getPathPattern(pattern);
return p.matches(path);
}
@Override
public boolean matchStart(String pattern, String path) {
PathPattern p = getPathPattern(pattern);
return p.matchStart(path);
}
@Override
public String extractPathWithinPattern(String pattern, String path) {
PathPattern p = getPathPattern(pattern);
return p.extractPathWithinPattern(path);
}
@Override
public Map<String, String> extractUriTemplateVariables(String pattern, String path) {
PathPattern p = getPathPattern(pattern);
return p.matchAndExtract(path);
}
@Override
public String combine(String pattern1, String pattern2) {
PathPattern pathPattern = getPathPattern(pattern1);
return pathPattern.combine(pattern2);
}
@Override
public Comparator<String> getPatternComparator(String path) {
return new PathPatternStringComparatorConsideringPath(path);
}
class PathPatternStringComparatorConsideringPath implements Comparator<String> {
PatternComparatorConsideringPath ppcp;
public PathPatternStringComparatorConsideringPath(String path) {
ppcp = new PatternComparatorConsideringPath(path);
}
@Override
public int compare(String o1, String o2) {
if (o1 == null) {
return (o2==null?0:+1);
} else if (o2 == null) {
return -1;
}
PathPattern p1 = getPathPattern(o1);
PathPattern p2 = getPathPattern(o2);
return ppcp.compare(p1,p2);
}
}
@Override
public boolean isPattern(String path) {
// TODO crude, should be smarter, lookup pattern and ask it
return (path.indexOf('*') != -1 || path.indexOf('?') != -1);
}
private PathPattern getPathPattern(String pattern) {
PathPattern pathPattern = cache.get(pattern);
if (pathPattern == null) {
pathPattern = parser.parse(pattern);
cache.put(pattern, pathPattern);
}
return pathPattern;
}
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.util.patterns;
import org.springframework.web.util.patterns.PathPattern.MatchingContext;
/**
* A path element representing capturing the rest of a path. In the pattern
* '/foo/{*foobar}' the /{*foobar} is represented as a {@link CaptureTheRestPathElement}.
*
* @author Andy Clement
*/
class CaptureTheRestPathElement extends PathElement {
private String variableName;
private char separator;
/**
* @param pos
* @param captureDescriptor a character array containing contents like '{' '*' 'a' 'b' '}'
* @param separator the separator ahead of this construct
*/
CaptureTheRestPathElement(int pos, char[] captureDescriptor, char separator) {
super(pos);
variableName = new String(captureDescriptor, 2, captureDescriptor.length - 3);
this.separator = separator;
}
@Override
public boolean matches(int candidateIndex, MatchingContext matchingContext) {
// No need to handle 'match start' checking as this captures everything
// anyway and cannot be followed by anything else
// assert next == null
while ((candidateIndex+1)<matchingContext.candidateLength &&
matchingContext.candidate[candidateIndex+1] == separator) {
candidateIndex++;
}
if (matchingContext.extractingVariables) {
matchingContext.set(variableName, new String(matchingContext.candidate, candidateIndex,
matchingContext.candidateLength - candidateIndex));
}
return true;
}
public String toString() {
return "CaptureTheRest(/{*" + variableName + "})";
}
@Override
public int getNormalizedLength() {
return 1;
}
@Override
public int getWildcardCount() {
return 0;
}
@Override
public int getCaptureCount() {
return 1;
}
}

View File

@@ -0,0 +1,121 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.util.patterns;
import java.util.regex.Matcher;
import org.springframework.web.util.patterns.PathPattern.MatchingContext;
/**
* A path element representing capturing a piece of the path as a variable. In the pattern
* '/foo/{bar}/goo' the {bar} is represented as a {@link CaptureVariablePathElement}.
*
* @author Andy Clement
*/
class CaptureVariablePathElement extends PathElement {
private String variableName;
private java.util.regex.Pattern constraintPattern;
/**
* @param pos the position in the pattern of this capture element
* @param captureDescriptor is of the form {AAAAA[:pattern]}
*/
CaptureVariablePathElement(int pos, char[] captureDescriptor, boolean caseSensitive) {
super(pos);
int colon = -1;
for (int i = 0; i < captureDescriptor.length; i++) {
if (captureDescriptor[i] == ':') {
colon = i;
break;
}
}
if (colon == -1) {
// no constraint
variableName = new String(captureDescriptor, 1, captureDescriptor.length - 2);
} else {
variableName = new String(captureDescriptor, 1, colon - 1);
if (caseSensitive) {
constraintPattern = java.util.regex.Pattern
.compile(new String(captureDescriptor, colon + 1, captureDescriptor.length - colon - 2));
} else {
constraintPattern = java.util.regex.Pattern.compile(
new String(captureDescriptor, colon + 1, captureDescriptor.length - colon - 2),
java.util.regex.Pattern.CASE_INSENSITIVE);
}
}
}
@Override
public boolean matches(int candidateIndex, MatchingContext matchingContext) {
int nextPos = matchingContext.scanAhead(candidateIndex);
CharSequence candidateCapture = null;
if (constraintPattern != null) {
// TODO possible optimization - only regex match if rest of pattern matches? Benefit likely to vary pattern to pattern
candidateCapture = new SubSequence(matchingContext.candidate, candidateIndex, nextPos);
Matcher m = constraintPattern.matcher(candidateCapture);
if (m.groupCount() != 0) {
throw new IllegalArgumentException("No capture groups allowed in the constraint regex: "+constraintPattern.pattern());
}
if (!m.matches()) {
return false;
}
}
boolean match = false;
if (next == null) {
match = (nextPos == matchingContext.candidateLength);
} else {
if (matchingContext.isMatchStartMatching && nextPos == matchingContext.candidateLength) {
match = true; // no more data but matches up to this point
} else {
match = next.matches(nextPos, matchingContext);
}
}
if (match && matchingContext.extractingVariables) {
matchingContext.set(variableName, new String(matchingContext.candidate, candidateIndex, nextPos - candidateIndex));
}
return match;
}
public String getVariableName() {
return this.variableName;
}
public String toString() {
return "CaptureVariable({" + variableName + (constraintPattern == null ? "" : ":" + constraintPattern.pattern()) + "})";
}
@Override
public int getNormalizedLength() {
return 1;
}
@Override
public int getWildcardCount() {
return 0;
}
@Override
public int getCaptureCount() {
return 1;
}
@Override
public int getScore() {
return CAPTURE_VARIABLE_WEIGHT;
}
}

View File

@@ -0,0 +1,87 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.util.patterns;
import org.springframework.web.util.patterns.PathPattern.MatchingContext;
/**
* A literal path element. In the pattern '/foo/bar/goo' there are three
* literal path elements 'foo', 'bar' and 'goo'.
*
* @author Andy Clement
*/
class LiteralPathElement extends PathElement {
private char[] text;
private int len;
private boolean caseSensitive;
public LiteralPathElement(int pos, char[] literalText, boolean caseSensitive) {
super(pos);
this.len = literalText.length;
this.caseSensitive = caseSensitive;
if (caseSensitive) {
this.text = literalText;
} else {
// Force all the text lower case to make matching faster
this.text = new char[literalText.length];
for (int i = 0; i < len; i++) {
this.text[i] = Character.toLowerCase(literalText[i]);
}
}
}
@Override
public boolean matches(int candidateIndex, MatchingContext matchingContext) {
if ((candidateIndex + text.length) > matchingContext.candidateLength) {
return false; // not enough data, cannot be a match
}
if (caseSensitive) {
for (int i = 0; i < len; i++) {
if (matchingContext.candidate[candidateIndex++] != text[i]) {
return false;
}
}
} else {
for (int i = 0; i < len; i++) {
// TODO revisit performance if doing a lot of case insensitive matching
if (Character.toLowerCase(matchingContext.candidate[candidateIndex++]) != text[i]) {
return false;
}
}
}
if (next == null) {
return candidateIndex == matchingContext.candidateLength;
} else {
if (matchingContext.isMatchStartMatching && candidateIndex == matchingContext.candidateLength) {
return true; // no more data but everything matched so far
}
return next.matches(candidateIndex, matchingContext);
}
}
@Override
public int getNormalizedLength() {
return len;
}
public String toString() {
return "Literal(" + new String(text) + ")";
}
}

View File

@@ -0,0 +1,88 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.util.patterns;
import org.springframework.web.util.patterns.PathPattern.MatchingContext;
/**
* Common supertype for the Ast nodes created to represent a path pattern.
*
* @author Andy Clement
*/
abstract class PathElement {
// Score related
protected static final int WILDCARD_WEIGHT = 100;
protected static final int CAPTURE_VARIABLE_WEIGHT = 1;
/**
* Position in the pattern where this path element starts
*/
protected int pos;
/**
* The next path element in the chain
*/
protected PathElement next;
/**
* The previous path element in the chain
*/
protected PathElement prev;
/**
* Create a new path element.
* @param pos the position where this path element starts in the pattern data
*/
PathElement(int pos) {
this.pos = pos;
}
/**
* Attempt to match this path element.
*
* @param candidatePos the current position within the candidate path
* @param matchingContext encapsulates context for the match including the candidate
* @return true if matches, otherwise false
*/
public abstract boolean matches(int candidatePos, MatchingContext matchingContext);
/**
* @return the length of the path element where captures are considered to be one character long
*/
public abstract int getNormalizedLength();
/**
* @return the number of variables captured by the path element
*/
public int getCaptureCount() {
return 0;
}
/**
* @return the number of wildcard elements (*, ?) in the path element
*/
public int getWildcardCount() {
return 0;
}
/**
* @return the score for this PathElement, combined score is used to compare parsed patterns.
*/
public int getScore() {
return 0;
}
}

View File

@@ -0,0 +1,462 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.util.patterns;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.springframework.util.PathMatcher;
/**
* Represents a parsed path pattern. Includes a chain of path elements
* for fast matching and accumulates computed state for quick comparison of
* patterns.
*
* @author Andy Clement
*/
public class PathPattern implements Comparable<PathPattern> {
private final static Map<String,String> NO_VARIABLES_MAP = Collections.emptyMap();
/** First path element in the parsed chain of path elements for this pattern */
private PathElement head;
/** The text of the parsed pattern */
private String patternString;
/** The separator used when parsing the pattern */
private char separator;
/** Will this match candidates in a case sensitive way? (case sensitivity at parse time) */
private boolean caseSensitive;
/** How many variables are captured in this pattern */
private int capturedVariableCount;
/**
* The normalized length is trying to measure the 'active' part of the pattern. It is computed
* by assuming all captured variables have a normalized length of 1. Effectively this means changing
* your variable name lengths isn't going to change the length of the active part of the pattern.
* Useful when comparing two patterns.
*/
int normalizedLength;
/**
* Does the pattern end with '&lt;separator&gt;*'
*/
boolean endsWithSeparatorWildcard = false;
/**
* Score is used to quickly compare patterns. Different pattern components are given different
* weights. A 'lower score' is more specific. Current weights:
* <ul>
* <li>Captured variables are worth 1
* <li>Wildcard is worth 100
* </ul>
*/
private int score;
/** Does the pattern end with {*...} */
private boolean isCatchAll = false;
public PathPattern(String patternText, PathElement head, char separator, boolean caseSensitive) {
this.head = head;
this.patternString = patternText;
this.separator = separator;
this.caseSensitive = caseSensitive;
// Compute fields for fast comparison
PathElement s = head;
while (s != null) {
this.capturedVariableCount += s.getCaptureCount();
this.normalizedLength += s.getNormalizedLength();
this.score += s.getScore();
if (s instanceof CaptureTheRestPathElement || s instanceof WildcardTheRestPathElement) {
this.isCatchAll = true;
}
if (s instanceof SeparatorPathElement && s.next!=null && s.next instanceof WildcardPathElement && s.next.next == null) {
this.endsWithSeparatorWildcard=true;
}
s = s.next;
}
}
/**
* @param path the candidate path to attempt to match against this pattern
* @return true if the path matches this pattern
*/
public boolean matches(String path) {
if (head == null) {
return (path == null) || (path.length() == 0);
} else if (path == null || path.length() == 0) {
if (head instanceof WildcardTheRestPathElement || head instanceof CaptureTheRestPathElement) {
path = ""; // Will allow CaptureTheRest to bind the variable to empty
} else {
return false;
}
}
MatchingContext matchingContext = new MatchingContext(path,false);
return head.matches(0, matchingContext);
}
/**
* @param path the path to check against the pattern
* @return true if the pattern matches as much of the path as is supplied
*/
public boolean matchStart(String path) {
if (head == null) {
return (path==null || path.length() == 0);
} else if (path == null || path.length() == 0) {
return true;
}
MatchingContext matchingContext = new MatchingContext(path,false);
matchingContext.setMatchStartMatching(true);
return head.matches(0, matchingContext);
}
/**
* @param path a path to match against this pattern
* @return a map of extracted variables - an empty map if no variables extracted.
*/
public Map<String, String> matchAndExtract(String path) {
MatchingContext matchingContext = new MatchingContext(path,true);
if (head != null && head.matches(0, matchingContext)) {
return matchingContext.getExtractedVariables();
} else {
if (path== null || path.length()==0) {
return NO_VARIABLES_MAP;
} else {
throw new IllegalStateException("Pattern \"" + this.toString() + "\" is not a match for \"" + path + "\"");
}
}
}
/**
* @return the original pattern string that was parsed to create this PathPattern
*/
public String getPatternString() {
return patternString;
}
public PathElement getHeadSection() {
return head;
}
/**
* Given a full path, determine the pattern-mapped part. <p>For example: <ul>
* <li>'{@code /docs/cvs/commit.html}' and '{@code /docs/cvs/commit.html} -> ''</li>
* <li>'{@code /docs/*}' and '{@code /docs/cvs/commit} -> '{@code cvs/commit}'</li>
* <li>'{@code /docs/cvs/*.html}' and '{@code /docs/cvs/commit.html} -> '{@code commit.html}'</li>
* <li>'{@code /docs/**}' and '{@code /docs/cvs/commit} -> '{@code cvs/commit}'</li>
* </ul>
* <p><b>Note:</b> Assumes that {@link #matches} returns {@code true} for '{@code pattern}' and '{@code path}', but
* does <strong>not</strong> enforce this. As per the contract on {@link PathMatcher}, this
* method will trim leading/trailing separators. It will also remove duplicate separators in
* the returned path.
* @param path a path that matches this pattern
* @return the subset of the path that is matched by pattern or "" if none of it is matched by pattern elements
*/
public String extractPathWithinPattern(String path) {
// assert this.matches(path)
PathElement s = head;
int separatorCount = 0;
// Find first path element that is pattern based
while (s != null) {
if (s instanceof SeparatorPathElement || s instanceof CaptureTheRestPathElement || s instanceof WildcardTheRestPathElement) {
separatorCount++;
}
if (s.getWildcardCount()!=0 || s.getCaptureCount()!=0) {
break;
}
s = s.next;
}
if (s == null) {
return ""; // There is no pattern mapped section
}
// Now separatorCount indicates how many sections of the path to skip
char[] pathChars = path.toCharArray();
int len = pathChars.length;
int pos = 0;
while (separatorCount > 0 && pos < len) {
if (path.charAt(pos++) == separator) {
// Skip any adjacent separators
while (path.charAt(pos) == separator) {
pos++;
}
separatorCount--;
}
}
int end = len;
// Trim trailing separators
while (path.charAt(end-1) == separator) {
end--;
}
// Check if multiple separators embedded in the resulting path, if so trim them out.
// Example: aaa////bbb//ccc/d -> aaa/bbb/ccc/d
// The stringWithDuplicateSeparatorsRemoved is only computed if necessary
int c = pos;
StringBuilder stringWithDuplicateSeparatorsRemoved = null;
while (c<end) {
char ch = path.charAt(c);
if (ch == separator) {
if ((c+1)<end && path.charAt(c+1)==separator) {
// multiple separators
if (stringWithDuplicateSeparatorsRemoved == null) {
// first time seen, need to capture all data up to this point
stringWithDuplicateSeparatorsRemoved = new StringBuilder();
stringWithDuplicateSeparatorsRemoved.append(path.substring(pos,c));
}
do {
c++;
} while ((c+1)<end && path.charAt(c+1)==separator);
}
}
if (stringWithDuplicateSeparatorsRemoved != null) {
stringWithDuplicateSeparatorsRemoved.append(ch);
}
c++;
}
if (stringWithDuplicateSeparatorsRemoved != null) {
return stringWithDuplicateSeparatorsRemoved.toString();
}
return pos == len ? "" : path.substring(pos,end);
}
/**
* Compare this pattern with a supplied pattern. Return -1,0,+1 if this pattern
* is more specific, the same or less specific than the supplied pattern.
* The aim is to sort more specific patterns first.
*/
@Override
public int compareTo(PathPattern p) {
// 1) null is sorted last
if (p == null) {
return -1;
}
// 2) catchall patterns are sorted last. If both catchall then the
// length is considered
if (isCatchAll()) {
if (p.isCatchAll()) {
int lenDifference = this.getNormalizedLength() - p.getNormalizedLength();
if (lenDifference != 0) {
return (lenDifference < 0) ? +1 : -1;
}
} else {
return +1;
}
} else if (p.isCatchAll()) {
return -1;
}
// 3) This will sort such that if they differ in terms of wildcards or
// captured variable counts, the one with the most will be sorted last
int score = this.getScore() - p.getScore();
if (score != 0) {
return (score < 0) ? -1 : +1;
}
// 4) longer is better
int lenDifference = this.getNormalizedLength() - p.getNormalizedLength();
return (lenDifference < 0) ? +1 : (lenDifference == 0 ? 0 : -1);
}
public int getScore() {
return score;
}
public boolean isCatchAll() {
return isCatchAll;
}
/**
* The normalized length is trying to measure the 'active' part of the pattern. It is computed
* by assuming all capture variables have a normalized length of 1. Effectively this means changing
* your variable name lengths isn't going to change the length of the active part of the pattern.
* Useful when comparing two patterns.
* @return the normalized length of the pattern
*/
public int getNormalizedLength() {
return normalizedLength;
}
public boolean equals(Object o) {
if (!(o instanceof PathPattern)) {
return false;
}
PathPattern p = (PathPattern) o;
return patternString.equals(p.getPatternString()) && separator == p.getSeparator()
&& caseSensitive == p.caseSensitive;
}
public int hashCode() {
return (patternString.hashCode() * 17 + separator) * 17 + (caseSensitive ? 1 : 0);
}
public String toChainString() {
StringBuilder buf = new StringBuilder();
PathElement pe = head;
while (pe!=null) {
buf.append(pe.toString()).append(" ");
pe = pe.next;
}
return buf.toString().trim();
}
public char getSeparator() {
return separator;
}
public int getCapturedVariableCount() {
return capturedVariableCount;
}
public String toString() {
return patternString;
}
/**
* 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;
private Map<String,String> extractedVariables;
public boolean extractingVariables;
public MatchingContext(String path, boolean extractVariables) {
candidate = path.toCharArray();
candidateLength = candidate.length;
this.extractingVariables = extractVariables;
}
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 NO_VARIABLES_MAP;
} 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;
}
}
/**
* Combine this pattern with another. Currently does not produce a new PathPattern, just produces a new string.
*/
public String combine(String pattern2string) {
// If one of them is empty the result is the other. If both empty the result is ""
if (patternString == null || patternString.length()==0) {
if (pattern2string == null || pattern2string.length()==0) {
return "";
} else {
return pattern2string;
}
} else if (pattern2string == null || pattern2string.length()==0) {
return patternString;
}
// /* + /hotel => /hotel
// /*.* + /*.html => /*.html
// However:
// /usr + /user => /usr/user
// /{foo} + /bar => /{foo}/bar
if (!patternString.equals(pattern2string) && capturedVariableCount==0 && matches(pattern2string)) {
return pattern2string;
}
// /hotels/* + /booking => /hotels/booking
// /hotels/* + booking => /hotels/booking
if (endsWithSeparatorWildcard) {
return concat(patternString.substring(0,patternString.length()-2), pattern2string);
}
// /hotels + /booking => /hotels/booking
// /hotels + booking => /hotels/booking
int starDotPos1 = patternString.indexOf("*."); // Are there any file prefix/suffix things to consider?
if (capturedVariableCount!=0 || starDotPos1 == -1 || separator=='.') {
return concat(patternString, pattern2string);
}
// /*.html + /hotel => /hotel.html
// /*.html + /hotel.* => /hotel.html
String firstExtension = 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: " + patternString + " and " + pattern2string);
}
return file2 + (firstExtensionWild?secondExtension:firstExtension);
}
/**
* Join two paths together including a separator if necessary. Extraneous separators are removed (if the first path
* ends with one and the second path starts with one).
* @param path1 First path
* @param path2 Second path
* @return joined path that may include separator if necessary
*/
private String concat(String path1, String path2) {
boolean path1EndsWithSeparator = path1.charAt(path1.length()-1)==separator;
boolean path2StartsWithSeparator = path2.charAt(0)==separator;
if (path1EndsWithSeparator && path2StartsWithSeparator) {
return path1 + path2.substring(1);
}
else if (path1EndsWithSeparator || path2StartsWithSeparator) {
return path1 + path2;
}
else {
return path1 + separator + path2;
}
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.util.patterns;
import java.util.Comparator;
/**
* Basic PathPattern comparator.
*
* @author Andy Clement
*/
public class PathPatternComparator implements Comparator<PathPattern> {
@Override
public int compare(PathPattern o1, PathPattern o2) {
// Nulls get sorted to the end
if (o1 == null) {
return (o2==null?0:+1);
} else if (o2 == null) {
return -1;
}
return o1.compareTo(o2);
}
}

View File

@@ -0,0 +1,390 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.util.patterns;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.PatternSyntaxException;
/**
* Parser for URI template patterns. It breaks the path pattern into a number of
* path elements in a linked list.
*
* @author Andy Clement
*/
public class PathPatternParser {
public final static char DEFAULT_SEPARATOR = '/';
// The expected path separator to split path elements during parsing
char separator = DEFAULT_SEPARATOR;
// Is the parser producing case sensitive PathPattern matchers
boolean caseSensitive = true;
// The input data for parsing
private char[] pathPatternData;
// The length of the input data
private int pathPatternLength;
// Current parsing position
int pos;
// How many ? characters in a particular path element
private int singleCharWildcardCount;
// Is the path pattern using * characters in a particular path element
private boolean wildcard = false;
// Is the construct {*...} being used in a particular path element
private boolean isCaptureTheRestVariable = false;
// Has the parser entered a {...} variable capture block in a particular
// path element
private boolean insideVariableCapture = false;
// How many variable captures are occurring in a particular path element
private int variableCaptureCount = 0;
// Start of the most recent path element in a particular path element
int pathElementStart;
// Start of the most recent variable capture in a particular path element
int variableCaptureStart;
// Variables captures in this path pattern
List<String> capturedVariableNames;
// The head of the path element chain currently being built
PathElement headPE;
// The most recently constructed path element in the chain
PathElement currentPE;
/**
* Default constructor, will use the default path separator to identify
* the elements of the path pattern.
*/
public PathPatternParser() {
}
/**
* Create a PatternParser that will use the specified separator instead of
* the default.
*
* @param separator the path separator to look for when parsing.
*/
public PathPatternParser(char separator) {
this.separator = separator;
}
public void setCaseSensitive(boolean caseSensitive) {
this.caseSensitive = caseSensitive;
}
/**
* Process the path pattern data, a character at a time, breaking it into
* path elements around separator boundaries and verifying the structure at each
* stage. Produces a PathPattern object that can be used for fast matching
* against paths.
*
* @param pathPattern the input path pattern, e.g. /foo/{bar}
* @return a PathPattern for quickly matching paths against the specified path pattern
*/
public PathPattern parse(String pathPattern) {
if (pathPattern == null) {
pathPattern = "";
}
// int starstar = pathPattern.indexOf("**");
// if (starstar!=-1 && starstar!=pathPattern.length()-2) {
// throw new IllegalStateException("Not allowed ** unless at end of pattern: "+pathPattern);
// }
pathPatternData = pathPattern.toCharArray();
pathPatternLength = pathPatternData.length;
headPE = null;
currentPE = null;
capturedVariableNames = null;
pathElementStart = -1;
pos = 0;
resetPathElementState();
while (pos < pathPatternLength) {
char ch = pathPatternData[pos];
if (ch == separator) {
if (pathElementStart != -1) {
pushPathElement(createPathElement());
}
// Skip over multiple separators
while ((pos+1) < pathPatternLength && pathPatternData[pos+1] == separator) {
pos++;
}
if (peekDoubleWildcard()) {
pushPathElement(new WildcardTheRestPathElement(pos,separator));
pos+=2;
} else {
pushPathElement(new SeparatorPathElement(pos, separator));
}
} else {
if (pathElementStart == -1) {
pathElementStart = pos;
}
if (ch == '?') {
singleCharWildcardCount++;
} else if (ch == '{') {
if (insideVariableCapture) {
throw new PatternParseException(pos, pathPatternData, PatternMessage.ILLEGAL_NESTED_CAPTURE);
// If we enforced that adjacent captures weren't allowed, this would do it (this would be an error: /foo/{bar}{boo}/)
// } else if (pos > 0 && pathPatternData[pos - 1] == '}') {
// throw new PatternParseException(pos, pathPatternData,
// PatternMessage.CANNOT_HAVE_ADJACENT_CAPTURES);
}
insideVariableCapture = true;
variableCaptureStart = pos;
} else if (ch == '}') {
if (!insideVariableCapture) {
throw new PatternParseException(pos, pathPatternData, PatternMessage.MISSING_OPEN_CAPTURE);
}
insideVariableCapture = false;
if (isCaptureTheRestVariable && (pos + 1) < pathPatternLength) {
throw new PatternParseException(pos + 1, pathPatternData,
PatternMessage.NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST);
}
variableCaptureCount++;
} else if (ch == ':') {
if (insideVariableCapture) {
skipCaptureRegex();
insideVariableCapture = false;
variableCaptureCount++;
}
} else if (ch == '*') {
if (insideVariableCapture) {
if (variableCaptureStart == pos - 1) {
isCaptureTheRestVariable = true;
}
}
wildcard = true;
}
// Check that the characters used for captured variable names are like java identifiers
if (insideVariableCapture) {
if ((variableCaptureStart + 1 + (isCaptureTheRestVariable ? 1 : 0)) == pos
&& !Character.isJavaIdentifierStart(ch)) {
throw new PatternParseException(pos, pathPatternData,
PatternMessage.ILLEGAL_CHARACTER_AT_START_OF_CAPTURE_DESCRIPTOR,
Character.toString(ch));
} else if ((pos > (variableCaptureStart + 1 + (isCaptureTheRestVariable ? 1 : 0))
&& !Character.isJavaIdentifierPart(ch))) {
throw new PatternParseException(pos, pathPatternData,
PatternMessage.ILLEGAL_CHARACTER_IN_CAPTURE_DESCRIPTOR, Character.toString(ch));
}
}
}
pos++;
}
if (pathElementStart != -1) {
pushPathElement(createPathElement());
}
return new PathPattern(pathPattern, headPE, separator, caseSensitive);
}
/**
* Just hit a ':' and want to jump over the regex specification for this
* variable. pos will be pointing at the ':', we want to skip until the }.
* <p>
* Nested {...} pairs don't have to be escaped: <tt>/abc/{var:x{1,2}}/def</tt>
* <p>An escaped } will not be treated as the end of the regex: <tt>/abc/{var:x\\{y:}/def</tt>
* <p>A separator that should not indicate the end of the regex can be escaped:
*/
private void skipCaptureRegex() {
pos++;
int regexStart = pos;
int curlyBracketDepth = 0; // how deep in nested {...} pairs
boolean previousBackslash = false;
while (pos < pathPatternLength) {
char ch = pathPatternData[pos];
if (ch == '\\' && !previousBackslash) {
pos++;
previousBackslash = true;
continue;
}
if (ch == '{' && !previousBackslash) {
curlyBracketDepth++;
} else if (ch == '}' && !previousBackslash) {
if (curlyBracketDepth == 0) {
if (regexStart == pos) {
throw new PatternParseException(regexStart, pathPatternData,
PatternMessage.MISSING_REGEX_CONSTRAINT);
}
return;
}
curlyBracketDepth--;
}
if (ch == separator && !previousBackslash) {
throw new PatternParseException(pos, pathPatternData, PatternMessage.MISSING_CLOSE_CAPTURE);
}
pos++;
previousBackslash=false;
}
throw new PatternParseException(pos - 1, pathPatternData, PatternMessage.MISSING_CLOSE_CAPTURE);
}
/**
* After processing a separator, a quick peek whether it is followed by **
* (and only ** before the end of the pattern or the next separator)
*/
private boolean peekDoubleWildcard() {
if ((pos + 2) >= pathPatternLength) {
return false;
}
if (pathPatternData[pos + 1] != '*' || pathPatternData[pos + 2] != '*') {
return false;
}
return (pos + 3 == pathPatternLength);
}
/**
* @param newPathElement the new path element to add to the chain being built
*/
private void pushPathElement(PathElement newPathElement) {
if (newPathElement instanceof CaptureTheRestPathElement) {
// There must be a separator ahead of this thing
// currentPE SHOULD be a SeparatorPathElement
if (currentPE == null) {
headPE = newPathElement;
currentPE = newPathElement;
} else if (currentPE instanceof SeparatorPathElement) {
PathElement peBeforeSeparator = currentPE.prev;
if (peBeforeSeparator == null) {
// /{*foobar} is at the start
headPE = newPathElement;
newPathElement.prev = peBeforeSeparator;
} else {
peBeforeSeparator.next = newPathElement;
newPathElement.prev = peBeforeSeparator;
}
currentPE = newPathElement;
} else {
throw new IllegalStateException("Expected SeparatorPathElement but was "+currentPE);
}
} else {
if (headPE == null) {
headPE = newPathElement;
currentPE = newPathElement;
} else {
currentPE.next = newPathElement;
newPathElement.prev = currentPE;
currentPE = newPathElement;
}
}
resetPathElementState();
}
/**
* Used the knowledge built up whilst processing since the last path element to determine what kind of path
* element to create.
* @return the new path element
*/
private PathElement createPathElement() {
if (insideVariableCapture) {
throw new PatternParseException(pos, pathPatternData, PatternMessage.MISSING_CLOSE_CAPTURE);
}
char[] pathElementText = new char[pos - pathElementStart];
System.arraycopy(pathPatternData, pathElementStart, pathElementText, 0, pos - pathElementStart);
PathElement newPE = null;
if (variableCaptureCount > 0) {
if (variableCaptureCount == 1 && pathElementStart == variableCaptureStart && pathPatternData[pos - 1] == '}') {
if (isCaptureTheRestVariable) {
// It is {*....}
newPE = new CaptureTheRestPathElement(pathElementStart, pathElementText, separator);
} else {
// It is a full capture of this element (possibly with constraint), for example: /foo/{abc}/
try {
newPE = new CaptureVariablePathElement(pathElementStart, pathElementText, caseSensitive);
} catch (PatternSyntaxException pse) {
throw new PatternParseException(pse, findRegexStart(pathPatternData,pathElementStart)+pse.getIndex(), pathPatternData, PatternMessage.JDK_PATTERN_SYNTAX_EXCEPTION);
}
recordCapturedVariable(pathElementStart, ((CaptureVariablePathElement) newPE).getVariableName());
}
} else {
if (isCaptureTheRestVariable) {
throw new PatternParseException(pathElementStart, pathPatternData, PatternMessage.CAPTURE_ALL_IS_STANDALONE_CONSTRUCT);
}
RegexPathElement newRegexSection = new RegexPathElement(pathElementStart, pathElementText, caseSensitive, pathPatternData);
for (String variableName : newRegexSection.getVariableNames()) {
recordCapturedVariable(pathElementStart, variableName);
}
newPE = newRegexSection;
}
} else {
if (wildcard) {
if (pos - 1 == pathElementStart) {
newPE = new WildcardPathElement(pathElementStart);
} else {
newPE = new RegexPathElement(pathElementStart, pathElementText, caseSensitive, pathPatternData);
}
} else if (singleCharWildcardCount!=0) {
newPE = new SingleCharWildcardedPathElement(pathElementStart, pathElementText, singleCharWildcardCount, caseSensitive);
} else {
newPE = new LiteralPathElement(pathElementStart, pathElementText, caseSensitive);
}
}
return newPE;
}
/**
* For a path element representing a captured variable, locate the constraint pattern.
* Assumes there is a constraint pattern.
* @param data a complete path expression, e.g. /aaa/bbb/{ccc:...}
* @param offset the start of the capture pattern of interest
* @return the index of the character after the ':' within the pattern expression relative to the start of the whole expression
*/
private int findRegexStart(char[] data, int offset) {
int pos = offset;
while (pos<data.length) {
if (data[pos] == ':') {
return pos + 1;
}
pos++;
}
return -1;
}
/**
* Reset all the flags and position markers computed during path element processing.
*/
private void resetPathElementState() {
pathElementStart = -1;
singleCharWildcardCount = 0;
insideVariableCapture = false;
variableCaptureCount = 0;
wildcard = false;
isCaptureTheRestVariable = false;
variableCaptureStart = -1;
}
/**
* Record a new captured variable. If it clashes with an existing one then report an error.
*/
private void recordCapturedVariable(int pos, String variableName) {
if (capturedVariableNames == null) {
capturedVariableNames = new ArrayList<>();
}
if (capturedVariableNames.contains(variableName)) {
throw new PatternParseException(pos, this.pathPatternData, PatternMessage.ILLEGAL_DOUBLE_CAPTURE, variableName);
}
capturedVariableNames.add(variableName);
}
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.util.patterns;
import java.util.Comparator;
/**
* Similar to {@link PathPatternComparator} but this takes account of a specified path and
* sorts anything that exactly matches it to be first.
*
* @author Andy Clement
*/
public class PatternComparatorConsideringPath implements Comparator<PathPattern> {
private String path;
public PatternComparatorConsideringPath(String path) {
this.path = path;
}
@Override
public int compare(PathPattern o1, PathPattern o2) {
// Nulls get sorted to the end
if (o1 == null) {
return (o2==null?0:+1);
} else if (o2 == null) {
return -1;
}
if (o1.getPatternString().equals(path)) {
return (o2.getPatternString().equals(path))?0:-1;
} else if (o2.getPatternString().equals(path)) {
return +1;
}
return o1.compareTo(o2);
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.util.patterns;
import java.text.MessageFormat;
/**
* The messages that can be included in a {@link PatternParseException} when there is a parse failure.
*
* @author Andy Clement
*/
public enum PatternMessage {
// @formatter:off
MISSING_CLOSE_CAPTURE("Expected close capture character after variable name '}'"),
MISSING_OPEN_CAPTURE("Missing preceeding open capture character before variable name'{'"),
ILLEGAL_NESTED_CAPTURE("Not allowed to nest variable captures"),
CANNOT_HAVE_ADJACENT_CAPTURES("Adjacent captures are not allowed"),
ILLEGAL_CHARACTER_AT_START_OF_CAPTURE_DESCRIPTOR("Character ''{0}'' is not allowed at start of captured variable name"),
ILLEGAL_CHARACTER_IN_CAPTURE_DESCRIPTOR("Character ''{0}'' is not allowed in a captured variable name"),
NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST("No more pattern data allowed after '{*...}' pattern element"),
BADLY_FORMED_CAPTURE_THE_REST("Expected form when capturing the rest of the path is simply '{*...}'"),
MISSING_REGEX_CONSTRAINT("Missing regex constraint on capture"),
ILLEGAL_DOUBLE_CAPTURE("Not allowed to capture ''{0}'' twice in the same pattern"),
JDK_PATTERN_SYNTAX_EXCEPTION("Exception occurred in pattern compilation"),
CAPTURE_ALL_IS_STANDALONE_CONSTRUCT("'{*...}' can only be preceeded by a path separator");
// @formatter:on
private final String message;
private PatternMessage(String message) {
this.message = message;
}
public String formatMessage(Object... inserts) {
return MessageFormat.format(this.message, inserts);
}
}

View File

@@ -0,0 +1,86 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.util.patterns;
/**
* Exception that is thrown when there is a problem with the pattern being parsed.
*
* @author Andy Clement
*/
public class PatternParseException extends RuntimeException {
private static final long serialVersionUID = 1L;
private int pos;
private char[] patternText;
private final PatternMessage message;
private final Object[] inserts;
public PatternParseException(int pos, char[] patternText, PatternMessage message, Object... inserts) {
super(message.formatMessage(inserts));
this.pos = pos;
this.patternText = patternText;
this.message = message;
this.inserts = inserts;
}
public PatternParseException(Throwable cause, int pos, char[] patternText, PatternMessage message, Object... inserts) {
super(message.formatMessage(inserts),cause);
this.pos = pos;
this.patternText = patternText;
this.message = message;
this.inserts = inserts;
}
/**
* @return a formatted message with inserts applied
*/
@Override
public String getMessage() {
return this.message.formatMessage(this.inserts);
}
/**
* @return a detailed message that includes the original pattern text with a pointer to the error position,
* as well as the error message.
*/
public String toDetailedString() {
StringBuilder buf = new StringBuilder();
buf.append(patternText).append('\n');
for (int i = 0; i < pos; i++) {
buf.append(' ');
}
buf.append("^\n");
buf.append(getMessage());
return buf.toString();
}
public Object[] getInserts() {
return this.inserts;
}
public int getPosition() {
return pos;
}
public PatternMessage getMessageType() {
return message;
}
}

View File

@@ -0,0 +1,174 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.util.patterns;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.util.patterns.PathPattern.MatchingContext;
/**
* A regex path element. Used to represent any complicated element of the path.
* For example in '<tt>/foo/&ast;_&ast;/&ast;_{foobar}</tt>' both <tt>*_*</tt> and <tt>*_{foobar}</tt>
* are {@link RegexPathElement} path elements. Derived from the general {@link AntPathMatcher} approach.
*
* @author Andy Clement
*/
class RegexPathElement extends PathElement {
private final java.util.regex.Pattern GLOB_PATTERN = java.util.regex.Pattern
.compile("\\?|\\*|\\{((?:\\{[^/]+?\\}|[^/{}]|\\\\[{}])+?)\\}");
private final String DEFAULT_VARIABLE_PATTERN = "(.*)";
private final List<String> variableNames = new LinkedList<>();
private char[] regex;
private java.util.regex.Pattern pattern;
private boolean caseSensitive;
private int wildcardCount;
RegexPathElement(int pos, char[] regex, boolean caseSensitive, char[] completePattern) {
super(pos);
this.regex = regex;
this.caseSensitive = caseSensitive;
buildPattern(regex, completePattern);
}
public void buildPattern(char[] regex, char[] completePattern) {
StringBuilder patternBuilder = new StringBuilder();
String text = new String(regex);
Matcher matcher = GLOB_PATTERN.matcher(text);
int end = 0;
while (matcher.find()) {
patternBuilder.append(quote(text, end, matcher.start()));
String match = matcher.group();
if ("?".equals(match)) {
patternBuilder.append('.');
} else if ("*".equals(match)) {
patternBuilder.append(".*");
wildcardCount++;
} else if (match.startsWith("{") && match.endsWith("}")) {
int colonIdx = match.indexOf(':');
if (colonIdx == -1) {
patternBuilder.append(DEFAULT_VARIABLE_PATTERN);
String variableName = matcher.group(1);
if (variableNames.contains(variableName)) {
throw new PatternParseException(pos, completePattern, PatternMessage.ILLEGAL_DOUBLE_CAPTURE,
variableName);
}
this.variableNames.add(variableName);
} else {
String variablePattern = match.substring(colonIdx + 1, match.length() - 1);
patternBuilder.append('(');
patternBuilder.append(variablePattern);
patternBuilder.append(')');
String variableName = match.substring(1, colonIdx);
if (variableNames.contains(variableName)) {
throw new PatternParseException(pos, completePattern, PatternMessage.ILLEGAL_DOUBLE_CAPTURE,
variableName);
}
this.variableNames.add(variableName);
}
}
end = matcher.end();
}
patternBuilder.append(quote(text, end, text.length()));
if (caseSensitive) {
pattern = java.util.regex.Pattern.compile(patternBuilder.toString());
} else {
pattern = java.util.regex.Pattern.compile(patternBuilder.toString(),
java.util.regex.Pattern.CASE_INSENSITIVE);
}
}
public List<String> getVariableNames() {
return variableNames;
}
private String quote(String s, int start, int end) {
if (start == end) {
return "";
}
return java.util.regex.Pattern.quote(s.substring(start, end));
}
@Override
public boolean matches(int candidateIndex, MatchingContext matchingContext) {
int p = matchingContext.scanAhead(candidateIndex);
Matcher m = pattern.matcher(new SubSequence(matchingContext.candidate, candidateIndex, p));
boolean matches = m.matches();
if (matches) {
if (next == null) {
// No more pattern, is there more data?
matches = (p == matchingContext.candidateLength);
} else {
if (matchingContext.isMatchStartMatching && p == matchingContext.candidateLength) {
return true; // no more data but matches up to this point
}
matches = next.matches(p, matchingContext);
}
}
if (matches && matchingContext.extractingVariables) {
// Process captures
if (this.variableNames.size() != m.groupCount()) { // SPR-8455
throw new IllegalArgumentException("The number of capturing groups in the pattern segment "
+ this.pattern + " does not match the number of URI template variables it defines, "
+ "which can occur if capturing groups are used in a URI template regex. "
+ "Use non-capturing groups instead.");
}
for (int i = 1; i <= m.groupCount(); i++) {
String name = this.variableNames.get(i - 1);
String value = m.group(i);
matchingContext.set(name, value);
}
}
return matches;
}
public String toString() {
return "Regex(" + new String(regex) + ")";
}
@Override
public int getNormalizedLength() {
int varsLength = 0;
for (String variableName : variableNames) {
varsLength += variableName.length();
}
return regex.length - varsLength - variableNames.size();
}
public int getCaptureCount() {
return variableNames.size();
}
@Override
public int getWildcardCount() {
return wildcardCount;
}
@Override
public int getScore() {
return getCaptureCount()*CAPTURE_VARIABLE_WEIGHT + getWildcardCount()*WILDCARD_WEIGHT;
}
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.util.patterns;
import org.springframework.web.util.patterns.PathPattern.MatchingContext;
/**
* A separator path element. In the pattern '/foo/bar' the two occurrences
* of '/' will be represented by a SeparatorPathElement (if the default
* separator of '/' is being used).
*
* @author Andy Clement
*/
class SeparatorPathElement extends PathElement {
private char separator;
SeparatorPathElement(int pos, char separator) {
super(pos);
this.separator = separator;
}
/**
* Matching a separator is easy, basically the character at candidateIndex
* must be the separator.
*/
@Override
public boolean matches(int candidateIndex, MatchingContext matchingContext) {
boolean matched = false;
if (candidateIndex < matchingContext.candidateLength) {
if (matchingContext.candidate[candidateIndex] == separator) {
// Skip further separators in the path (they are all 'matched'
// by a single SeparatorPathElement)
while ((candidateIndex+1)<matchingContext.candidateLength &&
matchingContext.candidate[candidateIndex+1] == separator) {
candidateIndex++;
}
if (next == null) {
matched = ((candidateIndex + 1) == matchingContext.candidateLength);
} else {
candidateIndex++;
if (matchingContext.isMatchStartMatching && candidateIndex == matchingContext.candidateLength) {
return true; // no more data but matches up to this point
}
matched = next.matches(candidateIndex, matchingContext);
}
}
}
return matched;
}
public String toString() {
return "Separator(" + separator + ")";
}
@Override
public int getNormalizedLength() {
return 1;
}
}

View File

@@ -0,0 +1,98 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.util.patterns;
import org.springframework.web.util.patterns.PathPattern.MatchingContext;
/**
* A literal path element that does includes the single character wildcard '?' one
* or more times (to basically many any character at that position).
*
* @author Andy Clement
*/
class SingleCharWildcardedPathElement extends PathElement {
private char[] text;
private int len;
private int questionMarkCount;
private boolean caseSensitive;
public SingleCharWildcardedPathElement(int pos, char[] literalText, int questionMarkCount, boolean caseSensitive) {
super(pos);
this.len = literalText.length;
this.questionMarkCount = questionMarkCount;
this.caseSensitive = caseSensitive;
if (caseSensitive) {
this.text = literalText;
} else {
this.text = new char[literalText.length];
for (int i = 0; i < len; i++) {
this.text[i] = Character.toLowerCase(literalText[i]);
}
}
}
@Override
public boolean matches(int candidateIndex, MatchingContext matchingContext) {
if (matchingContext.candidateLength < (candidateIndex + len)) {
return false; // There isn't enough data to match
}
char[] candidate = matchingContext.candidate;
if (caseSensitive) {
for (int i = 0; i < len; i++) {
char t = text[i];
if (t != '?' && candidate[candidateIndex] != t) {
return false;
}
candidateIndex++;
}
} else {
for (int i = 0; i < len; i++) {
char t = text[i];
if (t != '?' && Character.toLowerCase(candidate[candidateIndex]) != t) {
return false;
}
candidateIndex++;
}
}
if (next == null) {
return candidateIndex == matchingContext.candidateLength;
} else {
if (matchingContext.isMatchStartMatching && candidateIndex == matchingContext.candidateLength) {
return true; // no more data but matches up to this point
}
return next.matches(candidateIndex, matchingContext);
}
}
@Override
public int getWildcardCount() {
return questionMarkCount;
}
public String toString() {
return "SingleCharWildcarding(" + new String(text) + ")";
}
@Override
public int getNormalizedLength() {
return len;
}
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.util.patterns;
/**
* Used to represent a subsection of an array, useful when wanting to pass that subset of data
* to another method (e.g. a java regex matcher) but not wanting to create a new string object to hold
* all that data.
*
* @author Andy Clement
*/
class SubSequence implements CharSequence {
private char[] chars;
private int start, end;
SubSequence(char[] chars, int start, int end) {
this.chars = chars;
this.start = start;
this.end = end;
}
@Override
public int length() {
return end - start;
}
@Override
public char charAt(int index) {
return chars[start + index];
}
@Override
public CharSequence subSequence(int start, int end) {
return new SubSequence(chars, this.start + start, this.start + end);
}
public String toString() {
return new String(chars,start,end-start);
}
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.util.patterns;
import org.springframework.web.util.patterns.PathPattern.MatchingContext;
/**
* A wildcard path element. In the pattern '/foo/&ast;/goo' the * is
* represented by a WildcardPathElement.
*
* @author Andy Clement
*/
class WildcardPathElement extends PathElement {
public WildcardPathElement(int pos) {
super(pos);
}
/**
* Matching on a WildcardPathElement is quite straight forward. Just scan the
* candidate from the candidateIndex for the next separator or the end of the
* candidate.
*/
@Override
public boolean matches(int candidateIndex, MatchingContext matchingContext) {
int nextPos = matchingContext.scanAhead(candidateIndex);
if (next == null) {
return (nextPos == matchingContext.candidateLength);
} else {
if (matchingContext.isMatchStartMatching && nextPos == matchingContext.candidateLength) {
return true; // no more data but matches up to this point
}
return next.matches(nextPos, matchingContext);
}
}
@Override
public int getNormalizedLength() {
return 1;
}
public String toString() {
return "Wildcard(*)";
}
@Override
public int getWildcardCount() {
return 1;
}
@Override
public int getScore() {
return WILDCARD_WEIGHT;
}
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.util.patterns;
import org.springframework.web.util.patterns.PathPattern.MatchingContext;
/**
* A path element representing wildcarding the rest of a path. In the pattern
* '/foo/**' the /** is represented as a {@link WildcardTheRestPathElement}.
*
* @author Andy Clement
*/
class WildcardTheRestPathElement extends PathElement {
private char separator;
WildcardTheRestPathElement(int pos, char separator) {
super(pos);
this.separator = separator;
}
@Override
public boolean matches(int candidateIndex, MatchingContext matchingContext) {
return true;
}
public String toString() {
return "WildcardTheRest("+separator+"**)";
}
@Override
public int getNormalizedLength() {
return 1;
}
@Override
public int getWildcardCount() {
return 1;
}
}