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:
parent
6f029392c7
commit
f58ffad939
|
|
@ -21,9 +21,9 @@ import java.util.LinkedHashMap;
|
|||
import java.util.Map;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.PathMatcher;
|
||||
import org.springframework.web.util.ParsingPathMatcher;
|
||||
import org.springframework.web.util.UrlPathHelper;
|
||||
|
||||
/**
|
||||
|
|
@ -40,7 +40,7 @@ public class UrlBasedCorsConfigurationSource implements CorsConfigurationSource
|
|||
|
||||
private final Map<String, CorsConfiguration> corsConfigurations = new LinkedHashMap<>();
|
||||
|
||||
private PathMatcher pathMatcher = new AntPathMatcher();
|
||||
private PathMatcher pathMatcher = new ParsingPathMatcher();
|
||||
|
||||
private UrlPathHelper urlPathHelper = new UrlPathHelper();
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,115 @@
|
|||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.web.util;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.util.PathMatcher;
|
||||
import org.springframework.web.util.patterns.PathPattern;
|
||||
import org.springframework.web.util.patterns.PathPatternParser;
|
||||
import org.springframework.web.util.patterns.PatternComparatorConsideringPath;
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Andy Clement
|
||||
* @since 5.0
|
||||
*/
|
||||
public class ParsingPathMatcher implements PathMatcher {
|
||||
|
||||
Map<String,PathPattern> cache = new HashMap<>();
|
||||
|
||||
PathPatternParser parser;
|
||||
|
||||
public ParsingPathMatcher() {
|
||||
parser = new PathPatternParser();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean match(String pattern, String path) {
|
||||
PathPattern p = getPathPattern(pattern);
|
||||
return p.matches(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matchStart(String pattern, String path) {
|
||||
PathPattern p = getPathPattern(pattern);
|
||||
return p.matchStart(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String extractPathWithinPattern(String pattern, String path) {
|
||||
PathPattern p = getPathPattern(pattern);
|
||||
return p.extractPathWithinPattern(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> extractUriTemplateVariables(String pattern, String path) {
|
||||
PathPattern p = getPathPattern(pattern);
|
||||
return p.matchAndExtract(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String combine(String pattern1, String pattern2) {
|
||||
PathPattern pathPattern = getPathPattern(pattern1);
|
||||
return pathPattern.combine(pattern2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Comparator<String> getPatternComparator(String path) {
|
||||
return new PathPatternStringComparatorConsideringPath(path);
|
||||
}
|
||||
|
||||
class PathPatternStringComparatorConsideringPath implements Comparator<String> {
|
||||
|
||||
PatternComparatorConsideringPath ppcp;
|
||||
|
||||
public PathPatternStringComparatorConsideringPath(String path) {
|
||||
ppcp = new PatternComparatorConsideringPath(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compare(String o1, String o2) {
|
||||
if (o1 == null) {
|
||||
return (o2==null?0:+1);
|
||||
} else if (o2 == null) {
|
||||
return -1;
|
||||
}
|
||||
PathPattern p1 = getPathPattern(o1);
|
||||
PathPattern p2 = getPathPattern(o2);
|
||||
return ppcp.compare(p1,p2);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPattern(String path) {
|
||||
// TODO crude, should be smarter, lookup pattern and ask it
|
||||
return (path.indexOf('*') != -1 || path.indexOf('?') != -1);
|
||||
}
|
||||
|
||||
private PathPattern getPathPattern(String pattern) {
|
||||
PathPattern pathPattern = cache.get(pattern);
|
||||
if (pathPattern == null) {
|
||||
pathPattern = parser.parse(pattern);
|
||||
cache.put(pattern, pathPattern);
|
||||
}
|
||||
return pathPattern;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.web.util.patterns;
|
||||
|
||||
import org.springframework.web.util.patterns.PathPattern.MatchingContext;
|
||||
|
||||
/**
|
||||
* A path element representing capturing the rest of a path. In the pattern
|
||||
* '/foo/{*foobar}' the /{*foobar} is represented as a {@link CaptureTheRestPathElement}.
|
||||
*
|
||||
* @author Andy Clement
|
||||
*/
|
||||
class CaptureTheRestPathElement extends PathElement {
|
||||
|
||||
private String variableName;
|
||||
|
||||
private char separator;
|
||||
|
||||
/**
|
||||
* @param pos
|
||||
* @param captureDescriptor a character array containing contents like '{' '*' 'a' 'b' '}'
|
||||
* @param separator the separator ahead of this construct
|
||||
*/
|
||||
CaptureTheRestPathElement(int pos, char[] captureDescriptor, char separator) {
|
||||
super(pos);
|
||||
variableName = new String(captureDescriptor, 2, captureDescriptor.length - 3);
|
||||
this.separator = separator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(int candidateIndex, MatchingContext matchingContext) {
|
||||
// No need to handle 'match start' checking as this captures everything
|
||||
// anyway and cannot be followed by anything else
|
||||
// assert next == null
|
||||
while ((candidateIndex+1)<matchingContext.candidateLength &&
|
||||
matchingContext.candidate[candidateIndex+1] == separator) {
|
||||
candidateIndex++;
|
||||
}
|
||||
if (matchingContext.extractingVariables) {
|
||||
matchingContext.set(variableName, new String(matchingContext.candidate, candidateIndex,
|
||||
matchingContext.candidateLength - candidateIndex));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "CaptureTheRest(/{*" + variableName + "})";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNormalizedLength() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getWildcardCount() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCaptureCount() {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.web.util.patterns;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
|
||||
import org.springframework.web.util.patterns.PathPattern.MatchingContext;
|
||||
|
||||
/**
|
||||
* A path element representing capturing a piece of the path as a variable. In the pattern
|
||||
* '/foo/{bar}/goo' the {bar} is represented as a {@link CaptureVariablePathElement}.
|
||||
*
|
||||
* @author Andy Clement
|
||||
*/
|
||||
class CaptureVariablePathElement extends PathElement {
|
||||
|
||||
private String variableName;
|
||||
|
||||
private java.util.regex.Pattern constraintPattern;
|
||||
|
||||
/**
|
||||
* @param pos the position in the pattern of this capture element
|
||||
* @param captureDescriptor is of the form {AAAAA[:pattern]}
|
||||
*/
|
||||
CaptureVariablePathElement(int pos, char[] captureDescriptor, boolean caseSensitive) {
|
||||
super(pos);
|
||||
int colon = -1;
|
||||
for (int i = 0; i < captureDescriptor.length; i++) {
|
||||
if (captureDescriptor[i] == ':') {
|
||||
colon = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (colon == -1) {
|
||||
// no constraint
|
||||
variableName = new String(captureDescriptor, 1, captureDescriptor.length - 2);
|
||||
} else {
|
||||
variableName = new String(captureDescriptor, 1, colon - 1);
|
||||
if (caseSensitive) {
|
||||
constraintPattern = java.util.regex.Pattern
|
||||
.compile(new String(captureDescriptor, colon + 1, captureDescriptor.length - colon - 2));
|
||||
} else {
|
||||
constraintPattern = java.util.regex.Pattern.compile(
|
||||
new String(captureDescriptor, colon + 1, captureDescriptor.length - colon - 2),
|
||||
java.util.regex.Pattern.CASE_INSENSITIVE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(int candidateIndex, MatchingContext matchingContext) {
|
||||
int nextPos = matchingContext.scanAhead(candidateIndex);
|
||||
CharSequence candidateCapture = null;
|
||||
if (constraintPattern != null) {
|
||||
// TODO possible optimization - only regex match if rest of pattern matches? Benefit likely to vary pattern to pattern
|
||||
candidateCapture = new SubSequence(matchingContext.candidate, candidateIndex, nextPos);
|
||||
Matcher m = constraintPattern.matcher(candidateCapture);
|
||||
if (m.groupCount() != 0) {
|
||||
throw new IllegalArgumentException("No capture groups allowed in the constraint regex: "+constraintPattern.pattern());
|
||||
}
|
||||
if (!m.matches()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
boolean match = false;
|
||||
if (next == null) {
|
||||
match = (nextPos == matchingContext.candidateLength);
|
||||
} else {
|
||||
if (matchingContext.isMatchStartMatching && nextPos == matchingContext.candidateLength) {
|
||||
match = true; // no more data but matches up to this point
|
||||
} else {
|
||||
match = next.matches(nextPos, matchingContext);
|
||||
}
|
||||
}
|
||||
if (match && matchingContext.extractingVariables) {
|
||||
matchingContext.set(variableName, new String(matchingContext.candidate, candidateIndex, nextPos - candidateIndex));
|
||||
}
|
||||
return match;
|
||||
}
|
||||
|
||||
public String getVariableName() {
|
||||
return this.variableName;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "CaptureVariable({" + variableName + (constraintPattern == null ? "" : ":" + constraintPattern.pattern()) + "})";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNormalizedLength() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getWildcardCount() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCaptureCount() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getScore() {
|
||||
return CAPTURE_VARIABLE_WEIGHT;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.web.util.patterns;
|
||||
|
||||
import org.springframework.web.util.patterns.PathPattern.MatchingContext;
|
||||
|
||||
/**
|
||||
* A literal path element. In the pattern '/foo/bar/goo' there are three
|
||||
* literal path elements 'foo', 'bar' and 'goo'.
|
||||
*
|
||||
* @author Andy Clement
|
||||
*/
|
||||
class LiteralPathElement extends PathElement {
|
||||
|
||||
private char[] text;
|
||||
|
||||
private int len;
|
||||
|
||||
private boolean caseSensitive;
|
||||
|
||||
public LiteralPathElement(int pos, char[] literalText, boolean caseSensitive) {
|
||||
super(pos);
|
||||
this.len = literalText.length;
|
||||
this.caseSensitive = caseSensitive;
|
||||
if (caseSensitive) {
|
||||
this.text = literalText;
|
||||
} else {
|
||||
// Force all the text lower case to make matching faster
|
||||
this.text = new char[literalText.length];
|
||||
for (int i = 0; i < len; i++) {
|
||||
this.text[i] = Character.toLowerCase(literalText[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(int candidateIndex, MatchingContext matchingContext) {
|
||||
if ((candidateIndex + text.length) > matchingContext.candidateLength) {
|
||||
return false; // not enough data, cannot be a match
|
||||
}
|
||||
if (caseSensitive) {
|
||||
for (int i = 0; i < len; i++) {
|
||||
if (matchingContext.candidate[candidateIndex++] != text[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i < len; i++) {
|
||||
// TODO revisit performance if doing a lot of case insensitive matching
|
||||
if (Character.toLowerCase(matchingContext.candidate[candidateIndex++]) != text[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (next == null) {
|
||||
return candidateIndex == matchingContext.candidateLength;
|
||||
} else {
|
||||
if (matchingContext.isMatchStartMatching && candidateIndex == matchingContext.candidateLength) {
|
||||
return true; // no more data but everything matched so far
|
||||
}
|
||||
return next.matches(candidateIndex, matchingContext);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNormalizedLength() {
|
||||
return len;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "Literal(" + new String(text) + ")";
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.web.util.patterns;
|
||||
|
||||
import org.springframework.web.util.patterns.PathPattern.MatchingContext;
|
||||
|
||||
/**
|
||||
* Common supertype for the Ast nodes created to represent a path pattern.
|
||||
*
|
||||
* @author Andy Clement
|
||||
*/
|
||||
abstract class PathElement {
|
||||
|
||||
// Score related
|
||||
protected static final int WILDCARD_WEIGHT = 100;
|
||||
protected static final int CAPTURE_VARIABLE_WEIGHT = 1;
|
||||
|
||||
/**
|
||||
* Position in the pattern where this path element starts
|
||||
*/
|
||||
protected int pos;
|
||||
|
||||
/**
|
||||
* The next path element in the chain
|
||||
*/
|
||||
protected PathElement next;
|
||||
|
||||
/**
|
||||
* The previous path element in the chain
|
||||
*/
|
||||
protected PathElement prev;
|
||||
|
||||
/**
|
||||
* Create a new path element.
|
||||
* @param pos the position where this path element starts in the pattern data
|
||||
*/
|
||||
PathElement(int pos) {
|
||||
this.pos = pos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to match this path element.
|
||||
*
|
||||
* @param candidatePos the current position within the candidate path
|
||||
* @param matchingContext encapsulates context for the match including the candidate
|
||||
* @return true if matches, otherwise false
|
||||
*/
|
||||
public abstract boolean matches(int candidatePos, MatchingContext matchingContext);
|
||||
|
||||
/**
|
||||
* @return the length of the path element where captures are considered to be one character long
|
||||
*/
|
||||
public abstract int getNormalizedLength();
|
||||
|
||||
/**
|
||||
* @return the number of variables captured by the path element
|
||||
*/
|
||||
public int getCaptureCount() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the number of wildcard elements (*, ?) in the path element
|
||||
*/
|
||||
public int getWildcardCount() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the score for this PathElement, combined score is used to compare parsed patterns.
|
||||
*/
|
||||
public int getScore() {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,462 @@
|
|||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.web.util.patterns;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.util.PathMatcher;
|
||||
|
||||
/**
|
||||
* Represents a parsed path pattern. Includes a chain of path elements
|
||||
* for fast matching and accumulates computed state for quick comparison of
|
||||
* patterns.
|
||||
*
|
||||
* @author Andy Clement
|
||||
*/
|
||||
public class PathPattern implements Comparable<PathPattern> {
|
||||
|
||||
private final static Map<String,String> NO_VARIABLES_MAP = Collections.emptyMap();
|
||||
|
||||
/** First path element in the parsed chain of path elements for this pattern */
|
||||
private PathElement head;
|
||||
|
||||
/** The text of the parsed pattern */
|
||||
private String patternString;
|
||||
|
||||
/** The separator used when parsing the pattern */
|
||||
private char separator;
|
||||
|
||||
/** Will this match candidates in a case sensitive way? (case sensitivity at parse time) */
|
||||
private boolean caseSensitive;
|
||||
|
||||
/** How many variables are captured in this pattern */
|
||||
private int capturedVariableCount;
|
||||
|
||||
/**
|
||||
* The normalized length is trying to measure the 'active' part of the pattern. It is computed
|
||||
* by assuming all captured variables have a normalized length of 1. Effectively this means changing
|
||||
* your variable name lengths isn't going to change the length of the active part of the pattern.
|
||||
* Useful when comparing two patterns.
|
||||
*/
|
||||
int normalizedLength;
|
||||
|
||||
/**
|
||||
* Does the pattern end with '<separator>*'
|
||||
*/
|
||||
boolean endsWithSeparatorWildcard = false;
|
||||
|
||||
/**
|
||||
* Score is used to quickly compare patterns. Different pattern components are given different
|
||||
* weights. A 'lower score' is more specific. Current weights:
|
||||
* <ul>
|
||||
* <li>Captured variables are worth 1
|
||||
* <li>Wildcard is worth 100
|
||||
* </ul>
|
||||
*/
|
||||
private int score;
|
||||
|
||||
/** Does the pattern end with {*...} */
|
||||
private boolean isCatchAll = false;
|
||||
|
||||
public PathPattern(String patternText, PathElement head, char separator, boolean caseSensitive) {
|
||||
this.head = head;
|
||||
this.patternString = patternText;
|
||||
this.separator = separator;
|
||||
this.caseSensitive = caseSensitive;
|
||||
// Compute fields for fast comparison
|
||||
PathElement s = head;
|
||||
while (s != null) {
|
||||
this.capturedVariableCount += s.getCaptureCount();
|
||||
this.normalizedLength += s.getNormalizedLength();
|
||||
this.score += s.getScore();
|
||||
if (s instanceof CaptureTheRestPathElement || s instanceof WildcardTheRestPathElement) {
|
||||
this.isCatchAll = true;
|
||||
}
|
||||
if (s instanceof SeparatorPathElement && s.next!=null && s.next instanceof WildcardPathElement && s.next.next == null) {
|
||||
this.endsWithSeparatorWildcard=true;
|
||||
}
|
||||
s = s.next;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path the candidate path to attempt to match against this pattern
|
||||
* @return true if the path matches this pattern
|
||||
*/
|
||||
public boolean matches(String path) {
|
||||
if (head == null) {
|
||||
return (path == null) || (path.length() == 0);
|
||||
} else if (path == null || path.length() == 0) {
|
||||
if (head instanceof WildcardTheRestPathElement || head instanceof CaptureTheRestPathElement) {
|
||||
path = ""; // Will allow CaptureTheRest to bind the variable to empty
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
MatchingContext matchingContext = new MatchingContext(path,false);
|
||||
return head.matches(0, matchingContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path the path to check against the pattern
|
||||
* @return true if the pattern matches as much of the path as is supplied
|
||||
*/
|
||||
public boolean matchStart(String path) {
|
||||
if (head == null) {
|
||||
return (path==null || path.length() == 0);
|
||||
} else if (path == null || path.length() == 0) {
|
||||
return true;
|
||||
}
|
||||
MatchingContext matchingContext = new MatchingContext(path,false);
|
||||
matchingContext.setMatchStartMatching(true);
|
||||
return head.matches(0, matchingContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path a path to match against this pattern
|
||||
* @return a map of extracted variables - an empty map if no variables extracted.
|
||||
*/
|
||||
public Map<String, String> matchAndExtract(String path) {
|
||||
MatchingContext matchingContext = new MatchingContext(path,true);
|
||||
if (head != null && head.matches(0, matchingContext)) {
|
||||
return matchingContext.getExtractedVariables();
|
||||
} else {
|
||||
if (path== null || path.length()==0) {
|
||||
return NO_VARIABLES_MAP;
|
||||
} else {
|
||||
throw new IllegalStateException("Pattern \"" + this.toString() + "\" is not a match for \"" + path + "\"");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the original pattern string that was parsed to create this PathPattern
|
||||
*/
|
||||
public String getPatternString() {
|
||||
return patternString;
|
||||
}
|
||||
|
||||
public PathElement getHeadSection() {
|
||||
return head;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a full path, determine the pattern-mapped part. <p>For example: <ul>
|
||||
* <li>'{@code /docs/cvs/commit.html}' and '{@code /docs/cvs/commit.html} -> ''</li>
|
||||
* <li>'{@code /docs/*}' and '{@code /docs/cvs/commit} -> '{@code cvs/commit}'</li>
|
||||
* <li>'{@code /docs/cvs/*.html}' and '{@code /docs/cvs/commit.html} -> '{@code commit.html}'</li>
|
||||
* <li>'{@code /docs/**}' and '{@code /docs/cvs/commit} -> '{@code cvs/commit}'</li>
|
||||
* </ul>
|
||||
* <p><b>Note:</b> Assumes that {@link #matches} returns {@code true} for '{@code pattern}' and '{@code path}', but
|
||||
* does <strong>not</strong> enforce this. As per the contract on {@link PathMatcher}, this
|
||||
* method will trim leading/trailing separators. It will also remove duplicate separators in
|
||||
* the returned path.
|
||||
* @param path a path that matches this pattern
|
||||
* @return the subset of the path that is matched by pattern or "" if none of it is matched by pattern elements
|
||||
*/
|
||||
public String extractPathWithinPattern(String path) {
|
||||
// assert this.matches(path)
|
||||
PathElement s = head;
|
||||
int separatorCount = 0;
|
||||
// Find first path element that is pattern based
|
||||
while (s != null) {
|
||||
if (s instanceof SeparatorPathElement || s instanceof CaptureTheRestPathElement || s instanceof WildcardTheRestPathElement) {
|
||||
separatorCount++;
|
||||
}
|
||||
if (s.getWildcardCount()!=0 || s.getCaptureCount()!=0) {
|
||||
break;
|
||||
}
|
||||
s = s.next;
|
||||
}
|
||||
if (s == null) {
|
||||
return ""; // There is no pattern mapped section
|
||||
}
|
||||
// Now separatorCount indicates how many sections of the path to skip
|
||||
char[] pathChars = path.toCharArray();
|
||||
int len = pathChars.length;
|
||||
int pos = 0;
|
||||
while (separatorCount > 0 && pos < len) {
|
||||
if (path.charAt(pos++) == separator) {
|
||||
// Skip any adjacent separators
|
||||
while (path.charAt(pos) == separator) {
|
||||
pos++;
|
||||
}
|
||||
separatorCount--;
|
||||
}
|
||||
}
|
||||
int end = len;
|
||||
// Trim trailing separators
|
||||
while (path.charAt(end-1) == separator) {
|
||||
end--;
|
||||
}
|
||||
// Check if multiple separators embedded in the resulting path, if so trim them out.
|
||||
// Example: aaa////bbb//ccc/d -> aaa/bbb/ccc/d
|
||||
// The stringWithDuplicateSeparatorsRemoved is only computed if necessary
|
||||
int c = pos;
|
||||
StringBuilder stringWithDuplicateSeparatorsRemoved = null;
|
||||
while (c<end) {
|
||||
char ch = path.charAt(c);
|
||||
if (ch == separator) {
|
||||
if ((c+1)<end && path.charAt(c+1)==separator) {
|
||||
// multiple separators
|
||||
if (stringWithDuplicateSeparatorsRemoved == null) {
|
||||
// first time seen, need to capture all data up to this point
|
||||
stringWithDuplicateSeparatorsRemoved = new StringBuilder();
|
||||
stringWithDuplicateSeparatorsRemoved.append(path.substring(pos,c));
|
||||
}
|
||||
do {
|
||||
c++;
|
||||
} while ((c+1)<end && path.charAt(c+1)==separator);
|
||||
}
|
||||
}
|
||||
if (stringWithDuplicateSeparatorsRemoved != null) {
|
||||
stringWithDuplicateSeparatorsRemoved.append(ch);
|
||||
}
|
||||
c++;
|
||||
}
|
||||
if (stringWithDuplicateSeparatorsRemoved != null) {
|
||||
return stringWithDuplicateSeparatorsRemoved.toString();
|
||||
}
|
||||
return pos == len ? "" : path.substring(pos,end);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare this pattern with a supplied pattern. Return -1,0,+1 if this pattern
|
||||
* is more specific, the same or less specific than the supplied pattern.
|
||||
* The aim is to sort more specific patterns first.
|
||||
*/
|
||||
@Override
|
||||
public int compareTo(PathPattern p) {
|
||||
// 1) null is sorted last
|
||||
if (p == null) {
|
||||
return -1;
|
||||
}
|
||||
// 2) catchall patterns are sorted last. If both catchall then the
|
||||
// length is considered
|
||||
if (isCatchAll()) {
|
||||
if (p.isCatchAll()) {
|
||||
int lenDifference = this.getNormalizedLength() - p.getNormalizedLength();
|
||||
if (lenDifference != 0) {
|
||||
return (lenDifference < 0) ? +1 : -1;
|
||||
}
|
||||
} else {
|
||||
return +1;
|
||||
}
|
||||
} else if (p.isCatchAll()) {
|
||||
return -1;
|
||||
}
|
||||
// 3) This will sort such that if they differ in terms of wildcards or
|
||||
// captured variable counts, the one with the most will be sorted last
|
||||
int score = this.getScore() - p.getScore();
|
||||
if (score != 0) {
|
||||
return (score < 0) ? -1 : +1;
|
||||
}
|
||||
// 4) longer is better
|
||||
int lenDifference = this.getNormalizedLength() - p.getNormalizedLength();
|
||||
return (lenDifference < 0) ? +1 : (lenDifference == 0 ? 0 : -1);
|
||||
}
|
||||
|
||||
public int getScore() {
|
||||
return score;
|
||||
}
|
||||
|
||||
public boolean isCatchAll() {
|
||||
return isCatchAll;
|
||||
}
|
||||
|
||||
/**
|
||||
* The normalized length is trying to measure the 'active' part of the pattern. It is computed
|
||||
* by assuming all capture variables have a normalized length of 1. Effectively this means changing
|
||||
* your variable name lengths isn't going to change the length of the active part of the pattern.
|
||||
* Useful when comparing two patterns.
|
||||
* @return the normalized length of the pattern
|
||||
*/
|
||||
public int getNormalizedLength() {
|
||||
return normalizedLength;
|
||||
}
|
||||
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof PathPattern)) {
|
||||
return false;
|
||||
}
|
||||
PathPattern p = (PathPattern) o;
|
||||
return patternString.equals(p.getPatternString()) && separator == p.getSeparator()
|
||||
&& caseSensitive == p.caseSensitive;
|
||||
}
|
||||
|
||||
public int hashCode() {
|
||||
return (patternString.hashCode() * 17 + separator) * 17 + (caseSensitive ? 1 : 0);
|
||||
}
|
||||
|
||||
public String toChainString() {
|
||||
StringBuilder buf = new StringBuilder();
|
||||
PathElement pe = head;
|
||||
while (pe!=null) {
|
||||
buf.append(pe.toString()).append(" ");
|
||||
pe = pe.next;
|
||||
}
|
||||
return buf.toString().trim();
|
||||
}
|
||||
|
||||
public char getSeparator() {
|
||||
return separator;
|
||||
}
|
||||
|
||||
public int getCapturedVariableCount() {
|
||||
return capturedVariableCount;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return patternString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encapsulates context when attempting a match. Includes some fixed state like the
|
||||
* candidate currently being considered for a match but also some accumulators for
|
||||
* extracted variables.
|
||||
*/
|
||||
class MatchingContext {
|
||||
|
||||
// The candidate path to attempt a match against
|
||||
char[] candidate;
|
||||
|
||||
// The length of the candidate path
|
||||
int candidateLength;
|
||||
|
||||
boolean isMatchStartMatching = false;
|
||||
|
||||
private Map<String,String> extractedVariables;
|
||||
|
||||
public boolean extractingVariables;
|
||||
|
||||
public MatchingContext(String path, boolean extractVariables) {
|
||||
candidate = path.toCharArray();
|
||||
candidateLength = candidate.length;
|
||||
this.extractingVariables = extractVariables;
|
||||
}
|
||||
|
||||
public void setMatchStartMatching(boolean b) {
|
||||
isMatchStartMatching = b;
|
||||
}
|
||||
|
||||
public void set(String key, String value) {
|
||||
if (this.extractedVariables == null) {
|
||||
extractedVariables = new HashMap<>();
|
||||
}
|
||||
extractedVariables.put(key, value);
|
||||
}
|
||||
|
||||
public Map<String,String> getExtractedVariables() {
|
||||
if (this.extractedVariables == null) {
|
||||
return NO_VARIABLES_MAP;
|
||||
} else {
|
||||
return this.extractedVariables;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan ahead from the specified position for either the next separator
|
||||
* character or the end of the candidate.
|
||||
*
|
||||
* @param pos the starting position for the scan
|
||||
* @return the position of the next separator or the end of the candidate
|
||||
*/
|
||||
public int scanAhead(int pos) {
|
||||
while (pos < candidateLength) {
|
||||
if (candidate[pos] == separator) {
|
||||
return pos;
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
return candidateLength;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Combine this pattern with another. Currently does not produce a new PathPattern, just produces a new string.
|
||||
*/
|
||||
public String combine(String pattern2string) {
|
||||
// If one of them is empty the result is the other. If both empty the result is ""
|
||||
if (patternString == null || patternString.length()==0) {
|
||||
if (pattern2string == null || pattern2string.length()==0) {
|
||||
return "";
|
||||
} else {
|
||||
return pattern2string;
|
||||
}
|
||||
} else if (pattern2string == null || pattern2string.length()==0) {
|
||||
return patternString;
|
||||
}
|
||||
|
||||
// /* + /hotel => /hotel
|
||||
// /*.* + /*.html => /*.html
|
||||
// However:
|
||||
// /usr + /user => /usr/user
|
||||
// /{foo} + /bar => /{foo}/bar
|
||||
if (!patternString.equals(pattern2string) && capturedVariableCount==0 && matches(pattern2string)) {
|
||||
return pattern2string;
|
||||
}
|
||||
|
||||
// /hotels/* + /booking => /hotels/booking
|
||||
// /hotels/* + booking => /hotels/booking
|
||||
if (endsWithSeparatorWildcard) {
|
||||
return concat(patternString.substring(0,patternString.length()-2), pattern2string);
|
||||
}
|
||||
|
||||
// /hotels + /booking => /hotels/booking
|
||||
// /hotels + booking => /hotels/booking
|
||||
int starDotPos1 = patternString.indexOf("*."); // Are there any file prefix/suffix things to consider?
|
||||
if (capturedVariableCount!=0 || starDotPos1 == -1 || separator=='.') {
|
||||
return concat(patternString, pattern2string);
|
||||
}
|
||||
|
||||
// /*.html + /hotel => /hotel.html
|
||||
// /*.html + /hotel.* => /hotel.html
|
||||
String firstExtension = patternString.substring(starDotPos1+1); // looking for the first extension
|
||||
int dotPos2 = pattern2string.indexOf('.');
|
||||
String file2 = (dotPos2==-1?pattern2string:pattern2string.substring(0,dotPos2));
|
||||
String secondExtension = (dotPos2 == -1?"":pattern2string.substring(dotPos2));
|
||||
boolean firstExtensionWild = (firstExtension.equals(".*") || firstExtension.equals(""));
|
||||
boolean secondExtensionWild = (secondExtension.equals(".*") || secondExtension.equals(""));
|
||||
if (!firstExtensionWild && !secondExtensionWild) {
|
||||
throw new IllegalArgumentException("Cannot combine patterns: " + patternString + " and " + pattern2string);
|
||||
}
|
||||
return file2 + (firstExtensionWild?secondExtension:firstExtension);
|
||||
}
|
||||
|
||||
/**
|
||||
* Join two paths together including a separator if necessary. Extraneous separators are removed (if the first path
|
||||
* ends with one and the second path starts with one).
|
||||
* @param path1 First path
|
||||
* @param path2 Second path
|
||||
* @return joined path that may include separator if necessary
|
||||
*/
|
||||
private String concat(String path1, String path2) {
|
||||
boolean path1EndsWithSeparator = path1.charAt(path1.length()-1)==separator;
|
||||
boolean path2StartsWithSeparator = path2.charAt(0)==separator;
|
||||
if (path1EndsWithSeparator && path2StartsWithSeparator) {
|
||||
return path1 + path2.substring(1);
|
||||
}
|
||||
else if (path1EndsWithSeparator || path2StartsWithSeparator) {
|
||||
return path1 + path2;
|
||||
}
|
||||
else {
|
||||
return path1 + separator + path2;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.web.util.patterns;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
/**
|
||||
* Basic PathPattern comparator.
|
||||
*
|
||||
* @author Andy Clement
|
||||
*/
|
||||
public class PathPatternComparator implements Comparator<PathPattern> {
|
||||
|
||||
@Override
|
||||
public int compare(PathPattern o1, PathPattern o2) {
|
||||
// Nulls get sorted to the end
|
||||
if (o1 == null) {
|
||||
return (o2==null?0:+1);
|
||||
} else if (o2 == null) {
|
||||
return -1;
|
||||
}
|
||||
return o1.compareTo(o2);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,390 @@
|
|||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.web.util.patterns;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.PatternSyntaxException;
|
||||
|
||||
/**
|
||||
* Parser for URI template patterns. It breaks the path pattern into a number of
|
||||
* path elements in a linked list.
|
||||
*
|
||||
* @author Andy Clement
|
||||
*/
|
||||
public class PathPatternParser {
|
||||
|
||||
public final static char DEFAULT_SEPARATOR = '/';
|
||||
|
||||
// The expected path separator to split path elements during parsing
|
||||
char separator = DEFAULT_SEPARATOR;
|
||||
|
||||
// Is the parser producing case sensitive PathPattern matchers
|
||||
boolean caseSensitive = true;
|
||||
|
||||
// The input data for parsing
|
||||
private char[] pathPatternData;
|
||||
|
||||
// The length of the input data
|
||||
private int pathPatternLength;
|
||||
|
||||
// Current parsing position
|
||||
int pos;
|
||||
|
||||
// How many ? characters in a particular path element
|
||||
private int singleCharWildcardCount;
|
||||
|
||||
// Is the path pattern using * characters in a particular path element
|
||||
private boolean wildcard = false;
|
||||
|
||||
// Is the construct {*...} being used in a particular path element
|
||||
private boolean isCaptureTheRestVariable = false;
|
||||
|
||||
// Has the parser entered a {...} variable capture block in a particular
|
||||
// path element
|
||||
private boolean insideVariableCapture = false;
|
||||
|
||||
// How many variable captures are occurring in a particular path element
|
||||
private int variableCaptureCount = 0;
|
||||
|
||||
// Start of the most recent path element in a particular path element
|
||||
int pathElementStart;
|
||||
|
||||
// Start of the most recent variable capture in a particular path element
|
||||
int variableCaptureStart;
|
||||
|
||||
// Variables captures in this path pattern
|
||||
List<String> capturedVariableNames;
|
||||
|
||||
// The head of the path element chain currently being built
|
||||
PathElement headPE;
|
||||
|
||||
// The most recently constructed path element in the chain
|
||||
PathElement currentPE;
|
||||
|
||||
/**
|
||||
* Default constructor, will use the default path separator to identify
|
||||
* the elements of the path pattern.
|
||||
*/
|
||||
public PathPatternParser() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a PatternParser that will use the specified separator instead of
|
||||
* the default.
|
||||
*
|
||||
* @param separator the path separator to look for when parsing.
|
||||
*/
|
||||
public PathPatternParser(char separator) {
|
||||
this.separator = separator;
|
||||
}
|
||||
|
||||
public void setCaseSensitive(boolean caseSensitive) {
|
||||
this.caseSensitive = caseSensitive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the path pattern data, a character at a time, breaking it into
|
||||
* path elements around separator boundaries and verifying the structure at each
|
||||
* stage. Produces a PathPattern object that can be used for fast matching
|
||||
* against paths.
|
||||
*
|
||||
* @param pathPattern the input path pattern, e.g. /foo/{bar}
|
||||
* @return a PathPattern for quickly matching paths against the specified path pattern
|
||||
*/
|
||||
public PathPattern parse(String pathPattern) {
|
||||
if (pathPattern == null) {
|
||||
pathPattern = "";
|
||||
}
|
||||
// int starstar = pathPattern.indexOf("**");
|
||||
// if (starstar!=-1 && starstar!=pathPattern.length()-2) {
|
||||
// throw new IllegalStateException("Not allowed ** unless at end of pattern: "+pathPattern);
|
||||
// }
|
||||
pathPatternData = pathPattern.toCharArray();
|
||||
pathPatternLength = pathPatternData.length;
|
||||
headPE = null;
|
||||
currentPE = null;
|
||||
capturedVariableNames = null;
|
||||
pathElementStart = -1;
|
||||
pos = 0;
|
||||
resetPathElementState();
|
||||
while (pos < pathPatternLength) {
|
||||
char ch = pathPatternData[pos];
|
||||
if (ch == separator) {
|
||||
if (pathElementStart != -1) {
|
||||
pushPathElement(createPathElement());
|
||||
}
|
||||
// Skip over multiple separators
|
||||
while ((pos+1) < pathPatternLength && pathPatternData[pos+1] == separator) {
|
||||
pos++;
|
||||
}
|
||||
if (peekDoubleWildcard()) {
|
||||
pushPathElement(new WildcardTheRestPathElement(pos,separator));
|
||||
pos+=2;
|
||||
} else {
|
||||
pushPathElement(new SeparatorPathElement(pos, separator));
|
||||
}
|
||||
} else {
|
||||
if (pathElementStart == -1) {
|
||||
pathElementStart = pos;
|
||||
}
|
||||
if (ch == '?') {
|
||||
singleCharWildcardCount++;
|
||||
} else if (ch == '{') {
|
||||
if (insideVariableCapture) {
|
||||
throw new PatternParseException(pos, pathPatternData, PatternMessage.ILLEGAL_NESTED_CAPTURE);
|
||||
// If we enforced that adjacent captures weren't allowed, this would do it (this would be an error: /foo/{bar}{boo}/)
|
||||
// } else if (pos > 0 && pathPatternData[pos - 1] == '}') {
|
||||
// throw new PatternParseException(pos, pathPatternData,
|
||||
// PatternMessage.CANNOT_HAVE_ADJACENT_CAPTURES);
|
||||
}
|
||||
insideVariableCapture = true;
|
||||
variableCaptureStart = pos;
|
||||
} else if (ch == '}') {
|
||||
if (!insideVariableCapture) {
|
||||
throw new PatternParseException(pos, pathPatternData, PatternMessage.MISSING_OPEN_CAPTURE);
|
||||
}
|
||||
insideVariableCapture = false;
|
||||
if (isCaptureTheRestVariable && (pos + 1) < pathPatternLength) {
|
||||
throw new PatternParseException(pos + 1, pathPatternData,
|
||||
PatternMessage.NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST);
|
||||
}
|
||||
variableCaptureCount++;
|
||||
} else if (ch == ':') {
|
||||
if (insideVariableCapture) {
|
||||
skipCaptureRegex();
|
||||
insideVariableCapture = false;
|
||||
variableCaptureCount++;
|
||||
}
|
||||
} else if (ch == '*') {
|
||||
if (insideVariableCapture) {
|
||||
if (variableCaptureStart == pos - 1) {
|
||||
isCaptureTheRestVariable = true;
|
||||
}
|
||||
}
|
||||
wildcard = true;
|
||||
}
|
||||
// Check that the characters used for captured variable names are like java identifiers
|
||||
if (insideVariableCapture) {
|
||||
if ((variableCaptureStart + 1 + (isCaptureTheRestVariable ? 1 : 0)) == pos
|
||||
&& !Character.isJavaIdentifierStart(ch)) {
|
||||
throw new PatternParseException(pos, pathPatternData,
|
||||
PatternMessage.ILLEGAL_CHARACTER_AT_START_OF_CAPTURE_DESCRIPTOR,
|
||||
Character.toString(ch));
|
||||
|
||||
} else if ((pos > (variableCaptureStart + 1 + (isCaptureTheRestVariable ? 1 : 0))
|
||||
&& !Character.isJavaIdentifierPart(ch))) {
|
||||
throw new PatternParseException(pos, pathPatternData,
|
||||
PatternMessage.ILLEGAL_CHARACTER_IN_CAPTURE_DESCRIPTOR, Character.toString(ch));
|
||||
}
|
||||
}
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
if (pathElementStart != -1) {
|
||||
pushPathElement(createPathElement());
|
||||
}
|
||||
return new PathPattern(pathPattern, headPE, separator, caseSensitive);
|
||||
}
|
||||
|
||||
/**
|
||||
* Just hit a ':' and want to jump over the regex specification for this
|
||||
* variable. pos will be pointing at the ':', we want to skip until the }.
|
||||
* <p>
|
||||
* Nested {...} pairs don't have to be escaped: <tt>/abc/{var:x{1,2}}/def</tt>
|
||||
* <p>An escaped } will not be treated as the end of the regex: <tt>/abc/{var:x\\{y:}/def</tt>
|
||||
* <p>A separator that should not indicate the end of the regex can be escaped:
|
||||
*/
|
||||
private void skipCaptureRegex() {
|
||||
pos++;
|
||||
int regexStart = pos;
|
||||
int curlyBracketDepth = 0; // how deep in nested {...} pairs
|
||||
boolean previousBackslash = false;
|
||||
while (pos < pathPatternLength) {
|
||||
char ch = pathPatternData[pos];
|
||||
if (ch == '\\' && !previousBackslash) {
|
||||
pos++;
|
||||
previousBackslash = true;
|
||||
continue;
|
||||
}
|
||||
if (ch == '{' && !previousBackslash) {
|
||||
curlyBracketDepth++;
|
||||
} else if (ch == '}' && !previousBackslash) {
|
||||
if (curlyBracketDepth == 0) {
|
||||
if (regexStart == pos) {
|
||||
throw new PatternParseException(regexStart, pathPatternData,
|
||||
PatternMessage.MISSING_REGEX_CONSTRAINT);
|
||||
}
|
||||
return;
|
||||
}
|
||||
curlyBracketDepth--;
|
||||
}
|
||||
if (ch == separator && !previousBackslash) {
|
||||
throw new PatternParseException(pos, pathPatternData, PatternMessage.MISSING_CLOSE_CAPTURE);
|
||||
}
|
||||
pos++;
|
||||
previousBackslash=false;
|
||||
}
|
||||
throw new PatternParseException(pos - 1, pathPatternData, PatternMessage.MISSING_CLOSE_CAPTURE);
|
||||
}
|
||||
|
||||
/**
|
||||
* After processing a separator, a quick peek whether it is followed by **
|
||||
* (and only ** before the end of the pattern or the next separator)
|
||||
*/
|
||||
private boolean peekDoubleWildcard() {
|
||||
if ((pos + 2) >= pathPatternLength) {
|
||||
return false;
|
||||
}
|
||||
if (pathPatternData[pos + 1] != '*' || pathPatternData[pos + 2] != '*') {
|
||||
return false;
|
||||
}
|
||||
return (pos + 3 == pathPatternLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param newPathElement the new path element to add to the chain being built
|
||||
*/
|
||||
private void pushPathElement(PathElement newPathElement) {
|
||||
if (newPathElement instanceof CaptureTheRestPathElement) {
|
||||
// There must be a separator ahead of this thing
|
||||
// currentPE SHOULD be a SeparatorPathElement
|
||||
if (currentPE == null) {
|
||||
headPE = newPathElement;
|
||||
currentPE = newPathElement;
|
||||
} else if (currentPE instanceof SeparatorPathElement) {
|
||||
PathElement peBeforeSeparator = currentPE.prev;
|
||||
if (peBeforeSeparator == null) {
|
||||
// /{*foobar} is at the start
|
||||
headPE = newPathElement;
|
||||
newPathElement.prev = peBeforeSeparator;
|
||||
} else {
|
||||
peBeforeSeparator.next = newPathElement;
|
||||
newPathElement.prev = peBeforeSeparator;
|
||||
}
|
||||
currentPE = newPathElement;
|
||||
} else {
|
||||
throw new IllegalStateException("Expected SeparatorPathElement but was "+currentPE);
|
||||
}
|
||||
} else {
|
||||
if (headPE == null) {
|
||||
headPE = newPathElement;
|
||||
currentPE = newPathElement;
|
||||
} else {
|
||||
currentPE.next = newPathElement;
|
||||
newPathElement.prev = currentPE;
|
||||
currentPE = newPathElement;
|
||||
}
|
||||
}
|
||||
resetPathElementState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Used the knowledge built up whilst processing since the last path element to determine what kind of path
|
||||
* element to create.
|
||||
* @return the new path element
|
||||
*/
|
||||
private PathElement createPathElement() {
|
||||
if (insideVariableCapture) {
|
||||
throw new PatternParseException(pos, pathPatternData, PatternMessage.MISSING_CLOSE_CAPTURE);
|
||||
}
|
||||
char[] pathElementText = new char[pos - pathElementStart];
|
||||
System.arraycopy(pathPatternData, pathElementStart, pathElementText, 0, pos - pathElementStart);
|
||||
PathElement newPE = null;
|
||||
if (variableCaptureCount > 0) {
|
||||
if (variableCaptureCount == 1 && pathElementStart == variableCaptureStart && pathPatternData[pos - 1] == '}') {
|
||||
if (isCaptureTheRestVariable) {
|
||||
// It is {*....}
|
||||
newPE = new CaptureTheRestPathElement(pathElementStart, pathElementText, separator);
|
||||
} else {
|
||||
// It is a full capture of this element (possibly with constraint), for example: /foo/{abc}/
|
||||
try {
|
||||
newPE = new CaptureVariablePathElement(pathElementStart, pathElementText, caseSensitive);
|
||||
} catch (PatternSyntaxException pse) {
|
||||
throw new PatternParseException(pse, findRegexStart(pathPatternData,pathElementStart)+pse.getIndex(), pathPatternData, PatternMessage.JDK_PATTERN_SYNTAX_EXCEPTION);
|
||||
}
|
||||
recordCapturedVariable(pathElementStart, ((CaptureVariablePathElement) newPE).getVariableName());
|
||||
}
|
||||
} else {
|
||||
if (isCaptureTheRestVariable) {
|
||||
throw new PatternParseException(pathElementStart, pathPatternData, PatternMessage.CAPTURE_ALL_IS_STANDALONE_CONSTRUCT);
|
||||
}
|
||||
RegexPathElement newRegexSection = new RegexPathElement(pathElementStart, pathElementText, caseSensitive, pathPatternData);
|
||||
for (String variableName : newRegexSection.getVariableNames()) {
|
||||
recordCapturedVariable(pathElementStart, variableName);
|
||||
}
|
||||
newPE = newRegexSection;
|
||||
}
|
||||
} else {
|
||||
if (wildcard) {
|
||||
if (pos - 1 == pathElementStart) {
|
||||
newPE = new WildcardPathElement(pathElementStart);
|
||||
} else {
|
||||
newPE = new RegexPathElement(pathElementStart, pathElementText, caseSensitive, pathPatternData);
|
||||
}
|
||||
} else if (singleCharWildcardCount!=0) {
|
||||
newPE = new SingleCharWildcardedPathElement(pathElementStart, pathElementText, singleCharWildcardCount, caseSensitive);
|
||||
} else {
|
||||
newPE = new LiteralPathElement(pathElementStart, pathElementText, caseSensitive);
|
||||
}
|
||||
}
|
||||
return newPE;
|
||||
}
|
||||
|
||||
/**
|
||||
* For a path element representing a captured variable, locate the constraint pattern.
|
||||
* Assumes there is a constraint pattern.
|
||||
* @param data a complete path expression, e.g. /aaa/bbb/{ccc:...}
|
||||
* @param offset the start of the capture pattern of interest
|
||||
* @return the index of the character after the ':' within the pattern expression relative to the start of the whole expression
|
||||
*/
|
||||
private int findRegexStart(char[] data, int offset) {
|
||||
int pos = offset;
|
||||
while (pos<data.length) {
|
||||
if (data[pos] == ':') {
|
||||
return pos + 1;
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all the flags and position markers computed during path element processing.
|
||||
*/
|
||||
private void resetPathElementState() {
|
||||
pathElementStart = -1;
|
||||
singleCharWildcardCount = 0;
|
||||
insideVariableCapture = false;
|
||||
variableCaptureCount = 0;
|
||||
wildcard = false;
|
||||
isCaptureTheRestVariable = false;
|
||||
variableCaptureStart = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a new captured variable. If it clashes with an existing one then report an error.
|
||||
*/
|
||||
private void recordCapturedVariable(int pos, String variableName) {
|
||||
if (capturedVariableNames == null) {
|
||||
capturedVariableNames = new ArrayList<>();
|
||||
}
|
||||
if (capturedVariableNames.contains(variableName)) {
|
||||
throw new PatternParseException(pos, this.pathPatternData, PatternMessage.ILLEGAL_DOUBLE_CAPTURE, variableName);
|
||||
}
|
||||
capturedVariableNames.add(variableName);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.web.util.patterns;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
/**
|
||||
* Similar to {@link PathPatternComparator} but this takes account of a specified path and
|
||||
* sorts anything that exactly matches it to be first.
|
||||
*
|
||||
* @author Andy Clement
|
||||
*/
|
||||
public class PatternComparatorConsideringPath implements Comparator<PathPattern> {
|
||||
|
||||
private String path;
|
||||
|
||||
public PatternComparatorConsideringPath(String path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compare(PathPattern o1, PathPattern o2) {
|
||||
// Nulls get sorted to the end
|
||||
if (o1 == null) {
|
||||
return (o2==null?0:+1);
|
||||
} else if (o2 == null) {
|
||||
return -1;
|
||||
}
|
||||
if (o1.getPatternString().equals(path)) {
|
||||
return (o2.getPatternString().equals(path))?0:-1;
|
||||
} else if (o2.getPatternString().equals(path)) {
|
||||
return +1;
|
||||
}
|
||||
return o1.compareTo(o2);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.web.util.patterns;
|
||||
|
||||
import java.text.MessageFormat;
|
||||
|
||||
/**
|
||||
* The messages that can be included in a {@link PatternParseException} when there is a parse failure.
|
||||
*
|
||||
* @author Andy Clement
|
||||
*/
|
||||
public enum PatternMessage {
|
||||
|
||||
// @formatter:off
|
||||
MISSING_CLOSE_CAPTURE("Expected close capture character after variable name '}'"),
|
||||
MISSING_OPEN_CAPTURE("Missing preceeding open capture character before variable name'{'"),
|
||||
ILLEGAL_NESTED_CAPTURE("Not allowed to nest variable captures"),
|
||||
CANNOT_HAVE_ADJACENT_CAPTURES("Adjacent captures are not allowed"),
|
||||
ILLEGAL_CHARACTER_AT_START_OF_CAPTURE_DESCRIPTOR("Character ''{0}'' is not allowed at start of captured variable name"),
|
||||
ILLEGAL_CHARACTER_IN_CAPTURE_DESCRIPTOR("Character ''{0}'' is not allowed in a captured variable name"),
|
||||
NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST("No more pattern data allowed after '{*...}' pattern element"),
|
||||
BADLY_FORMED_CAPTURE_THE_REST("Expected form when capturing the rest of the path is simply '{*...}'"),
|
||||
MISSING_REGEX_CONSTRAINT("Missing regex constraint on capture"),
|
||||
ILLEGAL_DOUBLE_CAPTURE("Not allowed to capture ''{0}'' twice in the same pattern"),
|
||||
JDK_PATTERN_SYNTAX_EXCEPTION("Exception occurred in pattern compilation"),
|
||||
CAPTURE_ALL_IS_STANDALONE_CONSTRUCT("'{*...}' can only be preceeded by a path separator");
|
||||
// @formatter:on
|
||||
|
||||
private final String message;
|
||||
|
||||
private PatternMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public String formatMessage(Object... inserts) {
|
||||
return MessageFormat.format(this.message, inserts);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.web.util.patterns;
|
||||
|
||||
/**
|
||||
* Exception that is thrown when there is a problem with the pattern being parsed.
|
||||
*
|
||||
* @author Andy Clement
|
||||
*/
|
||||
public class PatternParseException extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private int pos;
|
||||
|
||||
private char[] patternText;
|
||||
|
||||
private final PatternMessage message;
|
||||
|
||||
private final Object[] inserts;
|
||||
|
||||
public PatternParseException(int pos, char[] patternText, PatternMessage message, Object... inserts) {
|
||||
super(message.formatMessage(inserts));
|
||||
this.pos = pos;
|
||||
this.patternText = patternText;
|
||||
this.message = message;
|
||||
this.inserts = inserts;
|
||||
}
|
||||
|
||||
public PatternParseException(Throwable cause, int pos, char[] patternText, PatternMessage message, Object... inserts) {
|
||||
super(message.formatMessage(inserts),cause);
|
||||
this.pos = pos;
|
||||
this.patternText = patternText;
|
||||
this.message = message;
|
||||
this.inserts = inserts;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a formatted message with inserts applied
|
||||
*/
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return this.message.formatMessage(this.inserts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a detailed message that includes the original pattern text with a pointer to the error position,
|
||||
* as well as the error message.
|
||||
*/
|
||||
public String toDetailedString() {
|
||||
StringBuilder buf = new StringBuilder();
|
||||
buf.append(patternText).append('\n');
|
||||
for (int i = 0; i < pos; i++) {
|
||||
buf.append(' ');
|
||||
}
|
||||
buf.append("^\n");
|
||||
buf.append(getMessage());
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
public Object[] getInserts() {
|
||||
return this.inserts;
|
||||
}
|
||||
|
||||
public int getPosition() {
|
||||
return pos;
|
||||
}
|
||||
|
||||
public PatternMessage getMessageType() {
|
||||
return message;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.web.util.patterns;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
import org.springframework.web.util.patterns.PathPattern.MatchingContext;
|
||||
|
||||
/**
|
||||
* A regex path element. Used to represent any complicated element of the path.
|
||||
* For example in '<tt>/foo/*_*/*_{foobar}</tt>' both <tt>*_*</tt> and <tt>*_{foobar}</tt>
|
||||
* are {@link RegexPathElement} path elements. Derived from the general {@link AntPathMatcher} approach.
|
||||
*
|
||||
* @author Andy Clement
|
||||
*/
|
||||
class RegexPathElement extends PathElement {
|
||||
|
||||
private final java.util.regex.Pattern GLOB_PATTERN = java.util.regex.Pattern
|
||||
.compile("\\?|\\*|\\{((?:\\{[^/]+?\\}|[^/{}]|\\\\[{}])+?)\\}");
|
||||
|
||||
private final String DEFAULT_VARIABLE_PATTERN = "(.*)";
|
||||
|
||||
private final List<String> variableNames = new LinkedList<>();
|
||||
|
||||
private char[] regex;
|
||||
|
||||
private java.util.regex.Pattern pattern;
|
||||
|
||||
private boolean caseSensitive;
|
||||
|
||||
private int wildcardCount;
|
||||
|
||||
RegexPathElement(int pos, char[] regex, boolean caseSensitive, char[] completePattern) {
|
||||
super(pos);
|
||||
this.regex = regex;
|
||||
this.caseSensitive = caseSensitive;
|
||||
buildPattern(regex, completePattern);
|
||||
}
|
||||
|
||||
public void buildPattern(char[] regex, char[] completePattern) {
|
||||
StringBuilder patternBuilder = new StringBuilder();
|
||||
String text = new String(regex);
|
||||
Matcher matcher = GLOB_PATTERN.matcher(text);
|
||||
int end = 0;
|
||||
while (matcher.find()) {
|
||||
patternBuilder.append(quote(text, end, matcher.start()));
|
||||
String match = matcher.group();
|
||||
if ("?".equals(match)) {
|
||||
patternBuilder.append('.');
|
||||
} else if ("*".equals(match)) {
|
||||
patternBuilder.append(".*");
|
||||
wildcardCount++;
|
||||
} else if (match.startsWith("{") && match.endsWith("}")) {
|
||||
int colonIdx = match.indexOf(':');
|
||||
if (colonIdx == -1) {
|
||||
patternBuilder.append(DEFAULT_VARIABLE_PATTERN);
|
||||
String variableName = matcher.group(1);
|
||||
if (variableNames.contains(variableName)) {
|
||||
throw new PatternParseException(pos, completePattern, PatternMessage.ILLEGAL_DOUBLE_CAPTURE,
|
||||
variableName);
|
||||
}
|
||||
this.variableNames.add(variableName);
|
||||
} else {
|
||||
String variablePattern = match.substring(colonIdx + 1, match.length() - 1);
|
||||
patternBuilder.append('(');
|
||||
patternBuilder.append(variablePattern);
|
||||
patternBuilder.append(')');
|
||||
String variableName = match.substring(1, colonIdx);
|
||||
if (variableNames.contains(variableName)) {
|
||||
throw new PatternParseException(pos, completePattern, PatternMessage.ILLEGAL_DOUBLE_CAPTURE,
|
||||
variableName);
|
||||
}
|
||||
this.variableNames.add(variableName);
|
||||
}
|
||||
}
|
||||
end = matcher.end();
|
||||
}
|
||||
patternBuilder.append(quote(text, end, text.length()));
|
||||
if (caseSensitive) {
|
||||
pattern = java.util.regex.Pattern.compile(patternBuilder.toString());
|
||||
} else {
|
||||
pattern = java.util.regex.Pattern.compile(patternBuilder.toString(),
|
||||
java.util.regex.Pattern.CASE_INSENSITIVE);
|
||||
}
|
||||
}
|
||||
|
||||
public List<String> getVariableNames() {
|
||||
return variableNames;
|
||||
}
|
||||
|
||||
private String quote(String s, int start, int end) {
|
||||
if (start == end) {
|
||||
return "";
|
||||
}
|
||||
return java.util.regex.Pattern.quote(s.substring(start, end));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(int candidateIndex, MatchingContext matchingContext) {
|
||||
int p = matchingContext.scanAhead(candidateIndex);
|
||||
Matcher m = pattern.matcher(new SubSequence(matchingContext.candidate, candidateIndex, p));
|
||||
boolean matches = m.matches();
|
||||
if (matches) {
|
||||
if (next == null) {
|
||||
// No more pattern, is there more data?
|
||||
matches = (p == matchingContext.candidateLength);
|
||||
} else {
|
||||
if (matchingContext.isMatchStartMatching && p == matchingContext.candidateLength) {
|
||||
return true; // no more data but matches up to this point
|
||||
}
|
||||
matches = next.matches(p, matchingContext);
|
||||
}
|
||||
}
|
||||
if (matches && matchingContext.extractingVariables) {
|
||||
// Process captures
|
||||
if (this.variableNames.size() != m.groupCount()) { // SPR-8455
|
||||
throw new IllegalArgumentException("The number of capturing groups in the pattern segment "
|
||||
+ this.pattern + " does not match the number of URI template variables it defines, "
|
||||
+ "which can occur if capturing groups are used in a URI template regex. "
|
||||
+ "Use non-capturing groups instead.");
|
||||
}
|
||||
for (int i = 1; i <= m.groupCount(); i++) {
|
||||
String name = this.variableNames.get(i - 1);
|
||||
String value = m.group(i);
|
||||
matchingContext.set(name, value);
|
||||
}
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "Regex(" + new String(regex) + ")";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNormalizedLength() {
|
||||
int varsLength = 0;
|
||||
for (String variableName : variableNames) {
|
||||
varsLength += variableName.length();
|
||||
}
|
||||
return regex.length - varsLength - variableNames.size();
|
||||
}
|
||||
|
||||
public int getCaptureCount() {
|
||||
return variableNames.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getWildcardCount() {
|
||||
return wildcardCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getScore() {
|
||||
return getCaptureCount()*CAPTURE_VARIABLE_WEIGHT + getWildcardCount()*WILDCARD_WEIGHT;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.web.util.patterns;
|
||||
|
||||
import org.springframework.web.util.patterns.PathPattern.MatchingContext;
|
||||
|
||||
/**
|
||||
* A separator path element. In the pattern '/foo/bar' the two occurrences
|
||||
* of '/' will be represented by a SeparatorPathElement (if the default
|
||||
* separator of '/' is being used).
|
||||
*
|
||||
* @author Andy Clement
|
||||
*/
|
||||
class SeparatorPathElement extends PathElement {
|
||||
|
||||
private char separator;
|
||||
|
||||
SeparatorPathElement(int pos, char separator) {
|
||||
super(pos);
|
||||
this.separator = separator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Matching a separator is easy, basically the character at candidateIndex
|
||||
* must be the separator.
|
||||
*/
|
||||
@Override
|
||||
public boolean matches(int candidateIndex, MatchingContext matchingContext) {
|
||||
boolean matched = false;
|
||||
if (candidateIndex < matchingContext.candidateLength) {
|
||||
if (matchingContext.candidate[candidateIndex] == separator) {
|
||||
// Skip further separators in the path (they are all 'matched'
|
||||
// by a single SeparatorPathElement)
|
||||
while ((candidateIndex+1)<matchingContext.candidateLength &&
|
||||
matchingContext.candidate[candidateIndex+1] == separator) {
|
||||
candidateIndex++;
|
||||
}
|
||||
if (next == null) {
|
||||
matched = ((candidateIndex + 1) == matchingContext.candidateLength);
|
||||
} else {
|
||||
candidateIndex++;
|
||||
if (matchingContext.isMatchStartMatching && candidateIndex == matchingContext.candidateLength) {
|
||||
return true; // no more data but matches up to this point
|
||||
}
|
||||
matched = next.matches(candidateIndex, matchingContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
return matched;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "Separator(" + separator + ")";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNormalizedLength() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.web.util.patterns;
|
||||
|
||||
import org.springframework.web.util.patterns.PathPattern.MatchingContext;
|
||||
|
||||
/**
|
||||
* A literal path element that does includes the single character wildcard '?' one
|
||||
* or more times (to basically many any character at that position).
|
||||
*
|
||||
* @author Andy Clement
|
||||
*/
|
||||
class SingleCharWildcardedPathElement extends PathElement {
|
||||
|
||||
private char[] text;
|
||||
|
||||
private int len;
|
||||
|
||||
private int questionMarkCount;
|
||||
|
||||
private boolean caseSensitive;
|
||||
|
||||
public SingleCharWildcardedPathElement(int pos, char[] literalText, int questionMarkCount, boolean caseSensitive) {
|
||||
super(pos);
|
||||
this.len = literalText.length;
|
||||
this.questionMarkCount = questionMarkCount;
|
||||
this.caseSensitive = caseSensitive;
|
||||
if (caseSensitive) {
|
||||
this.text = literalText;
|
||||
} else {
|
||||
this.text = new char[literalText.length];
|
||||
for (int i = 0; i < len; i++) {
|
||||
this.text[i] = Character.toLowerCase(literalText[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(int candidateIndex, MatchingContext matchingContext) {
|
||||
if (matchingContext.candidateLength < (candidateIndex + len)) {
|
||||
return false; // There isn't enough data to match
|
||||
}
|
||||
char[] candidate = matchingContext.candidate;
|
||||
if (caseSensitive) {
|
||||
for (int i = 0; i < len; i++) {
|
||||
char t = text[i];
|
||||
if (t != '?' && candidate[candidateIndex] != t) {
|
||||
return false;
|
||||
}
|
||||
candidateIndex++;
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i < len; i++) {
|
||||
char t = text[i];
|
||||
if (t != '?' && Character.toLowerCase(candidate[candidateIndex]) != t) {
|
||||
return false;
|
||||
}
|
||||
candidateIndex++;
|
||||
}
|
||||
}
|
||||
if (next == null) {
|
||||
return candidateIndex == matchingContext.candidateLength;
|
||||
} else {
|
||||
if (matchingContext.isMatchStartMatching && candidateIndex == matchingContext.candidateLength) {
|
||||
return true; // no more data but matches up to this point
|
||||
}
|
||||
return next.matches(candidateIndex, matchingContext);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getWildcardCount() {
|
||||
return questionMarkCount;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "SingleCharWildcarding(" + new String(text) + ")";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNormalizedLength() {
|
||||
return len;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.web.util.patterns;
|
||||
|
||||
/**
|
||||
* Used to represent a subsection of an array, useful when wanting to pass that subset of data
|
||||
* to another method (e.g. a java regex matcher) but not wanting to create a new string object to hold
|
||||
* all that data.
|
||||
*
|
||||
* @author Andy Clement
|
||||
*/
|
||||
class SubSequence implements CharSequence {
|
||||
|
||||
private char[] chars;
|
||||
private int start, end;
|
||||
|
||||
SubSequence(char[] chars, int start, int end) {
|
||||
this.chars = chars;
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int length() {
|
||||
return end - start;
|
||||
}
|
||||
|
||||
@Override
|
||||
public char charAt(int index) {
|
||||
return chars[start + index];
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence subSequence(int start, int end) {
|
||||
return new SubSequence(chars, this.start + start, this.start + end);
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return new String(chars,start,end-start);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.web.util.patterns;
|
||||
|
||||
import org.springframework.web.util.patterns.PathPattern.MatchingContext;
|
||||
|
||||
/**
|
||||
* A wildcard path element. In the pattern '/foo/*/goo' the * is
|
||||
* represented by a WildcardPathElement.
|
||||
*
|
||||
* @author Andy Clement
|
||||
*/
|
||||
class WildcardPathElement extends PathElement {
|
||||
|
||||
public WildcardPathElement(int pos) {
|
||||
super(pos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Matching on a WildcardPathElement is quite straight forward. Just scan the
|
||||
* candidate from the candidateIndex for the next separator or the end of the
|
||||
* candidate.
|
||||
*/
|
||||
@Override
|
||||
public boolean matches(int candidateIndex, MatchingContext matchingContext) {
|
||||
int nextPos = matchingContext.scanAhead(candidateIndex);
|
||||
if (next == null) {
|
||||
return (nextPos == matchingContext.candidateLength);
|
||||
} else {
|
||||
if (matchingContext.isMatchStartMatching && nextPos == matchingContext.candidateLength) {
|
||||
return true; // no more data but matches up to this point
|
||||
}
|
||||
return next.matches(nextPos, matchingContext);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNormalizedLength() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "Wildcard(*)";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getWildcardCount() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getScore() {
|
||||
return WILDCARD_WEIGHT;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright 2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.web.util.patterns;
|
||||
|
||||
import org.springframework.web.util.patterns.PathPattern.MatchingContext;
|
||||
|
||||
/**
|
||||
* A path element representing wildcarding the rest of a path. In the pattern
|
||||
* '/foo/**' the /** is represented as a {@link WildcardTheRestPathElement}.
|
||||
*
|
||||
* @author Andy Clement
|
||||
*/
|
||||
class WildcardTheRestPathElement extends PathElement {
|
||||
|
||||
private char separator;
|
||||
|
||||
WildcardTheRestPathElement(int pos, char separator) {
|
||||
super(pos);
|
||||
this.separator = separator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(int candidateIndex, MatchingContext matchingContext) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "WildcardTheRest("+separator+"**)";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNormalizedLength() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getWildcardCount() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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<>();
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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<>();
|
||||
|
||||
|
|
|
|||
|
|
@ -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<>();
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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<>();
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue