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:
committed by
Brian Clozel
parent
6f029392c7
commit
f58ffad939
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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) + ")";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 '<separator>*'
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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/*_*/*_{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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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/*/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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user