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;
}
}

View File

@ -0,0 +1,905 @@
/*
* 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.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
/**
* Exercise matching of {@link PathPattern} objects.
*
* @author Andy Clement
*/
public class PathPatternMatcherTests {
@Test
public void basicMatching() {
checkMatches(null, null);
checkMatches("", "");
checkMatches("", null);
checkNoMatch("/abc", null);
checkMatches(null, "");
checkNoMatch(null, "/abc");
checkMatches("/", "/");
checkNoMatch("/", "/a");
checkMatches("f", "f");
checkMatches("/foo", "/foo");
checkMatches("/foo/", "/foo/");
checkMatches("/foo/bar", "/foo/bar");
checkMatches("foo/bar", "foo/bar");
checkMatches("/foo/bar/", "/foo/bar/");
checkMatches("foo/bar/", "foo/bar/");
checkMatches("/foo/bar/woo", "/foo/bar/woo");
checkNoMatch("foo", "foobar");
checkMatches("/foo/bar", "/foo/bar");
checkNoMatch("/foo/bar", "/foo/baz");
// TODO Need more tests for escaped separators in path patterns and paths?
checkMatches("/foo\\/bar","/foo\\/bar"); // chain string is Separator(/) Literal(foo\) Separator(/) Literal(bar)
}
@Test
public void questionMarks() {
checkNoMatch("a", "ab");
checkMatches("/f?o/bar", "/foo/bar");
checkNoMatch("/foo/b2r", "/foo/bar");
checkNoMatch("?", "te");
checkMatches("?", "a");
checkMatches("???", "abc");
checkNoMatch("tes?", "te");
checkNoMatch("tes?", "tes");
checkNoMatch("tes?", "testt");
checkNoMatch("tes?", "tsst");
checkMatches(".?.a", ".a.a");
checkNoMatch(".?.a", ".aba");
}
@Test
public void captureTheRest() {
checkCapture("/customer/{*something}", "/customer/99", "something", "/99");
checkCapture("/customer/{*something}", "/customer/aa/bb/cc", "something",
"/aa/bb/cc");
checkCapture("/customer/{*something}", "/customer/", "something", "/");
checkCapture("/customer/////{*something}", "/customer/", "something", "/");
checkCapture("/customer/{*something}", "/customer//////99", "something", "/99");
checkCapture("/customer///{*something}", "/customer//////99", "something", "/99");
checkCapture("/customer/{*something}", "/customer", "something", "");
checkCapture("/{*something}", "", "something", "");
}
@Test
public void multipleSelectorsInPattern() {
checkMatches("///abc","/abc");
checkMatches("//","/");
checkMatches("abc","abc");
checkMatches("///abc//d/e","/abc/d/e");
checkMatches("///abc//{def}//////xyz","/abc/foo/xyz");
}
@Test
public void multipleSelectorsInPath() {
checkMatches("/abc","////abc");
checkMatches("/","//");
checkMatches("/abc//def///ghi","/abc/def/ghi");
}
@Test
public void multipleSelectorsInPatternAndPath() {
checkMatches("///one///two///three","//one/////two///////three");
checkMatches("//one//two//three","/one/////two/three");
checkCapture("///{foo}///bar","/one/bar","foo","one");
}
@Test
public void wildcards() {
checkMatches("/*/bar", "/foo/bar");
checkNoMatch("/*/bar", "/foo/baz");
checkMatches("/f*/bar", "/foo/bar");
checkMatches("/*/bar", "/foo/bar");
checkMatches("/a*b*c*d/bar", "/abcd/bar");
checkMatches("*a*", "testa");
checkMatches("a/*","a/");
checkMatches("a/*","a/a");
checkNoMatch("a/*","a/a/");
}
@Test
public void trailingSeparators() {
checkNoMatch("aaa/", "aaa");
}
@Test
public void constrainedMatches() {
checkCapture("{foo:[0-9]*}", "123", "foo", "123");
checkNoMatch("{foo:[0-9]*}", "abc");
checkNoMatch("/{foo:[0-9]*}", "abc");
checkCapture("/*/{foo:....}/**","/foo/barg/foo","foo","barg");
checkCapture("/*/{foo:....}/**","/foo/barg/abc/def/ghi","foo","barg");
checkNoMatch("{foo:....}", "99");
checkMatches("{foo:..}", "99");
checkCapture("/{abc:\\{\\}}","/{}","abc","{}");
checkCapture("/{abc:\\[\\]}","/[]","abc","[]");
checkCapture("/{abc:\\\\\\\\}","/\\\\"); // this is fun...
}
@Test
public void antPathMatcherTests() {
// test exact matching
checkMatches("test", "test");
checkMatches("/test", "/test");
checkMatches("http://example.org", "http://example.org");
checkNoMatch("/test.jpg", "test.jpg");
checkNoMatch("test", "/test");
checkNoMatch("/test", "test");
// test matching with ?'s
checkMatches("t?st", "test");
checkMatches("??st", "test");
checkMatches("tes?", "test");
checkMatches("te??", "test");
checkMatches("?es?", "test");
checkNoMatch("tes?", "tes");
checkNoMatch("tes?", "testt");
checkNoMatch("tes?", "tsst");
// test matching with *'s
checkMatches("*", "test");
checkMatches("test*", "test");
checkMatches("test*", "testTest");
checkMatches("test/*", "test/Test");
checkMatches("test/*", "test/t");
checkMatches("test/*", "test/");
checkMatches("*test*", "AnothertestTest");
checkMatches("*test", "Anothertest");
checkMatches("*.*", "test.");
checkMatches("*.*", "test.test");
checkMatches("*.*", "test.test.test");
checkMatches("test*aaa", "testblaaaa");
checkNoMatch("test*", "tst");
checkNoMatch("test*", "tsttest");
checkNoMatch("test*", "test/");
checkNoMatch("test*", "test/t");
checkNoMatch("test/*", "test");
checkNoMatch("*test*", "tsttst");
checkNoMatch("*test", "tsttst");
checkNoMatch("*.*", "tsttst");
checkNoMatch("test*aaa", "test");
checkNoMatch("test*aaa", "testblaaab");
// test matching with ?'s and /'s
checkMatches("/?", "/a");
checkMatches("/?/a", "/a/a");
checkMatches("/a/?", "/a/b");
checkMatches("/??/a", "/aa/a");
checkMatches("/a/??", "/a/bb");
checkMatches("/?", "/a");
checkMatches("/**", "");
checkMatches("/books/**", "/books");
checkMatches("/books////**", "/books");
checkMatches("/books////**", "/books////");
checkMatches("/**", "/testing/testing");
checkMatches("/*/**", "/testing/testing");
checkMatches("/bla*bla/test", "/blaXXXbla/test");
checkMatches("/*bla/test", "/XXXbla/test");
checkNoMatch("/bla*bla/test", "/blaXXXbl/test");
checkNoMatch("/*bla/test", "XXXblab/test");
checkNoMatch("/*bla/test", "XXXbl/test");
checkNoMatch("/????", "/bala/bla");
checkMatches("/foo/bar/**", "/foo/bar/");
checkMatches("/{bla}.html", "/testing.html");
checkCapture("/{bla}.*", "/testing.html", "bla", "testing");
}
@Test
public void matchStart() {
checkStartMatches("test/{a}_{b}/foo", "test/a_b");
checkStartMatches("test/?/abc", "test/a");
checkStartMatches("test/{*foobar}", "test/");
checkStartMatches("test/*/bar", "test/a");
checkStartMatches("test/{foo}/bar", "test/abc");
checkStartMatches("test//foo", "test//");
checkStartMatches("test/foo", "test/");
checkStartMatches("test/*", "test/");
checkStartMatches("test", "test");
checkStartNoMatch("test", "tes");
checkStartMatches("test/", "test");
// test exact matching
checkStartMatches("test", "test");
checkStartMatches("/test", "/test");
checkStartNoMatch("/test.jpg", "test.jpg");
checkStartNoMatch("test", "/test");
checkStartNoMatch("/test", "test");
// test matching with ?'s
checkStartMatches("t?st", "test");
checkStartMatches("??st", "test");
checkStartMatches("tes?", "test");
checkStartMatches("te??", "test");
checkStartMatches("?es?", "test");
checkStartNoMatch("tes?", "tes");
checkStartNoMatch("tes?", "testt");
checkStartNoMatch("tes?", "tsst");
// test matching with *'s
checkStartMatches("*", "test");
checkStartMatches("test*", "test");
checkStartMatches("test*", "testTest");
checkStartMatches("test/*", "test/Test");
checkStartMatches("test/*", "test/t");
checkStartMatches("test/*", "test/");
checkStartMatches("*test*", "AnothertestTest");
checkStartMatches("*test", "Anothertest");
checkStartMatches("*.*", "test.");
checkStartMatches("*.*", "test.test");
checkStartMatches("*.*", "test.test.test");
checkStartMatches("test*aaa", "testblaaaa");
checkStartNoMatch("test*", "tst");
checkStartNoMatch("test*", "test/");
checkStartNoMatch("test*", "tsttest");
checkStartNoMatch("test*", "test/t");
checkStartMatches("test/*", "test");
checkStartMatches("test/t*.txt", "test");
checkStartNoMatch("*test*", "tsttst");
checkStartNoMatch("*test", "tsttst");
checkStartNoMatch("*.*", "tsttst");
checkStartNoMatch("test*aaa", "test");
checkStartNoMatch("test*aaa", "testblaaab");
// test matching with ?'s and /'s
checkStartMatches("/?", "/a");
checkStartMatches("/?/a", "/a/a");
checkStartMatches("/a/?", "/a/b");
checkStartMatches("/??/a", "/aa/a");
checkStartMatches("/a/??", "/a/bb");
checkStartMatches("/?", "/a");
checkStartMatches("/**", "/testing/testing");
checkStartMatches("/*/**", "/testing/testing");
checkStartMatches("test*/**", "test/");
checkStartMatches("test*/**", "test/t");
checkStartMatches("/bla*bla/test", "/blaXXXbla/test");
checkStartMatches("/*bla/test", "/XXXbla/test");
checkStartNoMatch("/bla*bla/test", "/blaXXXbl/test");
checkStartNoMatch("/*bla/test", "XXXblab/test");
checkStartNoMatch("/*bla/test", "XXXbl/test");
checkStartNoMatch("/????", "/bala/bla");
checkStartMatches("/*bla*/*/bla/**",
"/XXXblaXXXX/testing/bla/testing/testing/");
checkStartMatches("/*bla*/*/bla/*",
"/XXXblaXXXX/testing/bla/testing");
checkStartMatches("/*bla*/*/bla/**",
"/XXXblaXXXX/testing/bla/testing/testing");
checkStartMatches("/*bla*/*/bla/**",
"/XXXblaXXXX/testing/bla/testing/testing.jpg");
checkStartMatches("/abc/{foo}","/abc/def");
checkStartNoMatch("/abc/{foo}","/abc/def/");
checkStartMatches("/abc/{foo}/","/abc/def/");
checkStartNoMatch("/abc/{foo}/","/abc/def/ghi");
checkStartMatches("/abc/{foo}/","/abc/def");
checkStartMatches("", "");
checkStartMatches("", null);
checkStartMatches("/abc", null);
checkStartMatches(null, "");
checkStartMatches(null, null);
checkStartNoMatch(null, "/abc");
}
@Test
public void caseSensitivity() {
PathPatternParser pp = new PathPatternParser();
pp.setCaseSensitive(false);
PathPattern p = pp.parse("abc");
assertTrue(p.matches("AbC"));
assertFalse(p.matches("def"));
p = pp.parse("fOo");
assertTrue(p.matches("FoO"));
p = pp.parse("/fOo/bAr");
assertTrue(p.matches("/FoO/BaR"));
pp = new PathPatternParser();
pp.setCaseSensitive(true);
p = pp.parse("abc");
assertFalse(p.matches("AbC"));
p = pp.parse("fOo");
assertFalse(p.matches("FoO"));
p = pp.parse("/fOo/bAr");
assertFalse(p.matches("/FoO/BaR"));
p = pp.parse("/fOO/bAr");
assertTrue(p.matches("/fOO/bAr"));
pp = new PathPatternParser();
pp.setCaseSensitive(false);
p = pp.parse("{foo:[A-Z]*}");
assertTrue(p.matches("abc"));
assertTrue(p.matches("ABC"));
pp = new PathPatternParser();
pp.setCaseSensitive(true);
p = pp.parse("{foo:[A-Z]*}");
assertFalse(p.matches("abc"));
assertTrue(p.matches("ABC"));
pp = new PathPatternParser();
pp.setCaseSensitive(false);
p = pp.parse("ab?");
assertTrue(p.matches("AbC"));
p = pp.parse("fO?");
assertTrue(p.matches("FoO"));
p = pp.parse("/fO?/bA?");
assertTrue(p.matches("/FoO/BaR"));
assertFalse(p.matches("/bAr/fOo"));
pp = new PathPatternParser();
pp.setCaseSensitive(true);
p = pp.parse("ab?");
assertFalse(p.matches("AbC"));
p = pp.parse("fO?");
assertFalse(p.matches("FoO"));
p = pp.parse("/fO?/bA?");
assertFalse(p.matches("/FoO/BaR"));
p = pp.parse("/fO?/bA?");
assertTrue(p.matches("/fOO/bAr"));
pp = new PathPatternParser();
pp.setCaseSensitive(false);
p = pp.parse("{abc:[A-Z]*}_{def:[A-Z]*}");
assertTrue(p.matches("abc_abc"));
assertTrue(p.matches("ABC_aBc"));
pp = new PathPatternParser();
pp.setCaseSensitive(true);
p = pp.parse("{abc:[A-Z]*}_{def:[A-Z]*}");
assertFalse(p.matches("abc_abc"));
assertTrue(p.matches("ABC_ABC"));
pp = new PathPatternParser();
pp.setCaseSensitive(false);
p = pp.parse("*?a?*");
assertTrue(p.matches("bab"));
assertTrue(p.matches("bAb"));
pp = new PathPatternParser();
pp.setCaseSensitive(true);
p = pp.parse("*?A?*");
assertFalse(p.matches("bab"));
assertTrue(p.matches("bAb"));
}
@Test
public void alternativeDelimiter() {
try {
separator = '.';
// test exact matching
checkMatches("test", "test");
checkMatches(".test", ".test");
checkNoMatch(".test/jpg", "test/jpg");
checkNoMatch("test", ".test");
checkNoMatch(".test", "test");
// test matching with ?'s
checkMatches("t?st", "test");
checkMatches("??st", "test");
checkMatches("tes?", "test");
checkMatches("te??", "test");
checkMatches("?es?", "test");
checkNoMatch("tes?", "tes");
checkNoMatch("tes?", "testt");
checkNoMatch("tes?", "tsst");
// test matching with *'s
checkMatches("*", "test");
checkMatches("test*", "test");
checkMatches("test*", "testTest");
checkMatches("*test*", "AnothertestTest");
checkMatches("*test", "Anothertest");
checkMatches("*/*", "test/");
checkMatches("*/*", "test/test");
checkMatches("*/*", "test/test/test");
checkMatches("test*aaa", "testblaaaa");
checkNoMatch("test*", "tst");
checkNoMatch("test*", "tsttest");
checkNoMatch("*test*", "tsttst");
checkNoMatch("*test", "tsttst");
checkNoMatch("*/*", "tsttst");
checkNoMatch("test*aaa", "test");
checkNoMatch("test*aaa", "testblaaab");
// test matching with ?'s and .'s
checkMatches(".?", ".a");
checkMatches(".?.a", ".a.a");
checkMatches(".a.?", ".a.b");
checkMatches(".??.a", ".aa.a");
checkMatches(".a.??", ".a.bb");
checkMatches(".?", ".a");
// test matching with **'s
checkMatches(".**", ".testing.testing");
checkMatches(".*.**", ".testing.testing");
checkMatches(".bla*bla.test", ".blaXXXbla.test");
checkMatches(".*bla.test", ".XXXbla.test");
checkNoMatch(".bla*bla.test", ".blaXXXbl.test");
checkNoMatch(".*bla.test", "XXXblab.test");
checkNoMatch(".*bla.test", "XXXbl.test");
}
finally {
separator = PathPatternParser.DEFAULT_SEPARATOR;
}
}
@Test
public void extractPathWithinPattern() throws Exception {
checkExtractPathWithinPattern("/welcome*/", "/welcome/","welcome");
checkExtractPathWithinPattern("/docs/commit.html","/docs/commit.html","");
checkExtractPathWithinPattern("/docs/*","/docs/cvs/commit","cvs/commit");
checkExtractPathWithinPattern("/docs/cvs/*.html","/docs/cvs/commit.html","commit.html");
checkExtractPathWithinPattern("/docs/**","/docs/cvs/commit","cvs/commit");
checkExtractPathWithinPattern("/doo/{*foobar}","/doo/customer.html","customer.html");
checkExtractPathWithinPattern("/doo/{*foobar}","/doo/daa/customer.html","daa/customer.html");
checkExtractPathWithinPattern("/*.html","/commit.html","commit.html");
checkExtractPathWithinPattern("/docs/*/*/*/*","/docs/cvs/other/commit.html","cvs/other/commit.html");
checkExtractPathWithinPattern("/d?cs/**","/docs/cvs/commit","docs/cvs/commit");
checkExtractPathWithinPattern("/docs/c?s/*.html","/docs/cvs/commit.html","cvs/commit.html");
checkExtractPathWithinPattern("/d?cs/*/*.html","/docs/cvs/commit.html","docs/cvs/commit.html");
checkExtractPathWithinPattern("/a/b/c*d*/*.html","/a/b/cod/foo.html","cod/foo.html");
checkExtractPathWithinPattern("a/{foo}/b/{bar}","a/c/b/d","c/b/d");
checkExtractPathWithinPattern("a/{foo}_{bar}/d/e","a/b_c/d/e","b_c/d/e");
checkExtractPathWithinPattern("aaa//*///ccc///ddd","aaa/bbb/ccc/ddd","bbb/ccc/ddd");
checkExtractPathWithinPattern("aaa/*/ccc/ddd","aaa//bbb//ccc/ddd","bbb/ccc/ddd");
checkExtractPathWithinPattern("aaa//*///ccc///ddd","aaa//bbb//ccc/ddd","bbb/ccc/ddd");
checkExtractPathWithinPattern("aaa//*///ccc///ddd","aaa/////bbb//ccc/ddd","bbb/ccc/ddd");
checkExtractPathWithinPattern("aaa/c*/ddd/","aaa/ccc///ddd///","ccc/ddd");
checkExtractPathWithinPattern("", "", "");
checkExtractPathWithinPattern("/", "", "");
checkExtractPathWithinPattern("", "/", "");
checkExtractPathWithinPattern("//", "", "");
checkExtractPathWithinPattern("", "//", "");
checkExtractPathWithinPattern("//", "//", "");
checkExtractPathWithinPattern("//", "/", "");
checkExtractPathWithinPattern("/", "//", "");
}
@Test
public void extractUriTemplateVariables() throws Exception {
checkCapture("/hotels/{hotel}", "/hotels/1", "hotel","1");
checkCapture("/h?tels/{hotel}","/hotels/1","hotel","1");
checkCapture("/hotels/{hotel}/bookings/{booking}","/hotels/1/bookings/2","hotel","1","booking","2");
checkCapture("/*/hotels/*/{hotel}","/foo/hotels/bar/1","hotel","1");
checkCapture("/{page}.html","/42.html","page","42");
checkCapture("/{page}.*","/42.html","page","42");
checkCapture("/A-{B}-C","/A-b-C","B","b");
checkCapture("/{name}.{extension}","/test.html","name","test","extension","html");
try {
checkCapture("/{one}/", "//", "one", "");
fail("Expected exception");
} catch (IllegalStateException e) {
assertEquals("Pattern \"/{one}/\" is not a match for \"//\"",e.getMessage());
}
try {
checkCapture("", "/abc");
fail("Expected exception");
} catch (IllegalStateException e) {
assertEquals("Pattern \"\" is not a match for \"/abc\"",e.getMessage());
}
assertEquals(0,checkCapture("", "").size());
checkCapture("{id}", "99", "id", "99");
checkCapture("/customer/{customerId}", "/customer/78", "customerId", "78");
checkCapture("/customer/{customerId}/banana", "/customer/42/banana", "customerId",
"42");
checkCapture("{id}/{id2}", "99/98", "id", "99", "id2", "98");
checkCapture("/foo/{bar}/boo/{baz}", "/foo/plum/boo/apple", "bar", "plum", "baz",
"apple");
checkCapture("/{bla}.*", "/testing.html", "bla", "testing");
Map<String,String> extracted = checkCapture("/abc","/abc");
assertEquals(0,extracted.size());
}
@Test
public void extractUriTemplateVariablesRegex() {
PathPatternParser pp = new PathPatternParser();
PathPattern p = null;
p = pp.parse("{symbolicName:[\\w\\.]+}-{version:[\\w\\.]+}.jar");
Map<String, String> result = p.matchAndExtract("com.example-1.0.0.jar");
assertEquals("com.example", result.get("symbolicName"));
assertEquals("1.0.0", result.get("version"));
p = pp.parse("{symbolicName:[\\w\\.]+}-sources-{version:[\\w\\.]+}.jar");
result = p.matchAndExtract("com.example-sources-1.0.0.jar");
assertEquals("com.example", result.get("symbolicName"));
assertEquals("1.0.0", result.get("version"));
}
@Test
public void extractUriTemplateVarsRegexQualifiers() {
PathPatternParser pp = new PathPatternParser();
PathPattern p = pp.parse("{symbolicName:[\\p{L}\\.]+}-sources-{version:[\\p{N}\\.]+}.jar");
Map<String, String> result = p.matchAndExtract("com.example-sources-1.0.0.jar");
assertEquals("com.example", result.get("symbolicName"));
assertEquals("1.0.0", result.get("version"));
p = pp.parse("{symbolicName:[\\w\\.]+}-sources-{version:[\\d\\.]+}-{year:\\d{4}}{month:\\d{2}}{day:\\d{2}}.jar");
result = p.matchAndExtract("com.example-sources-1.0.0-20100220.jar");
assertEquals("com.example", result.get("symbolicName"));
assertEquals("1.0.0", result.get("version"));
assertEquals("2010", result.get("year"));
assertEquals("02", result.get("month"));
assertEquals("20", result.get("day"));
p = pp.parse("{symbolicName:[\\p{L}\\.]+}-sources-{version:[\\p{N}\\.\\{\\}]+}.jar");
result = p.matchAndExtract("com.example-sources-1.0.0.{12}.jar");
assertEquals("com.example", result.get("symbolicName"));
assertEquals("1.0.0.{12}", result.get("version"));
}
@Test
public void extractUriTemplateVarsRegexCapturingGroups() {
PathPatternParser pp = new PathPatternParser();
PathPattern pathMatcher = pp.parse("/web/{id:foo(bar)?}_{goo}");
exception.expect(IllegalArgumentException.class);
exception.expectMessage(containsString("The number of capturing groups in the pattern"));
pathMatcher.matchAndExtract("/web/foobar_goo");
}
@Rule
public final ExpectedException exception = ExpectedException.none();
@Test
public void combine() {
TestPathCombiner pathMatcher = new TestPathCombiner();
assertEquals("", pathMatcher.combine(null, null));
assertEquals("/hotels", pathMatcher.combine("/hotels", null));
assertEquals("/hotels", pathMatcher.combine(null, "/hotels"));
assertEquals("/hotels/booking", pathMatcher.combine("/hotels/*", "booking"));
assertEquals("/hotels/booking", pathMatcher.combine("/hotels/*", "/booking"));
assertEquals("/hotels/**/booking", pathMatcher.combine("/hotels/**", "booking"));
assertEquals("/hotels/**/booking", pathMatcher.combine("/hotels/**", "/booking"));
assertEquals("/hotels/booking", pathMatcher.combine("/hotels", "/booking"));
assertEquals("/hotels/booking", pathMatcher.combine("/hotels", "booking"));
assertEquals("/hotels/booking", pathMatcher.combine("/hotels/", "booking"));
assertEquals("/hotels/{hotel}", pathMatcher.combine("/hotels/*", "{hotel}"));
assertEquals("/hotels/**/{hotel}", pathMatcher.combine("/hotels/**", "{hotel}"));
assertEquals("/hotels/{hotel}", pathMatcher.combine("/hotels", "{hotel}"));
assertEquals("/hotels/{hotel}.*", pathMatcher.combine("/hotels", "{hotel}.*"));
assertEquals("/hotels/*/booking/{booking}",
pathMatcher.combine("/hotels/*/booking", "{booking}"));
assertEquals("/hotel.html", pathMatcher.combine("/*.html", "/hotel.html"));
assertEquals("/hotel.html", pathMatcher.combine("/*.html", "/hotel"));
assertEquals("/hotel.html", pathMatcher.combine("/*.html", "/hotel.*"));
// TODO this seems rather bogus, should we eagerly show an error?
assertEquals("/d/e/f/hotel.html", pathMatcher.combine("/a/b/c/*.html", "/d/e/f/hotel.*"));
assertEquals("/*.html", pathMatcher.combine("/**", "/*.html"));
assertEquals("/*.html", pathMatcher.combine("/*", "/*.html"));
assertEquals("/*.html", pathMatcher.combine("/*.*", "/*.html"));
assertEquals("/{foo}/bar", pathMatcher.combine("/{foo}", "/bar")); // SPR-8858
assertEquals("/user/user", pathMatcher.combine("/user", "/user")); // SPR-7970
assertEquals("/{foo:.*[^0-9].*}/edit/",
pathMatcher.combine("/{foo:.*[^0-9].*}", "/edit/")); // SPR-10062
assertEquals("/1.0/foo/test", pathMatcher.combine("/1.0", "/foo/test"));
// SPR-10554
assertEquals("/hotel", pathMatcher.combine("/", "/hotel")); // SPR-12975
assertEquals("/hotel/booking", pathMatcher.combine("/hotel/", "/booking")); // SPR-12975
assertEquals("",pathMatcher.combine(null, null));
assertEquals("",pathMatcher.combine(null, ""));
assertEquals("",pathMatcher.combine("",null));
assertEquals("",pathMatcher.combine(null, null));
assertEquals("",pathMatcher.combine("", ""));
assertEquals("/hotel",pathMatcher.combine("", "/hotel"));
assertEquals("/hotel",pathMatcher.combine("/hotel", null));
assertEquals("/hotel",pathMatcher.combine("/hotel", ""));
// TODO Do we need special handling when patterns contain multiple dots?
}
@Test
public void combineWithTwoFileExtensionPatterns() {
TestPathCombiner pathMatcher = new TestPathCombiner();
exception.expect(IllegalArgumentException.class);
pathMatcher.combine("/*.html", "/*.txt");
}
@Test
public void patternComparator() {
Comparator<PathPattern> comparator = new PatternComparatorConsideringPath(
"/hotels/new");
assertEquals(0, comparator.compare(null, null));
assertEquals(1, comparator.compare(null, parse("/hotels/new")));
assertEquals(-1, comparator.compare(parse("/hotels/new"), null));
assertEquals(0, comparator.compare(parse("/hotels/new"), parse("/hotels/new")));
assertEquals(-1, comparator.compare(parse("/hotels/new"), parse("/hotels/*")));
assertEquals(1, comparator.compare(parse("/hotels/*"), parse("/hotels/new")));
assertEquals(0, comparator.compare(parse("/hotels/*"), parse("/hotels/*")));
assertEquals(-1,
comparator.compare(parse("/hotels/new"), parse("/hotels/{hotel}")));
assertEquals(1,
comparator.compare(parse("/hotels/{hotel}"), parse("/hotels/new")));
assertEquals(0,
comparator.compare(parse("/hotels/{hotel}"), parse("/hotels/{hotel}")));
assertEquals(-1, comparator.compare(parse("/hotels/{hotel}/booking"),
parse("/hotels/{hotel}/bookings/{booking}")));
assertEquals(1, comparator.compare(parse("/hotels/{hotel}/bookings/{booking}"),
parse("/hotels/{hotel}/booking")));
assertEquals(-1,
comparator.compare(
parse("/hotels/{hotel}/bookings/{booking}/cutomers/{customer}"),
parse("/**")));
assertEquals(1, comparator.compare(parse("/**"),
parse("/hotels/{hotel}/bookings/{booking}/cutomers/{customer}")));
assertEquals(0, comparator.compare(parse("/**"), parse("/**")));
assertEquals(-1,
comparator.compare(parse("/hotels/{hotel}"), parse("/hotels/*")));
assertEquals(1, comparator.compare(parse("/hotels/*"), parse("/hotels/{hotel}")));
assertEquals(-1, comparator.compare(parse("/hotels/*"), parse("/hotels/*/**")));
assertEquals(1, comparator.compare(parse("/hotels/*/**"), parse("/hotels/*")));
assertEquals(-1,
comparator.compare(parse("/hotels/new"), parse("/hotels/new.*")));
// SPR-6741
assertEquals(-1,
comparator.compare(
parse("/hotels/{hotel}/bookings/{booking}/cutomers/{customer}"),
parse("/hotels/**")));
assertEquals(1, comparator.compare(parse("/hotels/**"),
parse("/hotels/{hotel}/bookings/{booking}/cutomers/{customer}")));
assertEquals(1, comparator.compare(parse("/hotels/foo/bar/**"),
parse("/hotels/{hotel}")));
assertEquals(-1, comparator.compare(parse("/hotels/{hotel}"),
parse("/hotels/foo/bar/**")));
// SPR-8683
assertEquals(1, comparator.compare(parse("/**"), parse("/hotels/{hotel}")));
// longer is better
assertEquals(1, comparator.compare(parse("/hotels"), parse("/hotels2")));
// SPR-13139
assertEquals(-1, comparator.compare(parse("*"), parse("*/**")));
assertEquals(1, comparator.compare(parse("*/**"), parse("*")));
}
@Test
public void pathPatternComparator() {
PathPatternComparator ppc = new PathPatternComparator();
assertEquals(0,ppc.compare(null, null));
assertEquals(1,ppc.compare(null, parse("")));
assertEquals(-1,ppc.compare(parse(""), null));
assertEquals(0,ppc.compare(parse(""), parse("")));
}
@Test
public void patternCompareTo() {
PathPatternParser p = new PathPatternParser();
PathPattern pp = p.parse("/abc");
assertEquals(-1,pp.compareTo(null));
}
@Test
public void patternComparatorSort() {
Comparator<PathPattern> comparator = new PatternComparatorConsideringPath(
"/hotels/new");
List<PathPattern> paths = new ArrayList<>(3);
PathPatternParser pp = new PathPatternParser();
paths.add(null);
paths.add(pp.parse("/hotels/new"));
Collections.sort(paths, comparator);
assertEquals("/hotels/new", paths.get(0).getPatternString());
assertNull(paths.get(1));
paths.clear();
paths.add(pp.parse("/hotels/new"));
paths.add(null);
Collections.sort(paths, comparator);
assertEquals("/hotels/new", paths.get(0).getPatternString());
assertNull(paths.get(1));
paths.clear();
paths.add(pp.parse("/hotels/*"));
paths.add(pp.parse("/hotels/new"));
Collections.sort(paths, comparator);
assertEquals("/hotels/new", paths.get(0).getPatternString());
assertEquals("/hotels/*", paths.get(1).getPatternString());
paths.clear();
paths.add(pp.parse("/hotels/new"));
paths.add(pp.parse("/hotels/*"));
Collections.sort(paths, comparator);
assertEquals("/hotels/new", paths.get(0).getPatternString());
assertEquals("/hotels/*", paths.get(1).getPatternString());
paths.clear();
paths.add(pp.parse("/hotels/**"));
paths.add(pp.parse("/hotels/*"));
Collections.sort(paths, comparator);
assertEquals("/hotels/*", paths.get(0).getPatternString());
assertEquals("/hotels/**", paths.get(1).getPatternString());
paths.clear();
paths.add(pp.parse("/hotels/*"));
paths.add(pp.parse("/hotels/**"));
Collections.sort(paths, comparator);
assertEquals("/hotels/*", paths.get(0).getPatternString());
assertEquals("/hotels/**", paths.get(1).getPatternString());
paths.clear();
paths.add(pp.parse("/hotels/{hotel}"));
paths.add(pp.parse("/hotels/new"));
Collections.sort(paths, comparator);
assertEquals("/hotels/new", paths.get(0).getPatternString());
assertEquals("/hotels/{hotel}", paths.get(1).getPatternString());
paths.clear();
paths.add(pp.parse("/hotels/new"));
paths.add(pp.parse("/hotels/{hotel}"));
Collections.sort(paths, comparator);
assertEquals("/hotels/new", paths.get(0).getPatternString());
assertEquals("/hotels/{hotel}", paths.get(1).getPatternString());
paths.clear();
paths.add(pp.parse("/hotels/*"));
paths.add(pp.parse("/hotels/{hotel}"));
paths.add(pp.parse("/hotels/new"));
Collections.sort(paths, comparator);
assertEquals("/hotels/new", paths.get(0).getPatternString());
assertEquals("/hotels/{hotel}", paths.get(1).getPatternString());
assertEquals("/hotels/*", paths.get(2).getPatternString());
paths.clear();
paths.add(pp.parse("/hotels/ne*"));
paths.add(pp.parse("/hotels/n*"));
Collections.shuffle(paths);
Collections.sort(paths, comparator);
assertEquals("/hotels/ne*", paths.get(0).getPatternString());
assertEquals("/hotels/n*", paths.get(1).getPatternString());
paths.clear();
// comparator = new PatternComparatorConsideringPath("/hotels/new.html");
// paths.add(pp.parse("/hotels/new.*"));
// paths.add(pp.parse("/hotels/{hotel}"));
// Collections.shuffle(paths);
// Collections.sort(paths, comparator);
// assertEquals("/hotels/new.*", paths.get(0).toPatternString());
// assertEquals("/hotels/{hotel}", paths.get(1).toPatternString());
// paths.clear();
comparator = new PatternComparatorConsideringPath("/web/endUser/action/login.html");
paths.add(pp.parse("/*/login.*"));
paths.add(pp.parse("/*/endUser/action/login.*"));
Collections.sort(paths, comparator);
assertEquals("/*/endUser/action/login.*", paths.get(0).getPatternString());
assertEquals("/*/login.*", paths.get(1).getPatternString());
paths.clear();
}
@Test // SPR-13286
public void caseInsensitive() {
PathPatternParser pp = new PathPatternParser();
pp.setCaseSensitive(false);
PathPattern p = pp.parse("/group/{groupName}/members");
assertTrue(p.matches("/group/sales/members"));
assertTrue(p.matches("/Group/Sales/Members"));
assertTrue(p.matches("/group/Sales/members"));
}
@Test
public void patternmessage() {
PatternMessage[] values = PatternMessage.values();
assertNotNull(values);
for (PatternMessage pm: values) {
String name = pm.toString();
assertEquals(pm.ordinal(),PatternMessage.valueOf(name).ordinal());
}
}
private PathPattern parse(String path) {
PathPatternParser pp = new PathPatternParser();
return pp.parse(path);
}
private char separator = PathPatternParser.DEFAULT_SEPARATOR;
private void checkMatches(String uriTemplate, String path) {
PathPatternParser parser = (separator == PathPatternParser.DEFAULT_SEPARATOR
? new PathPatternParser() : new PathPatternParser(separator));
PathPattern p = parser.parse(uriTemplate);
assertTrue(p.matches(path));
}
private void checkStartNoMatch(String uriTemplate, String path) {
PathPatternParser p = new PathPatternParser();
PathPattern pattern = p.parse(uriTemplate);
assertFalse(pattern.matchStart(path));
}
private void checkStartMatches(String uriTemplate, String path) {
PathPatternParser p = new PathPatternParser();
PathPattern pattern = p.parse(uriTemplate);
assertTrue(pattern.matchStart(path));
}
private void checkNoMatch(String uriTemplate, String path) {
PathPatternParser p = new PathPatternParser();
PathPattern pattern = p.parse(uriTemplate);
assertFalse(pattern.matches(path));
}
private Map<String,String> checkCapture(String uriTemplate, String path, String... keyValues) {
PathPatternParser parser = new PathPatternParser();
PathPattern pattern = parser.parse(uriTemplate);
Map<String, String> matchResults = pattern.matchAndExtract(path);
Map<String, String> expectedKeyValues = new HashMap<>();
if (keyValues != null) {
for (int i = 0; i < keyValues.length; i += 2) {
expectedKeyValues.put(keyValues[i], keyValues[i + 1]);
}
}
Map<String, String> capturedVariables = matchResults;
for (Map.Entry<String, String> me : expectedKeyValues.entrySet()) {
String value = capturedVariables.get(me.getKey());
if (value == null) {
fail("Did not find key '" + me.getKey() + "' in captured variables: "
+ capturedVariables);
}
if (!value.equals(me.getValue())) {
fail("Expected value '" + me.getValue() + "' for key '" + me.getKey()
+ "' but was '" + value + "'");
}
}
return capturedVariables;
}
private void checkExtractPathWithinPattern(String pattern, String path, String expected) {
PathPatternParser ppp = new PathPatternParser();
PathPattern pp = ppp.parse(pattern);
String s = pp.extractPathWithinPattern(path);
assertEquals(expected,s);
}
static class TestPathCombiner {
PathPatternParser pp = new PathPatternParser();
public String combine(String string1, String string2) {
PathPattern pattern1 = pp.parse(string1);
return pattern1.combine(string2);
}
}
}

View File

@ -0,0 +1,465 @@
/*
* 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.Collections;
import java.util.List;
import java.util.Map;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Exercise the {@link PathPatternParser}.
*
* @author Andy Clement
*/
public class PathPatternParserTests {
private PathPattern p;
@Test
public void basicPatterns() {
checkStructure("/");
checkStructure("/foo");
checkStructure("foo");
checkStructure("foo/");
checkStructure("/foo/");
checkStructure("//");
}
@Test
public void singleCharWildcardPatterns() {
p = checkStructure("?");
assertPathElements(p , SingleCharWildcardedPathElement.class);
checkStructure("/?/");
checkStructure("//?abc?/");
}
@Test
public void multiwildcardPattern() {
p = checkStructure("/**");
assertPathElements(p,WildcardTheRestPathElement.class);
p = checkStructure("/**acb"); // this is not double wildcard use, it is / then **acb (an odd, unnecessary use of double *)
assertPathElements(p,SeparatorPathElement.class, RegexPathElement.class);
}
@Test
public void toStringTests() {
assertEquals("CaptureTheRest(/{*foobar})", checkStructure("/{*foobar}").toChainString());
assertEquals("CaptureVariable({foobar})", checkStructure("{foobar}").toChainString());
assertEquals("Literal(abc)", checkStructure("abc").toChainString());
assertEquals("Regex({a}_*_{b})", checkStructure("{a}_*_{b}").toChainString());
assertEquals("Separator(/)", checkStructure("/").toChainString());
assertEquals("SingleCharWildcarding(?a?b?c)", checkStructure("?a?b?c").toChainString());
assertEquals("Wildcard(*)", checkStructure("*").toChainString());
assertEquals("WildcardTheRest(/**)", checkStructure("/**").toChainString());
}
@Test
public void captureTheRestPatterns() {
checkError("/{*foobar}x{abc}", 10, PatternMessage.NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST);
p = checkStructure("{*foobar}");
assertPathElements(p, CaptureTheRestPathElement.class);
p = checkStructure("/{*foobar}");
assertPathElements(p, CaptureTheRestPathElement.class);
checkError("/{*foobar}/", 10, PatternMessage.NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST);
checkError("/{*foobar}abc", 10, PatternMessage.NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST);
checkError("/{*f%obar}", 4, PatternMessage.ILLEGAL_CHARACTER_IN_CAPTURE_DESCRIPTOR);
checkError("/{*foobar}abc",10,PatternMessage.NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST);
checkError("/{f*oobar}",3,PatternMessage.ILLEGAL_CHARACTER_IN_CAPTURE_DESCRIPTOR);
checkError("/{*foobar}/abc",10,PatternMessage.NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST);
checkError("/{abc}{*foobar}",1,PatternMessage.CAPTURE_ALL_IS_STANDALONE_CONSTRUCT);
checkError("/{abc}{*foobar}{foo}",15,PatternMessage.NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST);
}
@Test
public void equalsAndHashcode() {
PathPatternParser caseInsensitiveParser = new PathPatternParser();
caseInsensitiveParser.setCaseSensitive(false);
PathPatternParser caseSensitiveParser = new PathPatternParser();
PathPattern pp1 = caseInsensitiveParser.parse("/abc");
PathPattern pp2 = caseInsensitiveParser.parse("/abc");
PathPattern pp3 = caseInsensitiveParser.parse("/def");
assertEquals(pp1,pp2);
assertEquals(pp1.hashCode(),pp2.hashCode());
assertNotEquals(pp1, pp3);
assertFalse(pp1.equals("abc"));
pp1 = caseInsensitiveParser.parse("/abc");
pp2 = caseSensitiveParser.parse("/abc");
assertFalse(pp1.equals(pp2));
assertNotEquals(pp1.hashCode(),pp2.hashCode());
PathPatternParser alternateSeparatorParser = new PathPatternParser(':');
pp1 = caseInsensitiveParser.parse("abc");
pp2 = alternateSeparatorParser.parse("abc");
assertFalse(pp1.equals(pp2));
assertNotEquals(pp1.hashCode(),pp2.hashCode());
}
@Test
public void regexPathElementPatterns() {
checkError("/{var:[^/]*}", 8, PatternMessage.MISSING_CLOSE_CAPTURE);
checkError("/{var:abc", 8, PatternMessage.MISSING_CLOSE_CAPTURE);
checkError("/{var:a{{1,2}}}", 6, PatternMessage.JDK_PATTERN_SYNTAX_EXCEPTION);
p = checkStructure("/{var:\\\\}");
assertEquals(CaptureVariablePathElement.class.getName(),p.getHeadSection().next.getClass().getName());
assertTrue(p.matches("/\\"));
p = checkStructure("/{var:\\/}");
assertEquals(CaptureVariablePathElement.class.getName(),p.getHeadSection().next.getClass().getName());
assertFalse(p.matches("/aaa"));
p = checkStructure("/{var:a{1,2}}",1);
assertEquals(CaptureVariablePathElement.class.getName(),p.getHeadSection().next.getClass().getName());
p = checkStructure("/{var:[^\\/]*}",1);
assertEquals(CaptureVariablePathElement.class.getName(),p.getHeadSection().next.getClass().getName());
Map<String, String> result = p.matchAndExtract("/foo");
assertEquals("foo",result.get("var"));
p = checkStructure("/{var:\\[*}",1);
assertEquals(CaptureVariablePathElement.class.getName(),p.getHeadSection().next.getClass().getName());
result = p.matchAndExtract("/[[[");
assertEquals("[[[",result.get("var"));
p = checkStructure("/{var:[\\{]*}",1);
assertEquals(CaptureVariablePathElement.class.getName(),p.getHeadSection().next.getClass().getName());
result = p.matchAndExtract("/{{{");
assertEquals("{{{",result.get("var"));
p = checkStructure("/{var:[\\}]*}",1);
assertEquals(CaptureVariablePathElement.class.getName(),p.getHeadSection().next.getClass().getName());
result = p.matchAndExtract("/}}}");
assertEquals("}}}",result.get("var"));
p = checkStructure("*");
assertEquals(WildcardPathElement.class.getName(),p.getHeadSection().getClass().getName());
checkStructure("/*");
checkStructure("/*/");
checkStructure("*/");
checkStructure("/*/");
p = checkStructure("/*a*/");
assertEquals(RegexPathElement.class.getName(),p.getHeadSection().next.getClass().getName());
p = checkStructure("*/");
assertEquals(WildcardPathElement.class.getName(),p.getHeadSection().getClass().getName());
checkError("{foo}_{foo}", 0, PatternMessage.ILLEGAL_DOUBLE_CAPTURE, "foo");
checkError("/{bar}/{bar}", 7, PatternMessage.ILLEGAL_DOUBLE_CAPTURE, "bar");
checkError("/{bar}/{bar}_{foo}", 7, PatternMessage.ILLEGAL_DOUBLE_CAPTURE, "bar");
p = checkStructure("{symbolicName:[\\p{L}\\.]+}-sources-{version:[\\p{N}\\.]+}.jar");
assertEquals(RegexPathElement.class.getName(),p.getHeadSection().getClass().getName());
}
@Test
public void completeCapturingPatterns() {
p = checkStructure("{foo}");
assertEquals(CaptureVariablePathElement.class.getName(),p.getHeadSection().getClass().getName());
checkStructure("/{foo}");
checkStructure("//{f}/");
checkStructure("/{foo}/{bar}/{wibble}");
}
@Test
public void completeCaptureWithConstraints() {
p = checkStructure("{foo:...}");
assertPathElements(p, CaptureVariablePathElement.class);
p = checkStructure("{foo:[0-9]*}");
assertPathElements(p, CaptureVariablePathElement.class);
checkError("{foo:}",5,PatternMessage.MISSING_REGEX_CONSTRAINT);
}
@Test
public void partialCapturingPatterns() {
p = checkStructure("{foo}abc");
assertEquals(RegexPathElement.class.getName(),p.getHeadSection().getClass().getName());
checkStructure("abc{foo}");
checkStructure("/abc{foo}");
checkStructure("{foo}def/");
checkStructure("/abc{foo}def/");
checkStructure("{foo}abc{bar}");
checkStructure("{foo}abc{bar}/");
checkStructure("/{foo}abc{bar}/");
}
@Test
public void illegalCapturePatterns() {
checkError("{abc/",4,PatternMessage.MISSING_CLOSE_CAPTURE);
checkError("{abc:}/",5,PatternMessage.MISSING_REGEX_CONSTRAINT);
checkError("{",1,PatternMessage.MISSING_CLOSE_CAPTURE);
checkError("{abc",4,PatternMessage.MISSING_CLOSE_CAPTURE);
checkError("{/}",1,PatternMessage.MISSING_CLOSE_CAPTURE);
checkError("//{",3,PatternMessage.MISSING_CLOSE_CAPTURE);
checkError("}",0,PatternMessage.MISSING_OPEN_CAPTURE);
checkError("/}",1,PatternMessage.MISSING_OPEN_CAPTURE);
checkError("def}",3,PatternMessage.MISSING_OPEN_CAPTURE);
checkError("//{/}",3,PatternMessage.MISSING_CLOSE_CAPTURE);
checkError("//{{/}",3,PatternMessage.ILLEGAL_NESTED_CAPTURE);
checkError("//{abc{/}",6,PatternMessage.ILLEGAL_NESTED_CAPTURE);
checkError("/{0abc}/abc",2,PatternMessage.ILLEGAL_CHARACTER_AT_START_OF_CAPTURE_DESCRIPTOR);
checkError("/{a?bc}/abc",3,PatternMessage.ILLEGAL_CHARACTER_IN_CAPTURE_DESCRIPTOR);
checkError("/{abc}_{abc}",1,PatternMessage.ILLEGAL_DOUBLE_CAPTURE);
checkError("/foobar/{abc}_{abc}",8,PatternMessage.ILLEGAL_DOUBLE_CAPTURE);
checkError("/foobar/{abc:..}_{abc:..}",8,PatternMessage.ILLEGAL_DOUBLE_CAPTURE);
PathPattern pp = parse("/{abc:foo(bar)}");
try {
pp.matchAndExtract("/foo");
fail("Should have raised exception");
} catch (IllegalArgumentException iae) {
assertEquals("No capture groups allowed in the constraint regex: foo(bar)",iae.getMessage());
}
try {
pp.matchAndExtract("/foobar");
fail("Should have raised exception");
} catch (IllegalArgumentException iae) {
assertEquals("No capture groups allowed in the constraint regex: foo(bar)",iae.getMessage());
}
}
@Test
public void badPatterns() {
// checkError("/{foo}{bar}/",6,PatternMessage.CANNOT_HAVE_ADJACENT_CAPTURES);
checkError("/{?}/",2,PatternMessage.ILLEGAL_CHARACTER_AT_START_OF_CAPTURE_DESCRIPTOR,"?");
checkError("/{a?b}/",3,PatternMessage.ILLEGAL_CHARACTER_IN_CAPTURE_DESCRIPTOR,"?");
checkError("/{%%$}",2,PatternMessage.ILLEGAL_CHARACTER_AT_START_OF_CAPTURE_DESCRIPTOR,"%");
checkError("/{ }",2,PatternMessage.ILLEGAL_CHARACTER_AT_START_OF_CAPTURE_DESCRIPTOR," ");
checkError("/{%:[0-9]*}",2,PatternMessage.ILLEGAL_CHARACTER_AT_START_OF_CAPTURE_DESCRIPTOR,"%");
}
@Test
public void patternPropertyGetCaptureCountTests() {
// Test all basic section types
assertEquals(1,parse("{foo}").getCapturedVariableCount());
assertEquals(0,parse("foo").getCapturedVariableCount());
assertEquals(1,parse("{*foobar}").getCapturedVariableCount());
assertEquals(1,parse("/{*foobar}").getCapturedVariableCount());
assertEquals(0,parse("/**").getCapturedVariableCount());
assertEquals(1,parse("{abc}asdf").getCapturedVariableCount());
assertEquals(1,parse("{abc}_*").getCapturedVariableCount());
assertEquals(2,parse("{abc}_{def}").getCapturedVariableCount());
assertEquals(0,parse("/").getCapturedVariableCount());
assertEquals(0,parse("a?b").getCapturedVariableCount());
assertEquals(0,parse("*").getCapturedVariableCount());
// Test on full templates
assertEquals(0,parse("/foo/bar").getCapturedVariableCount());
assertEquals(1,parse("/{foo}").getCapturedVariableCount());
assertEquals(2,parse("/{foo}/{bar}").getCapturedVariableCount());
assertEquals(4,parse("/{foo}/{bar}_{goo}_{wibble}/abc/bar").getCapturedVariableCount());
}
@Test
public void patternPropertyGetWildcardCountTests() {
// Test all basic section types
assertEquals(computeScore(1,0),parse("{foo}").getScore());
assertEquals(computeScore(0,0),parse("foo").getScore());
assertEquals(computeScore(0,0),parse("{*foobar}").getScore());
// assertEquals(1,parse("/**").getScore());
assertEquals(computeScore(1,0),parse("{abc}asdf").getScore());
assertEquals(computeScore(1,1),parse("{abc}_*").getScore());
assertEquals(computeScore(2,0),parse("{abc}_{def}").getScore());
assertEquals(computeScore(0,0),parse("/").getScore());
assertEquals(computeScore(0,0),parse("a?b").getScore()); // currently deliberate
assertEquals(computeScore(0,1),parse("*").getScore());
// Test on full templates
assertEquals(computeScore(0,0),parse("/foo/bar").getScore());
assertEquals(computeScore(1,0),parse("/{foo}").getScore());
assertEquals(computeScore(2,0),parse("/{foo}/{bar}").getScore());
assertEquals(computeScore(4,0),parse("/{foo}/{bar}_{goo}_{wibble}/abc/bar").getScore());
assertEquals(computeScore(4,3),parse("/{foo}/*/*_*/{bar}_{goo}_{wibble}/abc/bar").getScore());
}
@Test
public void multipleSeparatorPatterns() {
p = checkStructure("///aaa");
assertEquals(4,p.getNormalizedLength());
assertPathElements(p,SeparatorPathElement.class,LiteralPathElement.class);
p = checkStructure("///aaa////aaa/b");
assertEquals(10,p.getNormalizedLength());
assertPathElements(p,SeparatorPathElement.class, LiteralPathElement.class,
SeparatorPathElement.class, LiteralPathElement.class, SeparatorPathElement.class, LiteralPathElement.class);
p = checkStructure("/////**");
assertEquals(1,p.getNormalizedLength());
assertPathElements(p,WildcardTheRestPathElement.class);
}
@Test
public void patternPropertyGetLengthTests() {
// Test all basic section types
assertEquals(1,parse("{foo}").getNormalizedLength());
assertEquals(3,parse("foo").getNormalizedLength());
assertEquals(1,parse("{*foobar}").getNormalizedLength());
assertEquals(1,parse("/{*foobar}").getNormalizedLength());
assertEquals(1,parse("/**").getNormalizedLength());
assertEquals(5,parse("{abc}asdf").getNormalizedLength());
assertEquals(3,parse("{abc}_*").getNormalizedLength());
assertEquals(3,parse("{abc}_{def}").getNormalizedLength());
assertEquals(1,parse("/").getNormalizedLength());
assertEquals(3,parse("a?b").getNormalizedLength());
assertEquals(1,parse("*").getNormalizedLength());
// Test on full templates
assertEquals(8,parse("/foo/bar").getNormalizedLength());
assertEquals(2,parse("/{foo}").getNormalizedLength());
assertEquals(4,parse("/{foo}/{bar}").getNormalizedLength());
assertEquals(16,parse("/{foo}/{bar}_{goo}_{wibble}/abc/bar").getNormalizedLength());
}
@Test
public void compareTests() {
PathPattern p1,p2,p3;
// Based purely on number of captures
p1 = parse("{a}");
p2 = parse("{a}/{b}");
p3 = parse("{a}/{b}/{c}");
assertEquals(-1,p1.compareTo(p2)); // Based on number of captures
List<PathPattern> patterns = new ArrayList<>();
patterns.add(p2);
patterns.add(p3);
patterns.add(p1);
Collections.sort(patterns,new PathPatternComparator());
assertEquals(p1,patterns.get(0));
// Based purely on length
p1 = parse("/a/b/c");
p2 = parse("/a/boo/c/doo");
p3 = parse("/asdjflaksjdfjasdf");
assertEquals(1,p1.compareTo(p2));
patterns = new ArrayList<>();
patterns.add(p2);
patterns.add(p3);
patterns.add(p1);
Collections.sort(patterns,new PathPatternComparator());
assertEquals(p3,patterns.get(0));
// Based purely on 'wildness'
p1 = parse("/*");
p2 = parse("/*/*");
p3 = parse("/*/*/*_*");
assertEquals(-1,p1.compareTo(p2));
patterns = new ArrayList<>();
patterns.add(p2);
patterns.add(p3);
patterns.add(p1);
Collections.sort(patterns,new PathPatternComparator());
assertEquals(p1,patterns.get(0));
// Based purely on catchAll
p1 = parse("{*foobar}");
p2 = parse("{*goo}");
assertEquals(0,p1.compareTo(p2));
p1 = parse("/{*foobar}");
p2 = parse("/abc/{*ww}");
assertEquals(+1,p1.compareTo(p2));
assertEquals(-1,p2.compareTo(p1));
p3 = parse("/this/that/theother");
assertTrue(p1.isCatchAll());
assertTrue(p2.isCatchAll());
assertFalse(p3.isCatchAll());
patterns = new ArrayList<>();
patterns.add(p2);
patterns.add(p3);
patterns.add(p1);
Collections.sort(patterns,new PathPatternComparator());
assertEquals(p3,patterns.get(0));
assertEquals(p2,patterns.get(1));
patterns = new ArrayList<>();
patterns.add(parse("/abc"));
patterns.add(null);
patterns.add(parse("/def"));
Collections.sort(patterns,new PathPatternComparator());
assertNull(patterns.get(2));
}
// ---
private PathPattern parse(String pattern) {
PathPatternParser patternParser = new PathPatternParser();
return patternParser.parse(pattern);
}
/**
* Verify the parsed chain of sections matches the original pattern and the separator count
* that has been determined is correct.
*/
private PathPattern checkStructure(String pattern) {
int count = 0;
for (int i=0;i<pattern.length();i++) {
if (pattern.charAt(i)=='/') {
// if (peekDoubleWildcard(pattern,i)) {
// // it is /**
// i+=2;
// } else {
count++;
// }
}
}
return checkStructure(pattern,count);
}
private PathPattern checkStructure(String pattern, int expectedSeparatorCount) {
p = parse(pattern);
assertEquals(pattern,p.getPatternString());
// assertEquals(expectedSeparatorCount,p.getSeparatorCount());
return p;
}
private void checkError(String pattern, int expectedPos, PatternMessage expectedMessage, String... expectedInserts) {
try {
p = parse(pattern);
fail("Expected to fail");
} catch (PatternParseException ppe) {
// System.out.println(ppe.toDetailedString());
assertEquals(ppe.toDetailedString(), expectedPos, ppe.getPosition());
assertEquals(ppe.toDetailedString(), expectedMessage, ppe.getMessageType());
if (expectedInserts.length!=0) {
assertEquals(ppe.getInserts().length,expectedInserts.length);
for (int i=0;i<expectedInserts.length;i++) {
assertEquals("Insert at position "+i+" is wrong",expectedInserts[i],ppe.getInserts()[i]);
}
}
}
}
@SafeVarargs
private final void assertPathElements(PathPattern p, Class<? extends PathElement>... sectionClasses) {
PathElement head = p.getHeadSection();
for (int i=0;i<sectionClasses.length;i++) {
if (head == null) {
fail("Ran out of data in parsed pattern. Pattern is: "+p.toChainString());
}
assertEquals("Not expected section type. Pattern is: "+p.toChainString(),sectionClasses[i].getSimpleName(),head.getClass().getSimpleName());
head = head.next;
}
}
// Mirrors the score computation logic in PathPattern
private int computeScore(int capturedVariableCount, int wildcardCount) {
return capturedVariableCount+wildcardCount*100;
}
}

View File

@ -31,11 +31,11 @@ import reactor.core.publisher.Mono;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.util.PathMatcher;
import org.springframework.web.reactive.function.BodyExtractor;
import org.springframework.web.server.WebSession;
import org.springframework.web.util.ParsingPathMatcher;
/**
* Implementations of {@link RequestPredicate} that implement various useful request matching operations, such as
@ -46,7 +46,7 @@ import org.springframework.web.server.WebSession;
*/
public abstract class RequestPredicates {
private static final PathMatcher DEFAULT_PATH_MATCHER = new AntPathMatcher();
private static final PathMatcher DEFAULT_PATH_MATCHER = new ParsingPathMatcher();
/**
* Returns a {@code RequestPredicate} that always matches.

View File

@ -22,7 +22,6 @@ import reactor.core.publisher.Mono;
import org.springframework.context.support.ApplicationObjectSupport;
import org.springframework.core.Ordered;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.util.PathMatcher;
import org.springframework.web.cors.CorsConfiguration;
@ -35,6 +34,7 @@ import org.springframework.web.reactive.HandlerMapping;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebHandler;
import org.springframework.web.server.support.HttpRequestPathHelper;
import org.springframework.web.util.ParsingPathMatcher;
/**
* Abstract base class for {@link org.springframework.web.reactive.HandlerMapping}
@ -53,7 +53,7 @@ public abstract class AbstractHandlerMapping extends ApplicationObjectSupport im
private HttpRequestPathHelper pathHelper = new HttpRequestPathHelper();
private PathMatcher pathMatcher = new AntPathMatcher();
private PathMatcher pathMatcher = new ParsingPathMatcher();
private final UrlBasedCorsConfigurationSource globalCorsConfigSource = new UrlBasedCorsConfigurationSource();

View File

@ -33,11 +33,11 @@ import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.support.HttpRequestPathHelper;
import org.springframework.web.util.ParsingPathMatcher;
/**
* A central component to use to obtain the public URL path that clients should
@ -56,7 +56,7 @@ public class ResourceUrlProvider implements ApplicationListener<ContextRefreshed
private HttpRequestPathHelper urlPathHelper = new HttpRequestPathHelper();
private PathMatcher pathMatcher = new AntPathMatcher();
private PathMatcher pathMatcher = new ParsingPathMatcher();
private final Map<String, ResourceWebHandler> handlerMap = new LinkedHashMap<>();

View File

@ -27,11 +27,11 @@ import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.support.HttpRequestPathHelper;
import org.springframework.web.util.ParsingPathMatcher;
/**
* A logical disjunction (' || ') request condition that matches a request
@ -90,7 +90,7 @@ public final class PatternsRequestCondition extends AbstractRequestCondition<Pat
this.patterns = Collections.unmodifiableSet(prependLeadingSlash(patterns));
this.pathHelper = (pathHelper != null ? pathHelper : new HttpRequestPathHelper());
this.pathMatcher = (pathMatcher != null ? pathMatcher : new AntPathMatcher());
this.pathMatcher = (pathMatcher != null ? pathMatcher : new ParsingPathMatcher());
this.useSuffixPatternMatch = useSuffixPatternMatch;
this.useTrailingSlashMatch = useTrailingSlashMatch;
if (fileExtensions != null) {

View File

@ -74,7 +74,7 @@ public class SimpleUrlHandlerMappingTests {
testUrl("welcome.html", null, handlerMapping, null);
testUrl("/pathmatchingAA.html", mainController, handlerMapping, "pathmatchingAA.html");
testUrl("/pathmatchingA.html", null, handlerMapping, null);
testUrl("/administrator/pathmatching.html", mainController, handlerMapping, "pathmatching.html");
testUrl("/administrator/pathmatching.html", mainController, handlerMapping, "/administrator/pathmatching.html");
testUrl("/administrator/test/pathmatching.html", mainController, handlerMapping, "test/pathmatching.html");
testUrl("/administratort/pathmatching.html", null, handlerMapping, null);
testUrl("/administrator/another/bla.xml", mainController, handlerMapping, "/administrator/another/bla.xml");

View File

@ -98,9 +98,9 @@ public class PatternsRequestConditionTests {
@Test
public void matchSortPatterns() throws Exception {
PatternsRequestCondition condition = new PatternsRequestCondition("/**", "/foo/bar", "/foo/*");
PatternsRequestCondition condition = new PatternsRequestCondition("/*/*", "/foo/bar", "/foo/*");
PatternsRequestCondition match = condition.getMatchingCondition(createExchange("/foo/bar"));
PatternsRequestCondition expected = new PatternsRequestCondition("/foo/bar", "/foo/*", "/**");
PatternsRequestCondition expected = new PatternsRequestCondition("/foo/bar", "/foo/*", "/*/*");
assertEquals(expected, match);
}

View File

@ -33,7 +33,6 @@ import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse;
import org.springframework.stereotype.Controller;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.method.HandlerMethod;
@ -41,6 +40,7 @@ import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.adapter.DefaultServerWebExchange;
import org.springframework.web.server.session.MockWebSessionManager;
import org.springframework.web.server.session.WebSessionManager;
import org.springframework.web.util.ParsingPathMatcher;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@ -158,7 +158,7 @@ public class HandlerMethodMappingTests {
private static class MyHandlerMethodMapping extends AbstractHandlerMethodMapping<String> {
private PathMatcher pathMatcher = new AntPathMatcher();
private PathMatcher pathMatcher = new ParsingPathMatcher();
@Override
protected boolean isHandler(Class<?> beanType) {

View File

@ -1,23 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="mapping" class="org.springframework.web.reactive.handler.SimpleUrlHandlerMapping">
<property name="urlDecode" value="true" />
<property name="mappings">
<value>
welcome.html=mainController
/**/pathmatchingTest.html=mainController
/**/pathmatching??.html=mainController
/**/path??matching.html=mainController
/**/??path??matching.html=mainController
/**/*.jsp=mainController
/administrator/**/pathmatching.html=mainController
/administrator/**/testlast*=mainController
/*pathmatchingTest.html=mainController
/pathmatching??.html=mainController
/administrator/pathmatching.html=mainController
/administrator/*/pathmatching.html=mainController
/administrator/*/testlast*=mainController
/administrator/testing/longer/*=mainController
/??path??matching.html=mainController
/path??matching.html=mainController
/administrator/another/bla.xml=mainController
/administrator/testing/longer/**/**/**/**/**=mainController
/administrator/testing/longer2/**/**/bla/**=mainController
/*test*.jpeg=mainController
/*/test.jpeg=mainController
/outofpattern*yeah=mainController

View File

@ -0,0 +1,345 @@
/*
* Copyright 2002-2012 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.servlet;
import java.io.IOException;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.util.PathMatcher;
import org.springframework.util.StringUtils;
import org.springframework.web.context.support.ServletContextResource;
import org.springframework.web.util.ParsingPathMatcher;
/**
* Simple servlet that can expose an internal resource, including a
* default URL if the specified resource is not found. An alternative,
* for example, to trying and catching exceptions when using JSP include.
*
* <p>A further usage of this servlet is the ability to apply last-modified
* timestamps to quasi-static resources (typically JSPs). This can happen
* as bridge to parameter-specified resources, or as proxy for a specific
* target resource (or a list of specific target resources to combine).
*
* <p>A typical usage would map a URL like "/ResourceServlet" onto an instance
* of this servlet, and use the "JSP include" action to include this URL,
* with the "resource" parameter indicating the actual target path in the WAR.
*
* <p>The {@code defaultUrl} property can be set to the internal
* resource path of a default URL, to be rendered when the target resource
* is not found or not specified in the first place.
*
* <p>The "resource" parameter and the {@code defaultUrl} property can
* also specify a list of target resources to combine. Those resources will be
* included one by one to build the response. If last-modified determination
* is active, the newest timestamp among those files will be used.
*
* <p>The {@code allowedResources} property can be set to a URL
* pattern of resources that should be available via this servlet.
* If not set, any target resource can be requested, including resources
* in the WEB-INF directory!
*
* <p>If using this servlet for direct access rather than via includes,
* the {@code contentType} property should be specified to apply a
* proper content type. Note that a content type header in the target JSP will
* be ignored when including the resource via a RequestDispatcher include.
*
* <p>To apply last-modified timestamps for the target resource, set the
* {@code applyLastModified} property to true. This servlet will then
* return the file timestamp of the target resource as last-modified value,
* falling back to the startup time of this servlet if not retrievable.
*
* <p>Note that applying the last-modified timestamp in the above fashion
* just makes sense if the target resource does not generate content that
* depends on the HttpSession or cookies; it is just allowed to evaluate
* request parameters.
*
* <p>A typical case for such last-modified usage is a JSP that just makes
* minimal usage of basic means like includes or message resolution to
* build quasi-static content. Regenerating such content on every request
* is unnecessary; it can be cached as long as the file hasn't changed.
*
* <p>Note that this servlet will apply the last-modified timestamp if you
* tell it to do so: It's your decision whether the content of the target
* resource can be cached in such a fashion. Typical use cases are helper
* resources that are not fronted by a controller, like JavaScript files
* that are generated by a JSP (without depending on the HttpSession).
*
* @author Juergen Hoeller
* @author Rod Johnson
* @see #setDefaultUrl
* @see #setAllowedResources
* @see #setApplyLastModified
*/
@SuppressWarnings("serial")
public class ResourceServlet extends HttpServletBean {
/**
* Any number of these characters are considered delimiters
* between multiple resource paths in a single String value.
*/
public static final String RESOURCE_URL_DELIMITERS = ",; \t\n";
/**
* Name of the parameter that must contain the actual resource path.
*/
public static final String RESOURCE_PARAM_NAME = "resource";
private String defaultUrl;
private String allowedResources;
private String contentType;
private boolean applyLastModified = false;
private PathMatcher pathMatcher;
private long startupTime;
/**
* Set the URL within the current web application from which to
* include content if the requested path isn't found, or if none
* is specified in the first place.
* <p>If specifying multiple URLs, they will be included one by one
* to build the response. If last-modified determination is active,
* the newest timestamp among those files will be used.
* @see #setApplyLastModified
*/
public void setDefaultUrl(String defaultUrl) {
this.defaultUrl = defaultUrl;
}
/**
* Set allowed resources as URL pattern, e.g. "/WEB-INF/res/*.jsp",
* The parameter can be any Ant-style pattern parsable by AntPathMatcher.
* @see org.springframework.util.AntPathMatcher
*/
public void setAllowedResources(String allowedResources) {
this.allowedResources = allowedResources;
}
/**
* Set the content type of the target resource (typically a JSP).
* Default is none, which is appropriate when including resources.
* <p>For directly accessing resources, for example to leverage this
* servlet's last-modified support, specify a content type here.
* Note that a content type header in the target JSP will be ignored
* when including the resource via a RequestDispatcher include.
*/
public void setContentType(String contentType) {
this.contentType = contentType;
}
/**
* Set whether to apply the file timestamp of the target resource
* as last-modified value. Default is "false".
* <p>This is mainly intended for JSP targets that don't generate
* session-specific or database-driven content: Such files can be
* cached by the browser as long as the last-modified timestamp
* of the JSP file doesn't change.
* <p>This will only work correctly with expanded WAR files that
* allow access to the file timestamps. Else, the startup time
* of this servlet is returned.
*/
public void setApplyLastModified(boolean applyLastModified) {
this.applyLastModified = applyLastModified;
}
/**
* Remember the startup time, using no last-modified time before it.
*/
@Override
protected void initServletBean() {
this.pathMatcher = getPathMatcher();
this.startupTime = System.currentTimeMillis();
}
/**
* Return a PathMatcher to use for matching the "allowedResources" URL pattern.
* Default is AntPathMatcher.
* @see #setAllowedResources
* @see org.springframework.util.AntPathMatcher
*/
protected PathMatcher getPathMatcher() {
return new ParsingPathMatcher();
}
/**
* Determine the URL of the target resource and include it.
* @see #determineResourceUrl
*/
@Override
protected final void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// determine URL of resource to include
String resourceUrl = determineResourceUrl(request);
if (resourceUrl != null) {
try {
doInclude(request, response, resourceUrl);
}
catch (ServletException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Failed to include content of resource [" + resourceUrl + "]", ex);
}
// Try including default URL if appropriate.
if (!includeDefaultUrl(request, response)) {
throw ex;
}
}
catch (IOException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Failed to include content of resource [" + resourceUrl + "]", ex);
}
// Try including default URL if appropriate.
if (!includeDefaultUrl(request, response)) {
throw ex;
}
}
}
// no resource URL specified -> try to include default URL.
else if (!includeDefaultUrl(request, response)) {
throw new ServletException("No target resource URL found for request");
}
}
/**
* Determine the URL of the target resource of this request.
* <p>Default implementation returns the value of the "resource" parameter.
* Can be overridden in subclasses.
* @param request current HTTP request
* @return the URL of the target resource, or {@code null} if none found
* @see #RESOURCE_PARAM_NAME
*/
protected String determineResourceUrl(HttpServletRequest request) {
return request.getParameter(RESOURCE_PARAM_NAME);
}
/**
* Include the specified default URL, if appropriate.
* @param request current HTTP request
* @param response current HTTP response
* @return whether a default URL was included
* @throws ServletException if thrown by the RequestDispatcher
* @throws IOException if thrown by the RequestDispatcher
*/
private boolean includeDefaultUrl(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
if (this.defaultUrl == null) {
return false;
}
doInclude(request, response, this.defaultUrl);
return true;
}
/**
* Include the specified resource via the RequestDispatcher.
* @param request current HTTP request
* @param response current HTTP response
* @param resourceUrl the URL of the target resource
* @throws ServletException if thrown by the RequestDispatcher
* @throws IOException if thrown by the RequestDispatcher
*/
private void doInclude(HttpServletRequest request, HttpServletResponse response, String resourceUrl)
throws ServletException, IOException {
if (this.contentType != null) {
response.setContentType(this.contentType);
}
String[] resourceUrls =
StringUtils.tokenizeToStringArray(resourceUrl, RESOURCE_URL_DELIMITERS);
for (int i = 0; i < resourceUrls.length; i++) {
// check whether URL matches allowed resources
if (this.allowedResources != null && !this.pathMatcher.match(this.allowedResources, resourceUrls[i])) {
throw new ServletException("Resource [" + resourceUrls[i] +
"] does not match allowed pattern [" + this.allowedResources + "]");
}
if (logger.isDebugEnabled()) {
logger.debug("Including resource [" + resourceUrls[i] + "]");
}
RequestDispatcher rd = request.getRequestDispatcher(resourceUrls[i]);
rd.include(request, response);
}
}
/**
* Return the last-modified timestamp of the file that corresponds
* to the target resource URL (i.e. typically the request ".jsp" file).
* Will simply return -1 if "applyLastModified" is false (the default).
* <p>Returns no last-modified date before the startup time of this servlet,
* to allow for message resolution etc that influences JSP contents,
* assuming that those background resources might have changed on restart.
* <p>Returns the startup time of this servlet if the file that corresponds
* to the target resource URL couldn't be resolved (for example, because
* the WAR is not expanded).
* @see #determineResourceUrl
* @see #getFileTimestamp
*/
@Override
protected final long getLastModified(HttpServletRequest request) {
if (this.applyLastModified) {
String resourceUrl = determineResourceUrl(request);
if (resourceUrl == null) {
resourceUrl = this.defaultUrl;
}
if (resourceUrl != null) {
String[] resourceUrls = StringUtils.tokenizeToStringArray(resourceUrl, RESOURCE_URL_DELIMITERS);
long latestTimestamp = -1;
for (int i = 0; i < resourceUrls.length; i++) {
long timestamp = getFileTimestamp(resourceUrls[i]);
if (timestamp > latestTimestamp) {
latestTimestamp = timestamp;
}
}
return (latestTimestamp > this.startupTime ? latestTimestamp : this.startupTime);
}
}
return -1;
}
/**
* Return the file timestamp for the given resource.
* @param resourceUrl the URL of the resource
* @return the file timestamp in milliseconds, or -1 if not determinable
*/
protected long getFileTimestamp(String resourceUrl) {
ServletContextResource resource = new ServletContextResource(getServletContext(), resourceUrl);
try {
long lastModifiedTime = resource.lastModified();
if (logger.isDebugEnabled()) {
logger.debug("Last-modified timestamp of " + resource + " is " + lastModifiedTime);
}
return lastModifiedTime;
}
catch (IOException ex) {
logger.warn("Couldn't retrieve last-modified timestamp of [" + resource +
"] - using ResourceServlet startup time");
return -1;
}
}
}

View File

@ -55,7 +55,6 @@ import org.springframework.http.converter.support.AllEncompassingFormHttpMessage
import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter;
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
import org.springframework.http.converter.xml.SourceHttpMessageConverter;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.ClassUtils;
import org.springframework.util.PathMatcher;
import org.springframework.validation.Errors;
@ -94,6 +93,7 @@ import org.springframework.web.servlet.resource.ResourceUrlProvider;
import org.springframework.web.servlet.resource.ResourceUrlProviderExposingInterceptor;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.ViewResolverComposite;
import org.springframework.web.util.ParsingPathMatcher;
import org.springframework.web.util.UrlPathHelper;
/**
@ -140,7 +140,7 @@ import org.springframework.web.util.UrlPathHelper;
* exception types
* </ul>
*
* <p>Registers an {@link AntPathMatcher} and a {@link UrlPathHelper}
* <p>Registers an {@link ParsingPathMatcher} and a {@link UrlPathHelper}
* to be used by:
* <ul>
* <li>the {@link RequestMappingHandlerMapping},
@ -345,7 +345,7 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
@Bean
public PathMatcher mvcPathMatcher() {
PathMatcher pathMatcher = getPathMatchConfigurer().getPathMatcher();
return (pathMatcher != null ? pathMatcher : new AntPathMatcher());
return (pathMatcher != null ? pathMatcher : new ParsingPathMatcher());
}
/**

View File

@ -27,7 +27,6 @@ import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.core.Ordered;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.util.PathMatcher;
import org.springframework.web.HttpRequestHandler;
@ -42,6 +41,7 @@ import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.servlet.HandlerExecutionChain;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.util.ParsingPathMatcher;
import org.springframework.web.util.UrlPathHelper;
/**
@ -72,7 +72,7 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport
private UrlPathHelper urlPathHelper = new UrlPathHelper();
private PathMatcher pathMatcher = new AntPathMatcher();
private PathMatcher pathMatcher = new ParsingPathMatcher();
private final List<Object> interceptors = new ArrayList<>();

View File

@ -25,12 +25,12 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.http.CacheControl;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.util.PathMatcher;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.support.WebContentGenerator;
import org.springframework.web.util.ParsingPathMatcher;
import org.springframework.web.util.UrlPathHelper;
/**
@ -52,7 +52,7 @@ public class WebContentInterceptor extends WebContentGenerator implements Handle
private UrlPathHelper urlPathHelper = new UrlPathHelper();
private PathMatcher pathMatcher = new AntPathMatcher();
private PathMatcher pathMatcher = new ParsingPathMatcher();
private Map<String, Integer> cacheMappings = new HashMap<>();

View File

@ -27,9 +27,9 @@ import java.util.List;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import org.springframework.util.StringUtils;
import org.springframework.web.util.ParsingPathMatcher;
import org.springframework.web.util.UrlPathHelper;
/**
@ -104,7 +104,7 @@ public final class PatternsRequestCondition extends AbstractRequestCondition<Pat
this.patterns = Collections.unmodifiableSet(prependLeadingSlash(patterns));
this.pathHelper = (urlPathHelper != null ? urlPathHelper : new UrlPathHelper());
this.pathMatcher = (pathMatcher != null ? pathMatcher : new AntPathMatcher());
this.pathMatcher = (pathMatcher != null ? pathMatcher : new ParsingPathMatcher());
this.useSuffixPatternMatch = useSuffixPatternMatch;
this.useTrailingSlashMatch = useTrailingSlashMatch;
if (fileExtensions != null) {

View File

@ -45,7 +45,6 @@ import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.SynthesizingMethodParameter;
import org.springframework.objenesis.ObjenesisException;
import org.springframework.objenesis.SpringObjenesis;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.PathMatcher;
@ -63,6 +62,7 @@ import org.springframework.web.method.support.CompositeUriComponentsContributor;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import org.springframework.web.util.ParsingPathMatcher;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
@ -98,7 +98,7 @@ public class MvcUriComponentsBuilder {
private static final SpringObjenesis objenesis = new SpringObjenesis();
private static final PathMatcher pathMatcher = new AntPathMatcher();
private static final PathMatcher pathMatcher = new ParsingPathMatcher();
private static final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();

View File

@ -31,9 +31,9 @@ import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;
import org.springframework.web.util.ParsingPathMatcher;
import org.springframework.web.util.UrlPathHelper;
/**
@ -53,7 +53,7 @@ public class ResourceUrlProvider implements ApplicationListener<ContextRefreshed
private UrlPathHelper urlPathHelper = new UrlPathHelper();
private PathMatcher pathMatcher = new AntPathMatcher();
private PathMatcher pathMatcher = new ParsingPathMatcher();
private final Map<String, ResourceHttpRequestHandler> handlerMap = new LinkedHashMap<>();

View File

@ -37,6 +37,7 @@ import org.springframework.web.servlet.handler.MappedInterceptor;
import org.springframework.web.servlet.handler.WebRequestHandlerInterceptorAdapter;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.theme.ThemeChangeInterceptor;
import org.springframework.web.util.ParsingPathMatcher;
import static org.junit.Assert.*;
@ -152,7 +153,7 @@ public class InterceptorRegistryTests {
private List<HandlerInterceptor> getInterceptorsForPath(String lookupPath) {
PathMatcher pathMatcher = new AntPathMatcher();
PathMatcher pathMatcher = new ParsingPathMatcher();
List<HandlerInterceptor> result = new ArrayList<>();
for (Object interceptor : this.registry.getInterceptors()) {
if (interceptor instanceof MappedInterceptor) {

View File

@ -83,6 +83,7 @@ import org.springframework.web.servlet.resource.ResourceUrlProviderExposingInter
import org.springframework.web.servlet.view.BeanNameViewResolver;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.ViewResolverComposite;
import org.springframework.web.util.ParsingPathMatcher;
import org.springframework.web.util.UrlPathHelper;
import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;
@ -320,7 +321,7 @@ public class WebMvcConfigurationSupportTests {
assertNotNull(urlPathHelper);
assertNotNull(pathMatcher);
assertEquals(AntPathMatcher.class, pathMatcher.getClass());
assertEquals(ParsingPathMatcher.class, pathMatcher.getClass());
}
private ApplicationContext initContext(Class<?>... configClasses) {

View File

@ -38,6 +38,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.support.StaticWebApplicationContext;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.util.ParsingPathMatcher;
import org.springframework.web.util.UrlPathHelper;
@ -244,7 +245,7 @@ public class HandlerMethodMappingTests {
private UrlPathHelper pathHelper = new UrlPathHelper();
private PathMatcher pathMatcher = new AntPathMatcher();
private PathMatcher pathMatcher = new ParsingPathMatcher();
public MyHandlerMethodMapping() {

View File

@ -81,7 +81,7 @@ public class PathMatchingUrlHandlerMappingTests {
HandlerExecutionChain hec = getHandler(req);
assertTrue("Handler is null", hec != null);
assertTrue("Handler is correct bean", hec.getHandler() == bean);
assertEquals("pathmatchingTest.html", req.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE));
assertEquals("/pathmatchingTest.html", req.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE));
// no match, no forward slash included
req = new MockHttpServletRequest("GET", "welcome.html");
@ -121,11 +121,6 @@ public class PathMatchingUrlHandlerMappingTests {
hec = getHandler(req);
assertTrue("Handler is correct bean", hec != null && hec.getHandler() == bean);
// this as well, because there's a **/in there as well
req = new MockHttpServletRequest("GET", "/testing/bla.jsp");
hec = getHandler(req);
assertTrue("Handler is correct bean", hec != null && hec.getHandler() == bean);
// should match because exact pattern is there
req = new MockHttpServletRequest("GET", "/administrator/another/bla.xml");
hec = getHandler(req);

View File

@ -23,11 +23,11 @@ import static org.junit.Assert.*;
import org.springframework.mock.web.test.MockHttpServletRequest;
import org.springframework.mock.web.test.MockHttpServletResponse;
import org.springframework.ui.ModelMap;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.util.ParsingPathMatcher;
/**
* @author Juergen Hoeller
@ -36,7 +36,7 @@ import org.springframework.web.servlet.ModelAndView;
*/
public class UrlFilenameViewControllerTests {
private PathMatcher pathMatcher = new AntPathMatcher();
private PathMatcher pathMatcher = new ParsingPathMatcher();
@Test

View File

@ -62,19 +62,21 @@ public class WebContentInterceptorTests {
@Test
public void mappedCacheConfigurationOverridesGlobal() throws Exception {
Properties mappings = new Properties();
mappings.setProperty("**/*handle.vm", "-1");
mappings.setProperty("*/*handle.vm", "-1"); // was **/*handle.vm
WebContentInterceptor interceptor = new WebContentInterceptor();
interceptor.setCacheSeconds(10);
interceptor.setCacheMappings(mappings);
request.setRequestURI("http://localhost:7070/example/adminhandle.vm");
// request.setRequestURI("http://localhost:7070/example/adminhandle.vm");
request.setRequestURI("example/adminhandle.vm");
interceptor.preHandle(request, response, null);
Iterable<String> cacheControlHeaders = response.getHeaders("Cache-Control");
assertThat(cacheControlHeaders, Matchers.emptyIterable());
request.setRequestURI("http://localhost:7070/example/bingo.html");
// request.setRequestURI("http://localhost:7070/example/bingo.html");
request.setRequestURI("example/bingo.html");
interceptor.preHandle(request, response, null);
cacheControlHeaders = response.getHeaders("Cache-Control");
@ -143,10 +145,11 @@ public class WebContentInterceptorTests {
interceptor.setUseExpiresHeader(true);
interceptor.setAlwaysMustRevalidate(true);
Properties mappings = new Properties();
mappings.setProperty("**/*.cache.html", "10");
mappings.setProperty("*/*.cache.html", "10"); // was **/*.cache.html
interceptor.setCacheMappings(mappings);
request.setRequestURI("http://example.org/foo/page.html");
// request.setRequestURI("http://example.org/foo/page.html");
request.setRequestURI("foo/page.html");
interceptor.preHandle(request, response, null);
Iterable<String> expiresHeaders = response.getHeaders("Expires");
@ -157,7 +160,8 @@ public class WebContentInterceptorTests {
assertThat(pragmaHeaders, Matchers.contains("no-cache"));
response = new MockHttpServletResponse();
request.setRequestURI("http://example.org/page.cache.html");
// request.setRequestURI("http://example.org/page.cache.html");
request.setRequestURI("foo/page.cache.html");
interceptor.preHandle(request, response, null);
expiresHeaders = response.getHeaders("Expires");

View File

@ -2364,22 +2364,22 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl
@Controller
static class MyRelativeMethodPathDispatchingController {
@RequestMapping("**/myHandle")
@RequestMapping("*/myHandle") // was **/myHandle
public void myHandle(HttpServletResponse response) throws IOException {
response.getWriter().write("myView");
}
@RequestMapping("/**/*Other")
@RequestMapping("/*/*Other") // was /**/*Other
public void myOtherHandle(HttpServletResponse response) throws IOException {
response.getWriter().write("myOtherView");
}
@RequestMapping("**/myLang")
@RequestMapping("*/myLang") // was **/myLang
public void myLangHandle(HttpServletResponse response) throws IOException {
response.getWriter().write("myLangView");
}
@RequestMapping("/**/surprise")
@RequestMapping("/*/surprise") // was /**/surprise
public void mySurpriseHandle(HttpServletResponse response) throws IOException {
response.getWriter().write("mySurpriseView");
}
@ -2643,7 +2643,7 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl
@Controller
public static class PathOrderingController {
@RequestMapping(value = {"/dir/myPath1.do", "/**/*.do"})
@RequestMapping(value = {"/dir/myPath1.do", "/*/*.do"})
public void method1(Writer writer) throws IOException {
writer.write("method1");
}

View File

@ -571,14 +571,14 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab
@RequestMapping("/category")
public static class MultiPathController {
@RequestMapping(value = {"/{category}/page/{page}", "/**/{category}/page/{page}"})
@RequestMapping(value = {"/{category}/page/{page}", "/*/{category}/page/{page}"})
public void category(@PathVariable String category, @PathVariable int page, Writer writer) throws IOException {
writer.write("handle1-");
writer.write("category-" + category);
writer.write("page-" + page);
}
@RequestMapping(value = {"/{category}", "/**/{category}"})
@RequestMapping(value = {"/{category}", "/*/{category}"})
public void category(@PathVariable String category, Writer writer) throws IOException {
writer.write("handle2-");
writer.write("category-" + category);
@ -598,7 +598,7 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab
}
@Controller
@RequestMapping("/*/menu/**")
@RequestMapping("/*/menu/") // was /*/menu/**
public static class MenuTreeController {
@RequestMapping("type/{var}")

View File

@ -8,16 +8,17 @@
<property name="mappings">
<value>
welcome.html=mainController
/**/pathmatchingTest.html=mainController
/**/pathmatching??.html=mainController
/**/path??matching.html=mainController
/**/??path??matching.html=mainController
/**/*.jsp=mainController
/administrator/**/pathmatching.html=mainController
/administrator/**/testlast*=mainController
/path??matching.html=mainController
/pathmatchingTest.html=mainController
??path??matching.html=mainController
/administrator/pathmatching.html=mainController
/administrator/testlast*=mainController
/administrator/testing/longer/{*foobar}=mainController
/administrator/*/testlast*=mainController
/administrator/*/pathmatching.html=mainController
/pathmatching??.html=mainController
/*.jsp=mainController
/administrator/another/bla.xml=mainController
/administrator/testing/longer/**/**/**/**/**=mainController
/administrator/testing/longer2/**/**/bla/**=mainController
/*test*.jpeg=mainController
/*/test.jpeg=mainController
/outofpattern*yeah=mainController