Issue: SPR-14544
This commit is contained in:
Brian Clozel 2017-02-08 13:58:23 +01:00
parent f786feb5e1
commit a4da313a0a
19 changed files with 600 additions and 465 deletions

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2016 the original author or authors. * Copyright 2002-2017 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.web.util; package org.springframework.web.util;
import java.util.Comparator; import java.util.Comparator;
@ -26,16 +27,22 @@ import org.springframework.web.util.patterns.PatternComparatorConsideringPath;
/** /**
* * {@link PathMatcher} implementation for path patterns parsed
* as {@link PathPatternParser} and compiled as {@link PathPattern}s.
*
* <p>Once parsed, {@link PathPattern}s are tailored for fast matching
* and quick comparison.
*
* @author Andy Clement * @author Andy Clement
* @since 5.0 * @since 5.0
* @see PathPattern
*/ */
public class ParsingPathMatcher implements PathMatcher { public class ParsingPathMatcher implements PathMatcher {
Map<String,PathPattern> cache = new HashMap<>(); Map<String, PathPattern> cache = new HashMap<>();
PathPatternParser parser; PathPatternParser parser;
public ParsingPathMatcher() { public ParsingPathMatcher() {
parser = new PathPatternParser(); parser = new PathPatternParser();
} }
@ -74,27 +81,28 @@ public class ParsingPathMatcher implements PathMatcher {
public Comparator<String> getPatternComparator(String path) { public Comparator<String> getPatternComparator(String path) {
return new PathPatternStringComparatorConsideringPath(path); return new PathPatternStringComparatorConsideringPath(path);
} }
class PathPatternStringComparatorConsideringPath implements Comparator<String> { class PathPatternStringComparatorConsideringPath implements Comparator<String> {
PatternComparatorConsideringPath ppcp; PatternComparatorConsideringPath ppcp;
public PathPatternStringComparatorConsideringPath(String path) { public PathPatternStringComparatorConsideringPath(String path) {
ppcp = new PatternComparatorConsideringPath(path); ppcp = new PatternComparatorConsideringPath(path);
} }
@Override @Override
public int compare(String o1, String o2) { public int compare(String o1, String o2) {
if (o1 == null) { if (o1 == null) {
return (o2==null?0:+1); return (o2 == null ? 0 : +1);
} else if (o2 == null) { }
else if (o2 == null) {
return -1; return -1;
} }
PathPattern p1 = getPathPattern(o1); PathPattern p1 = getPathPattern(o1);
PathPattern p2 = getPathPattern(o2); PathPattern p2 = getPathPattern(o2);
return ppcp.compare(p1,p2); return ppcp.compare(p1, p2);
} }
} }
@Override @Override

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2016 the original author or authors. * Copyright 2002-2017 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.web.util.patterns; package org.springframework.web.util.patterns;
import org.springframework.web.util.patterns.PathPattern.MatchingContext; import org.springframework.web.util.patterns.PathPattern.MatchingContext;
@ -20,13 +21,13 @@ import org.springframework.web.util.patterns.PathPattern.MatchingContext;
/** /**
* A path element representing capturing the rest of a path. In the pattern * A path element representing capturing the rest of a path. In the pattern
* '/foo/{*foobar}' the /{*foobar} is represented as a {@link CaptureTheRestPathElement}. * '/foo/{*foobar}' the /{*foobar} is represented as a {@link CaptureTheRestPathElement}.
* *
* @author Andy Clement * @author Andy Clement
*/ */
class CaptureTheRestPathElement extends PathElement { class CaptureTheRestPathElement extends PathElement {
private String variableName; private String variableName;
private char separator; private char separator;
/** /**
@ -45,14 +46,14 @@ class CaptureTheRestPathElement extends PathElement {
// No need to handle 'match start' checking as this captures everything // No need to handle 'match start' checking as this captures everything
// anyway and cannot be followed by anything else // anyway and cannot be followed by anything else
// assert next == null // assert next == null
// If there is more data, it must start with the separator // If there is more data, it must start with the separator
if (candidateIndex<matchingContext.candidateLength && if (candidateIndex < matchingContext.candidateLength &&
matchingContext.candidate[candidateIndex] != separator) { matchingContext.candidate[candidateIndex] != separator) {
return false; return false;
} }
while ((candidateIndex+1)<matchingContext.candidateLength && while ((candidateIndex + 1) < matchingContext.candidateLength &&
matchingContext.candidate[candidateIndex+1] == separator) { matchingContext.candidate[candidateIndex + 1] == separator) {
candidateIndex++; candidateIndex++;
} }
if (matchingContext.extractingVariables) { if (matchingContext.extractingVariables) {

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2016 the original author or authors. * Copyright 2002-2017 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.web.util.patterns; package org.springframework.web.util.patterns;
import java.util.regex.Matcher; import java.util.regex.Matcher;
@ -22,15 +23,15 @@ import org.springframework.web.util.patterns.PathPattern.MatchingContext;
/** /**
* A path element representing capturing a piece of the path as a variable. In the pattern * A path element representing capturing a piece of the path as a variable. In the pattern
* '/foo/{bar}/goo' the {bar} is represented as a {@link CaptureVariablePathElement}. * '/foo/{bar}/goo' the {bar} is represented as a {@link CaptureVariablePathElement}.
* *
* @author Andy Clement * @author Andy Clement
*/ */
class CaptureVariablePathElement extends PathElement { class CaptureVariablePathElement extends PathElement {
private String variableName; private String variableName;
private java.util.regex.Pattern constraintPattern; private java.util.regex.Pattern constraintPattern;
/** /**
* @param pos the position in the pattern of this capture element * @param pos the position in the pattern of this capture element
* @param captureDescriptor is of the form {AAAAA[:pattern]} * @param captureDescriptor is of the form {AAAAA[:pattern]}
@ -47,12 +48,14 @@ class CaptureVariablePathElement extends PathElement {
if (colon == -1) { if (colon == -1) {
// no constraint // no constraint
variableName = new String(captureDescriptor, 1, captureDescriptor.length - 2); variableName = new String(captureDescriptor, 1, captureDescriptor.length - 2);
} else { }
else {
variableName = new String(captureDescriptor, 1, colon - 1); variableName = new String(captureDescriptor, 1, colon - 1);
if (caseSensitive) { if (caseSensitive) {
constraintPattern = java.util.regex.Pattern constraintPattern = java.util.regex.Pattern
.compile(new String(captureDescriptor, colon + 1, captureDescriptor.length - colon - 2)); .compile(new String(captureDescriptor, colon + 1, captureDescriptor.length - colon - 2));
} else { }
else {
constraintPattern = java.util.regex.Pattern.compile( constraintPattern = java.util.regex.Pattern.compile(
new String(captureDescriptor, colon + 1, captureDescriptor.length - colon - 2), new String(captureDescriptor, colon + 1, captureDescriptor.length - colon - 2),
java.util.regex.Pattern.CASE_INSENSITIVE); java.util.regex.Pattern.CASE_INSENSITIVE);
@ -69,7 +72,7 @@ class CaptureVariablePathElement extends PathElement {
candidateCapture = new SubSequence(matchingContext.candidate, candidateIndex, nextPos); candidateCapture = new SubSequence(matchingContext.candidate, candidateIndex, nextPos);
Matcher m = constraintPattern.matcher(candidateCapture); Matcher m = constraintPattern.matcher(candidateCapture);
if (m.groupCount() != 0) { if (m.groupCount() != 0) {
throw new IllegalArgumentException("No capture groups allowed in the constraint regex: "+constraintPattern.pattern()); throw new IllegalArgumentException("No capture groups allowed in the constraint regex: " + constraintPattern.pattern());
} }
if (!m.matches()) { if (!m.matches()) {
return false; return false;
@ -78,10 +81,12 @@ class CaptureVariablePathElement extends PathElement {
boolean match = false; boolean match = false;
if (next == null) { if (next == null) {
match = (nextPos == matchingContext.candidateLength); match = (nextPos == matchingContext.candidateLength);
} else { }
else {
if (matchingContext.isMatchStartMatching && nextPos == matchingContext.candidateLength) { if (matchingContext.isMatchStartMatching && nextPos == matchingContext.candidateLength) {
match = true; // no more data but matches up to this point match = true; // no more data but matches up to this point
} else { }
else {
match = next.matches(nextPos, matchingContext); match = next.matches(nextPos, matchingContext);
} }
} }
@ -90,7 +95,7 @@ class CaptureVariablePathElement extends PathElement {
} }
return match; return match;
} }
public String getVariableName() { public String getVariableName() {
return this.variableName; return this.variableName;
} }
@ -98,7 +103,7 @@ class CaptureVariablePathElement extends PathElement {
public String toString() { public String toString() {
return "CaptureVariable({" + variableName + (constraintPattern == null ? "" : ":" + constraintPattern.pattern()) + "})"; return "CaptureVariable({" + variableName + (constraintPattern == null ? "" : ":" + constraintPattern.pattern()) + "})";
} }
@Override @Override
public int getNormalizedLength() { public int getNormalizedLength() {
return 1; return 1;
@ -113,7 +118,7 @@ class CaptureVariablePathElement extends PathElement {
public int getCaptureCount() { public int getCaptureCount() {
return 1; return 1;
} }
@Override @Override
public int getScore() { public int getScore() {
return CAPTURE_VARIABLE_WEIGHT; return CAPTURE_VARIABLE_WEIGHT;

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2016 the original author or authors. * Copyright 2002-2017 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.web.util.patterns; package org.springframework.web.util.patterns;
import org.springframework.web.util.patterns.PathPattern.MatchingContext; import org.springframework.web.util.patterns.PathPattern.MatchingContext;
@ -20,15 +21,15 @@ import org.springframework.web.util.patterns.PathPattern.MatchingContext;
/** /**
* A literal path element. In the pattern '/foo/bar/goo' there are three * A literal path element. In the pattern '/foo/bar/goo' there are three
* literal path elements 'foo', 'bar' and 'goo'. * literal path elements 'foo', 'bar' and 'goo'.
* *
* @author Andy Clement * @author Andy Clement
*/ */
class LiteralPathElement extends PathElement { class LiteralPathElement extends PathElement {
private char[] text; private char[] text;
private int len; private int len;
private boolean caseSensitive; private boolean caseSensitive;
public LiteralPathElement(int pos, char[] literalText, boolean caseSensitive) { public LiteralPathElement(int pos, char[] literalText, boolean caseSensitive) {
@ -37,7 +38,8 @@ class LiteralPathElement extends PathElement {
this.caseSensitive = caseSensitive; this.caseSensitive = caseSensitive;
if (caseSensitive) { if (caseSensitive) {
this.text = literalText; this.text = literalText;
} else { }
else {
// Force all the text lower case to make matching faster // Force all the text lower case to make matching faster
this.text = new char[literalText.length]; this.text = new char[literalText.length];
for (int i = 0; i < len; i++) { for (int i = 0; i < len; i++) {
@ -57,7 +59,8 @@ class LiteralPathElement extends PathElement {
return false; return false;
} }
} }
} else { }
else {
for (int i = 0; i < len; i++) { for (int i = 0; i < len; i++) {
// TODO revisit performance if doing a lot of case insensitive matching // TODO revisit performance if doing a lot of case insensitive matching
if (Character.toLowerCase(matchingContext.candidate[candidateIndex++]) != text[i]) { if (Character.toLowerCase(matchingContext.candidate[candidateIndex++]) != text[i]) {
@ -67,7 +70,8 @@ class LiteralPathElement extends PathElement {
} }
if (next == null) { if (next == null) {
return candidateIndex == matchingContext.candidateLength; return candidateIndex == matchingContext.candidateLength;
} else { }
else {
if (matchingContext.isMatchStartMatching && candidateIndex == matchingContext.candidateLength) { if (matchingContext.isMatchStartMatching && candidateIndex == matchingContext.candidateLength) {
return true; // no more data but everything matched so far return true; // no more data but everything matched so far
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2016 the original author or authors. * Copyright 2002-2017 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -13,31 +13,34 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.web.util.patterns; package org.springframework.web.util.patterns;
import org.springframework.web.util.patterns.PathPattern.MatchingContext; import org.springframework.web.util.patterns.PathPattern.MatchingContext;
/** /**
* Common supertype for the Ast nodes created to represent a path pattern. * Common supertype for the Ast nodes created to represent a path pattern.
* *
* @author Andy Clement * @author Andy Clement
* @since 5.0
*/ */
abstract class PathElement { abstract class PathElement {
// Score related // Score related
protected static final int WILDCARD_WEIGHT = 100; protected static final int WILDCARD_WEIGHT = 100;
protected static final int CAPTURE_VARIABLE_WEIGHT = 1; protected static final int CAPTURE_VARIABLE_WEIGHT = 1;
/** /**
* Position in the pattern where this path element starts * Position in the pattern where this path element starts
*/ */
protected int pos; protected int pos;
/** /**
* The next path element in the chain * The next path element in the chain
*/ */
protected PathElement next; protected PathElement next;
/** /**
* The previous path element in the chain * The previous path element in the chain
*/ */
@ -53,7 +56,7 @@ abstract class PathElement {
/** /**
* Attempt to match this path element. * Attempt to match this path element.
* *
* @param candidatePos the current position within the candidate path * @param candidatePos the current position within the candidate path
* @param matchingContext encapsulates context for the match including the candidate * @param matchingContext encapsulates context for the match including the candidate
* @return true if matches, otherwise false * @return true if matches, otherwise false
@ -64,7 +67,7 @@ abstract class PathElement {
* @return the length of the path element where captures are considered to be one character long * @return the length of the path element where captures are considered to be one character long
*/ */
public abstract int getNormalizedLength(); public abstract int getNormalizedLength();
/** /**
* @return the number of variables captured by the path element * @return the number of variables captured by the path element
*/ */
@ -78,7 +81,7 @@ abstract class PathElement {
public int getWildcardCount() { public int getWildcardCount() {
return 0; return 0;
} }
/** /**
* @return the score for this PathElement, combined score is used to compare parsed patterns. * @return the score for this PathElement, combined score is used to compare parsed patterns.
*/ */

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2016 the original author or authors. * Copyright 2002-2017 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.web.util.patterns; package org.springframework.web.util.patterns;
import java.util.Collections; import java.util.Collections;
@ -25,19 +26,49 @@ import org.springframework.util.PathMatcher;
* Represents a parsed path pattern. Includes a chain of path elements * Represents a parsed path pattern. Includes a chain of path elements
* for fast matching and accumulates computed state for quick comparison of * for fast matching and accumulates computed state for quick comparison of
* patterns. * patterns.
* *
* <p>PathPatterns match URL paths using the following rules:<br>
* <ul>
* <li>{@code ?} matches one character</li>
* <li>{@code *} matches zero or more characters within a path segment</li>
* <li>{@code **} matches zero or more <em>path segments</em> until the end of the path</li>
* <li>{@code {spring}} matches a <em>path segment</em> and captures it as a variable named "spring"</li>
* <li>{@code {spring:[a-z]+}} matches the regexp {@code [a-z]+} as a path variable named "spring"</li>
* <li>{@code {*spring}} matches zero or more <em>path segments</em> until the end of the path
* and captures it as a variable named "spring"</li>
* </ul>
*
* <h3>Examples</h3>
* <ul>
* <li>{@code /pages/t?st.html} &mdash; matches {@code /pages/test.html} but also
* {@code /pages/tast.html} but not {@code /pages/toast.html}</li>
* <li>{@code /resources/*.png} &mdash; matches all {@code .png} files in the
* {@code resources} directory</li>
* <li><code>/resources/&#42;&#42;</code> &mdash; matches all files
* underneath the {@code /resources/} path, including {@code /resources/image.png}
* and {@code /resources/css/spring.css}</li>
* <li><code>/resources/{&#42;path}</code> &mdash; matches all files
* underneath the {@code /resources/} path and captures their relative path in
* a variable named "path"; {@code /resources/image.png} will match with
* "spring" -> "/image.png", and {@code /resources/css/spring.css} will match
* with "spring" -> "/css/spring.css"</li>
* <li>{@code /resources/{filename:\\w+}.dat} will match {@code /resources/spring.dat}
* and assign the value {@code "spring"} to the {@code filename} variable</li>
* </ul>
*
* @author Andy Clement * @author Andy Clement
* @since 5.0
*/ */
public class PathPattern implements Comparable<PathPattern> { public class PathPattern implements Comparable<PathPattern> {
private final static Map<String,String> NO_VARIABLES_MAP = Collections.emptyMap(); private final static Map<String, String> NO_VARIABLES_MAP = Collections.emptyMap();
/** First path element in the parsed chain of path elements for this pattern */ /** First path element in the parsed chain of path elements for this pattern */
private PathElement head; private PathElement head;
/** The text of the parsed pattern */ /** The text of the parsed pattern */
private String patternString; private String patternString;
/** The separator used when parsing the pattern */ /** The separator used when parsing the pattern */
private char separator; private char separator;
@ -54,7 +85,7 @@ public class PathPattern implements Comparable<PathPattern> {
* Useful when comparing two patterns. * Useful when comparing two patterns.
*/ */
int normalizedLength; int normalizedLength;
/** /**
* Does the pattern end with '&lt;separator&gt;*' * Does the pattern end with '&lt;separator&gt;*'
*/ */
@ -87,8 +118,9 @@ public class PathPattern implements Comparable<PathPattern> {
if (s instanceof CaptureTheRestPathElement || s instanceof WildcardTheRestPathElement) { if (s instanceof CaptureTheRestPathElement || s instanceof WildcardTheRestPathElement) {
this.isCatchAll = true; this.isCatchAll = true;
} }
if (s instanceof SeparatorPathElement && s.next!=null && s.next instanceof WildcardPathElement && s.next.next == null) { if (s instanceof SeparatorPathElement && s.next != null
this.endsWithSeparatorWildcard=true; && s.next instanceof WildcardPathElement && s.next.next == null) {
this.endsWithSeparatorWildcard = true;
} }
s = s.next; s = s.next;
} }
@ -101,28 +133,31 @@ public class PathPattern implements Comparable<PathPattern> {
public boolean matches(String path) { public boolean matches(String path) {
if (head == null) { if (head == null) {
return (path == null) || (path.length() == 0); return (path == null) || (path.length() == 0);
} else if (path == null || path.length() == 0) { }
else if (path == null || path.length() == 0) {
if (head instanceof WildcardTheRestPathElement || head instanceof CaptureTheRestPathElement) { if (head instanceof WildcardTheRestPathElement || head instanceof CaptureTheRestPathElement) {
path = ""; // Will allow CaptureTheRest to bind the variable to empty path = ""; // Will allow CaptureTheRest to bind the variable to empty
} else { }
else {
return false; return false;
} }
} }
MatchingContext matchingContext = new MatchingContext(path,false); MatchingContext matchingContext = new MatchingContext(path, false);
return head.matches(0, matchingContext); return head.matches(0, matchingContext);
} }
/** /**
* @param path the path to check against the pattern * @param path the path to check against the pattern
* @return true if the pattern matches as much of the path as is supplied * @return true if the pattern matches as much of the path as is supplied
*/ */
public boolean matchStart(String path) { public boolean matchStart(String path) {
if (head == null) { if (head == null) {
return (path==null || path.length() == 0); return (path == null || path.length() == 0);
} else if (path == null || path.length() == 0) { }
else if (path == null || path.length() == 0) {
return true; return true;
} }
MatchingContext matchingContext = new MatchingContext(path,false); MatchingContext matchingContext = new MatchingContext(path, false);
matchingContext.setMatchStartMatching(true); matchingContext.setMatchStartMatching(true);
return head.matches(0, matchingContext); return head.matches(0, matchingContext);
} }
@ -132,14 +167,17 @@ public class PathPattern implements Comparable<PathPattern> {
* @return a map of extracted variables - an empty map if no variables extracted. * @return a map of extracted variables - an empty map if no variables extracted.
*/ */
public Map<String, String> matchAndExtract(String path) { public Map<String, String> matchAndExtract(String path) {
MatchingContext matchingContext = new MatchingContext(path,true); MatchingContext matchingContext = new MatchingContext(path, true);
if (head != null && head.matches(0, matchingContext)) { if (head != null && head.matches(0, matchingContext)) {
return matchingContext.getExtractedVariables(); return matchingContext.getExtractedVariables();
} else { }
if (path== null || path.length()==0) { else {
if (path == null || path.length() == 0) {
return NO_VARIABLES_MAP; return NO_VARIABLES_MAP;
} else { }
throw new IllegalStateException("Pattern \"" + this.toString() + "\" is not a match for \"" + path + "\""); else {
throw new IllegalStateException("Pattern \"" + this.toString()
+ "\" is not a match for \"" + path + "\"");
} }
} }
} }
@ -150,7 +188,7 @@ public class PathPattern implements Comparable<PathPattern> {
public String getPatternString() { public String getPatternString() {
return patternString; return patternString;
} }
public PathElement getHeadSection() { public PathElement getHeadSection() {
return head; return head;
} }
@ -168,17 +206,18 @@ public class PathPattern implements Comparable<PathPattern> {
* the returned path. * the returned path.
* @param path a path that matches this pattern * @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 * @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) { public String extractPathWithinPattern(String path) {
// assert this.matches(path) // assert this.matches(path)
PathElement s = head; PathElement s = head;
int separatorCount = 0; int separatorCount = 0;
// Find first path element that is pattern based // Find first path element that is pattern based
while (s != null) { while (s != null) {
if (s instanceof SeparatorPathElement || s instanceof CaptureTheRestPathElement || s instanceof WildcardTheRestPathElement) { if (s instanceof SeparatorPathElement || s instanceof CaptureTheRestPathElement
|| s instanceof WildcardTheRestPathElement) {
separatorCount++; separatorCount++;
} }
if (s.getWildcardCount()!=0 || s.getCaptureCount()!=0) { if (s.getWildcardCount() != 0 || s.getCaptureCount() != 0) {
break; break;
} }
s = s.next; s = s.next;
@ -201,7 +240,7 @@ public class PathPattern implements Comparable<PathPattern> {
} }
int end = len; int end = len;
// Trim trailing separators // Trim trailing separators
while (path.charAt(end-1) == separator) { while (path.charAt(end - 1) == separator) {
end--; end--;
} }
// Check if multiple separators embedded in the resulting path, if so trim them out. // Check if multiple separators embedded in the resulting path, if so trim them out.
@ -209,19 +248,19 @@ public class PathPattern implements Comparable<PathPattern> {
// The stringWithDuplicateSeparatorsRemoved is only computed if necessary // The stringWithDuplicateSeparatorsRemoved is only computed if necessary
int c = pos; int c = pos;
StringBuilder stringWithDuplicateSeparatorsRemoved = null; StringBuilder stringWithDuplicateSeparatorsRemoved = null;
while (c<end) { while (c < end) {
char ch = path.charAt(c); char ch = path.charAt(c);
if (ch == separator) { if (ch == separator) {
if ((c+1)<end && path.charAt(c+1)==separator) { if ((c + 1) < end && path.charAt(c + 1) == separator) {
// multiple separators // multiple separators
if (stringWithDuplicateSeparatorsRemoved == null) { if (stringWithDuplicateSeparatorsRemoved == null) {
// first time seen, need to capture all data up to this point // first time seen, need to capture all data up to this point
stringWithDuplicateSeparatorsRemoved = new StringBuilder(); stringWithDuplicateSeparatorsRemoved = new StringBuilder();
stringWithDuplicateSeparatorsRemoved.append(path.substring(pos,c)); stringWithDuplicateSeparatorsRemoved.append(path.substring(pos, c));
} }
do { do {
c++; c++;
} while ((c+1)<end && path.charAt(c+1)==separator); } while ((c + 1) < end && path.charAt(c + 1) == separator);
} }
} }
if (stringWithDuplicateSeparatorsRemoved != null) { if (stringWithDuplicateSeparatorsRemoved != null) {
@ -232,9 +271,9 @@ public class PathPattern implements Comparable<PathPattern> {
if (stringWithDuplicateSeparatorsRemoved != null) { if (stringWithDuplicateSeparatorsRemoved != null) {
return stringWithDuplicateSeparatorsRemoved.toString(); return stringWithDuplicateSeparatorsRemoved.toString();
} }
return pos == len ? "" : path.substring(pos,end); return pos == len ? "" : path.substring(pos, end);
} }
/** /**
* Compare this pattern with a supplied pattern. Return -1,0,+1 if this pattern * 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. * is more specific, the same or less specific than the supplied pattern.
@ -254,10 +293,12 @@ public class PathPattern implements Comparable<PathPattern> {
if (lenDifference != 0) { if (lenDifference != 0) {
return (lenDifference < 0) ? +1 : -1; return (lenDifference < 0) ? +1 : -1;
} }
} else { }
else {
return +1; return +1;
} }
} else if (p.isCatchAll()) { }
else if (p.isCatchAll()) {
return -1; return -1;
} }
// 3) This will sort such that if they differ in terms of wildcards or // 3) This will sort such that if they differ in terms of wildcards or
@ -306,7 +347,7 @@ public class PathPattern implements Comparable<PathPattern> {
public String toChainString() { public String toChainString() {
StringBuilder buf = new StringBuilder(); StringBuilder buf = new StringBuilder();
PathElement pe = head; PathElement pe = head;
while (pe!=null) { while (pe != null) {
buf.append(pe.toString()).append(" "); buf.append(pe.toString()).append(" ");
pe = pe.next; pe = pe.next;
} }
@ -320,7 +361,7 @@ public class PathPattern implements Comparable<PathPattern> {
public int getCapturedVariableCount() { public int getCapturedVariableCount() {
return capturedVariableCount; return capturedVariableCount;
} }
public String toString() { public String toString() {
return patternString; return patternString;
} }
@ -340,7 +381,7 @@ public class PathPattern implements Comparable<PathPattern> {
boolean isMatchStartMatching = false; boolean isMatchStartMatching = false;
private Map<String,String> extractedVariables; private Map<String, String> extractedVariables;
public boolean extractingVariables; public boolean extractingVariables;
@ -361,10 +402,11 @@ public class PathPattern implements Comparable<PathPattern> {
extractedVariables.put(key, value); extractedVariables.put(key, value);
} }
public Map<String,String> getExtractedVariables() { public Map<String, String> getExtractedVariables() {
if (this.extractedVariables == null) { if (this.extractedVariables == null) {
return NO_VARIABLES_MAP; return NO_VARIABLES_MAP;
} else { }
else {
return this.extractedVariables; return this.extractedVariables;
} }
} }
@ -372,7 +414,7 @@ public class PathPattern implements Comparable<PathPattern> {
/** /**
* Scan ahead from the specified position for either the next separator * Scan ahead from the specified position for either the next separator
* character or the end of the candidate. * character or the end of the candidate.
* *
* @param pos the starting position for the scan * @param pos the starting position for the scan
* @return the position of the next separator or the end of the candidate * @return the position of the next separator or the end of the candidate
*/ */
@ -392,62 +434,65 @@ public class PathPattern implements Comparable<PathPattern> {
*/ */
public String combine(String pattern2string) { public String combine(String pattern2string) {
// If one of them is empty the result is the other. If both empty the result is "" // If one of them is empty the result is the other. If both empty the result is ""
if (patternString == null || patternString.length()==0) { if (patternString == null || patternString.length() == 0) {
if (pattern2string == null || pattern2string.length()==0) { if (pattern2string == null || pattern2string.length() == 0) {
return ""; return "";
} else { }
else {
return pattern2string; return pattern2string;
} }
} else if (pattern2string == null || pattern2string.length()==0) { }
else if (pattern2string == null || pattern2string.length() == 0) {
return patternString; return patternString;
} }
// /* + /hotel => /hotel // /* + /hotel => /hotel
// /*.* + /*.html => /*.html // /*.* + /*.html => /*.html
// However: // However:
// /usr + /user => /usr/user // /usr + /user => /usr/user
// /{foo} + /bar => /{foo}/bar // /{foo} + /bar => /{foo}/bar
if (!patternString.equals(pattern2string) && capturedVariableCount==0 && matches(pattern2string)) { if (!patternString.equals(pattern2string) && capturedVariableCount == 0 && matches(pattern2string)) {
return pattern2string; return pattern2string;
} }
// /hotels/* + /booking => /hotels/booking // /hotels/* + /booking => /hotels/booking
// /hotels/* + booking => /hotels/booking // /hotels/* + booking => /hotels/booking
if (endsWithSeparatorWildcard) { if (endsWithSeparatorWildcard) {
return concat(patternString.substring(0,patternString.length()-2), pattern2string); return concat(patternString.substring(0, patternString.length() - 2), pattern2string);
} }
// /hotels + /booking => /hotels/booking // /hotels + /booking => /hotels/booking
// /hotels + booking => /hotels/booking // /hotels + booking => /hotels/booking
int starDotPos1 = patternString.indexOf("*."); // Are there any file prefix/suffix things to consider? int starDotPos1 = patternString.indexOf("*."); // Are there any file prefix/suffix things to consider?
if (capturedVariableCount!=0 || starDotPos1 == -1 || separator=='.') { if (capturedVariableCount != 0 || starDotPos1 == -1 || separator == '.') {
return concat(patternString, pattern2string); return concat(patternString, pattern2string);
} }
// /*.html + /hotel => /hotel.html // /*.html + /hotel => /hotel.html
// /*.html + /hotel.* => /hotel.html // /*.html + /hotel.* => /hotel.html
String firstExtension = patternString.substring(starDotPos1+1); // looking for the first extension String firstExtension = patternString.substring(starDotPos1 + 1); // looking for the first extension
int dotPos2 = pattern2string.indexOf('.'); int dotPos2 = pattern2string.indexOf('.');
String file2 = (dotPos2==-1?pattern2string:pattern2string.substring(0,dotPos2)); String file2 = (dotPos2 == -1 ? pattern2string : pattern2string.substring(0, dotPos2));
String secondExtension = (dotPos2 == -1?"":pattern2string.substring(dotPos2)); String secondExtension = (dotPos2 == -1 ? "" : pattern2string.substring(dotPos2));
boolean firstExtensionWild = (firstExtension.equals(".*") || firstExtension.equals("")); boolean firstExtensionWild = (firstExtension.equals(".*") || firstExtension.equals(""));
boolean secondExtensionWild = (secondExtension.equals(".*") || secondExtension.equals("")); boolean secondExtensionWild = (secondExtension.equals(".*") || secondExtension.equals(""));
if (!firstExtensionWild && !secondExtensionWild) { if (!firstExtensionWild && !secondExtensionWild) {
throw new IllegalArgumentException("Cannot combine patterns: " + patternString + " and " + pattern2string); throw new IllegalArgumentException("Cannot combine patterns: " + patternString + " and " + pattern2string);
} }
return file2 + (firstExtensionWild?secondExtension:firstExtension); return file2 + (firstExtensionWild ? secondExtension : firstExtension);
} }
/** /**
* Join two paths together including a separator if necessary. Extraneous separators are removed (if the first path * 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). * ends with one and the second path starts with one).
* @param path1 First path * @param path1 First path
* @param path2 Second path * @param path2 Second path
* @return joined path that may include separator if necessary * @return joined path that may include separator if necessary
*/ */
private String concat(String path1, String path2) { private String concat(String path1, String path2) {
boolean path1EndsWithSeparator = path1.charAt(path1.length()-1)==separator; boolean path1EndsWithSeparator = path1.charAt(path1.length() - 1) == separator;
boolean path2StartsWithSeparator = path2.charAt(0)==separator; boolean path2StartsWithSeparator = path2.charAt(0) == separator;
if (path1EndsWithSeparator && path2StartsWithSeparator) { if (path1EndsWithSeparator && path2StartsWithSeparator) {
return path1 + path2.substring(1); return path1 + path2.substring(1);
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2016 the original author or authors. * Copyright 2002-2017 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -13,13 +13,14 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.web.util.patterns; package org.springframework.web.util.patterns;
import java.util.Comparator; import java.util.Comparator;
/** /**
* Basic PathPattern comparator. * Basic PathPattern comparator.
* *
* @author Andy Clement * @author Andy Clement
*/ */
public class PathPatternComparator implements Comparator<PathPattern> { public class PathPatternComparator implements Comparator<PathPattern> {
@ -28,8 +29,9 @@ public class PathPatternComparator implements Comparator<PathPattern> {
public int compare(PathPattern o1, PathPattern o2) { public int compare(PathPattern o1, PathPattern o2) {
// Nulls get sorted to the end // Nulls get sorted to the end
if (o1 == null) { if (o1 == null) {
return (o2==null?0:+1); return (o2 == null ? 0 : +1);
} else if (o2 == null) { }
else if (o2 == null) {
return -1; return -1;
} }
return o1.compareTo(o2); return o1.compareTo(o2);

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2016 the original author or authors. * Copyright 2002-2017 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.web.util.patterns; package org.springframework.web.util.patterns;
import java.util.ArrayList; import java.util.ArrayList;
@ -21,9 +22,10 @@ import java.util.regex.PatternSyntaxException;
/** /**
* Parser for URI template patterns. It breaks the path pattern into a number of * Parser for URI template patterns. It breaks the path pattern into a number of
* path elements in a linked list. * {@link PathElement}s in a linked list.
* *
* @author Andy Clement * @author Andy Clement
* @since 5.0
*/ */
public class PathPatternParser { public class PathPatternParser {
@ -85,7 +87,7 @@ public class PathPatternParser {
/** /**
* Create a PatternParser that will use the specified separator instead of * Create a PatternParser that will use the specified separator instead of
* the default. * the default.
* *
* @param separator the path separator to look for when parsing. * @param separator the path separator to look for when parsing.
*/ */
public PathPatternParser(char separator) { public PathPatternParser(char separator) {
@ -101,7 +103,7 @@ public class PathPatternParser {
* path elements around separator boundaries and verifying the structure at each * path elements around separator boundaries and verifying the structure at each
* stage. Produces a PathPattern object that can be used for fast matching * stage. Produces a PathPattern object that can be used for fast matching
* against paths. * against paths.
* *
* @param pathPattern the input path pattern, e.g. /foo/{bar} * @param pathPattern the input path pattern, e.g. /foo/{bar}
* @return a PathPattern for quickly matching paths against the specified path pattern * @return a PathPattern for quickly matching paths against the specified path pattern
*/ */
@ -128,32 +130,37 @@ public class PathPatternParser {
pushPathElement(createPathElement()); pushPathElement(createPathElement());
} }
// Skip over multiple separators // Skip over multiple separators
while ((pos+1) < pathPatternLength && pathPatternData[pos+1] == separator) { while ((pos + 1) < pathPatternLength && pathPatternData[pos + 1] == separator) {
pos++; pos++;
} }
if (peekDoubleWildcard()) { if (peekDoubleWildcard()) {
pushPathElement(new WildcardTheRestPathElement(pos,separator)); pushPathElement(new WildcardTheRestPathElement(pos, separator));
pos+=2; pos += 2;
} else { }
else {
pushPathElement(new SeparatorPathElement(pos, separator)); pushPathElement(new SeparatorPathElement(pos, separator));
} }
} else { }
else {
if (pathElementStart == -1) { if (pathElementStart == -1) {
pathElementStart = pos; pathElementStart = pos;
} }
if (ch == '?') { if (ch == '?') {
singleCharWildcardCount++; singleCharWildcardCount++;
} else if (ch == '{') { }
else if (ch == '{') {
if (insideVariableCapture) { if (insideVariableCapture) {
throw new PatternParseException(pos, pathPatternData, PatternMessage.ILLEGAL_NESTED_CAPTURE); 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}/) // 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] == '}') { // } else if (pos > 0 && pathPatternData[pos - 1] == '}') {
// throw new PatternParseException(pos, pathPatternData, // throw new PatternParseException(pos, pathPatternData,
// PatternMessage.CANNOT_HAVE_ADJACENT_CAPTURES); // PatternMessage.CANNOT_HAVE_ADJACENT_CAPTURES);
} }
insideVariableCapture = true; insideVariableCapture = true;
variableCaptureStart = pos; variableCaptureStart = pos;
} else if (ch == '}') { }
else if (ch == '}') {
if (!insideVariableCapture) { if (!insideVariableCapture) {
throw new PatternParseException(pos, pathPatternData, PatternMessage.MISSING_OPEN_CAPTURE); throw new PatternParseException(pos, pathPatternData, PatternMessage.MISSING_OPEN_CAPTURE);
} }
@ -163,13 +170,15 @@ public class PathPatternParser {
PatternMessage.NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST); PatternMessage.NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST);
} }
variableCaptureCount++; variableCaptureCount++;
} else if (ch == ':') { }
else if (ch == ':') {
if (insideVariableCapture) { if (insideVariableCapture) {
skipCaptureRegex(); skipCaptureRegex();
insideVariableCapture = false; insideVariableCapture = false;
variableCaptureCount++; variableCaptureCount++;
} }
} else if (ch == '*') { }
else if (ch == '*') {
if (insideVariableCapture) { if (insideVariableCapture) {
if (variableCaptureStart == pos - 1) { if (variableCaptureStart == pos - 1) {
isCaptureTheRestVariable = true; isCaptureTheRestVariable = true;
@ -185,7 +194,8 @@ public class PathPatternParser {
PatternMessage.ILLEGAL_CHARACTER_AT_START_OF_CAPTURE_DESCRIPTOR, PatternMessage.ILLEGAL_CHARACTER_AT_START_OF_CAPTURE_DESCRIPTOR,
Character.toString(ch)); Character.toString(ch));
} else if ((pos > (variableCaptureStart + 1 + (isCaptureTheRestVariable ? 1 : 0)) }
else if ((pos > (variableCaptureStart + 1 + (isCaptureTheRestVariable ? 1 : 0))
&& !Character.isJavaIdentifierPart(ch))) { && !Character.isJavaIdentifierPart(ch))) {
throw new PatternParseException(pos, pathPatternData, throw new PatternParseException(pos, pathPatternData,
PatternMessage.ILLEGAL_CHARACTER_IN_CAPTURE_DESCRIPTOR, Character.toString(ch)); PatternMessage.ILLEGAL_CHARACTER_IN_CAPTURE_DESCRIPTOR, Character.toString(ch));
@ -219,10 +229,11 @@ public class PathPatternParser {
pos++; pos++;
previousBackslash = true; previousBackslash = true;
continue; continue;
} }
if (ch == '{' && !previousBackslash) { if (ch == '{' && !previousBackslash) {
curlyBracketDepth++; curlyBracketDepth++;
} else if (ch == '}' && !previousBackslash) { }
else if (ch == '}' && !previousBackslash) {
if (curlyBracketDepth == 0) { if (curlyBracketDepth == 0) {
if (regexStart == pos) { if (regexStart == pos) {
throw new PatternParseException(regexStart, pathPatternData, throw new PatternParseException(regexStart, pathPatternData,
@ -236,7 +247,7 @@ public class PathPatternParser {
throw new PatternParseException(pos, pathPatternData, PatternMessage.MISSING_CLOSE_CAPTURE); throw new PatternParseException(pos, pathPatternData, PatternMessage.MISSING_CLOSE_CAPTURE);
} }
pos++; pos++;
previousBackslash=false; previousBackslash = false;
} }
throw new PatternParseException(pos - 1, pathPatternData, PatternMessage.MISSING_CLOSE_CAPTURE); throw new PatternParseException(pos - 1, pathPatternData, PatternMessage.MISSING_CLOSE_CAPTURE);
} }
@ -265,25 +276,30 @@ public class PathPatternParser {
if (currentPE == null) { if (currentPE == null) {
headPE = newPathElement; headPE = newPathElement;
currentPE = newPathElement; currentPE = newPathElement;
} else if (currentPE instanceof SeparatorPathElement) { }
else if (currentPE instanceof SeparatorPathElement) {
PathElement peBeforeSeparator = currentPE.prev; PathElement peBeforeSeparator = currentPE.prev;
if (peBeforeSeparator == null) { if (peBeforeSeparator == null) {
// /{*foobar} is at the start // /{*foobar} is at the start
headPE = newPathElement; headPE = newPathElement;
newPathElement.prev = peBeforeSeparator; newPathElement.prev = peBeforeSeparator;
} else { }
else {
peBeforeSeparator.next = newPathElement; peBeforeSeparator.next = newPathElement;
newPathElement.prev = peBeforeSeparator; newPathElement.prev = peBeforeSeparator;
} }
currentPE = newPathElement; currentPE = newPathElement;
} else {
throw new IllegalStateException("Expected SeparatorPathElement but was "+currentPE);
} }
} else { else {
throw new IllegalStateException("Expected SeparatorPathElement but was " + currentPE);
}
}
else {
if (headPE == null) { if (headPE == null) {
headPE = newPathElement; headPE = newPathElement;
currentPE = newPathElement; currentPE = newPathElement;
} else { }
else {
currentPE.next = newPathElement; currentPE.next = newPathElement;
newPathElement.prev = currentPE; newPathElement.prev = currentPE;
currentPE = newPathElement; currentPE = newPathElement;
@ -305,39 +321,51 @@ public class PathPatternParser {
System.arraycopy(pathPatternData, pathElementStart, pathElementText, 0, pos - pathElementStart); System.arraycopy(pathPatternData, pathElementStart, pathElementText, 0, pos - pathElementStart);
PathElement newPE = null; PathElement newPE = null;
if (variableCaptureCount > 0) { if (variableCaptureCount > 0) {
if (variableCaptureCount == 1 && pathElementStart == variableCaptureStart && pathPatternData[pos - 1] == '}') { if (variableCaptureCount == 1
&& pathElementStart == variableCaptureStart && pathPatternData[pos - 1] == '}') {
if (isCaptureTheRestVariable) { if (isCaptureTheRestVariable) {
// It is {*....} // It is {*....}
newPE = new CaptureTheRestPathElement(pathElementStart, pathElementText, separator); newPE = new CaptureTheRestPathElement(pathElementStart, pathElementText, separator);
} else { }
else {
// It is a full capture of this element (possibly with constraint), for example: /foo/{abc}/ // It is a full capture of this element (possibly with constraint), for example: /foo/{abc}/
try { try {
newPE = new CaptureVariablePathElement(pathElementStart, pathElementText, caseSensitive); newPE = new CaptureVariablePathElement(pathElementStart, pathElementText, caseSensitive);
} catch (PatternSyntaxException pse) { }
throw new PatternParseException(pse, findRegexStart(pathPatternData,pathElementStart)+pse.getIndex(), pathPatternData, PatternMessage.JDK_PATTERN_SYNTAX_EXCEPTION); catch (PatternSyntaxException pse) {
throw new PatternParseException(pse, findRegexStart(pathPatternData, pathElementStart)
+ pse.getIndex(), pathPatternData, PatternMessage.JDK_PATTERN_SYNTAX_EXCEPTION);
} }
recordCapturedVariable(pathElementStart, ((CaptureVariablePathElement) newPE).getVariableName()); recordCapturedVariable(pathElementStart, ((CaptureVariablePathElement) newPE).getVariableName());
} }
} else { }
else {
if (isCaptureTheRestVariable) { if (isCaptureTheRestVariable) {
throw new PatternParseException(pathElementStart, pathPatternData, PatternMessage.CAPTURE_ALL_IS_STANDALONE_CONSTRUCT); throw new PatternParseException(pathElementStart, pathPatternData,
PatternMessage.CAPTURE_ALL_IS_STANDALONE_CONSTRUCT);
} }
RegexPathElement newRegexSection = new RegexPathElement(pathElementStart, pathElementText, caseSensitive, pathPatternData); RegexPathElement newRegexSection = new RegexPathElement(pathElementStart, pathElementText,
caseSensitive, pathPatternData);
for (String variableName : newRegexSection.getVariableNames()) { for (String variableName : newRegexSection.getVariableNames()) {
recordCapturedVariable(pathElementStart, variableName); recordCapturedVariable(pathElementStart, variableName);
} }
newPE = newRegexSection; newPE = newRegexSection;
} }
} else { }
else {
if (wildcard) { if (wildcard) {
if (pos - 1 == pathElementStart) { if (pos - 1 == pathElementStart) {
newPE = new WildcardPathElement(pathElementStart); newPE = new WildcardPathElement(pathElementStart);
} else { }
else {
newPE = new RegexPathElement(pathElementStart, pathElementText, caseSensitive, pathPatternData); newPE = new RegexPathElement(pathElementStart, pathElementText, caseSensitive, pathPatternData);
} }
} else if (singleCharWildcardCount!=0) { }
newPE = new SingleCharWildcardedPathElement(pathElementStart, pathElementText, singleCharWildcardCount, caseSensitive); else if (singleCharWildcardCount != 0) {
} else { newPE = new SingleCharWildcardedPathElement(pathElementStart, pathElementText,
singleCharWildcardCount, caseSensitive);
}
else {
newPE = new LiteralPathElement(pathElementStart, pathElementText, caseSensitive); newPE = new LiteralPathElement(pathElementStart, pathElementText, caseSensitive);
} }
} }
@ -349,11 +377,12 @@ public class PathPatternParser {
* Assumes there is a constraint pattern. * Assumes there is a constraint pattern.
* @param data a complete path expression, e.g. /aaa/bbb/{ccc:...} * @param data a complete path expression, e.g. /aaa/bbb/{ccc:...}
* @param offset the start of the capture pattern of interest * @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 * @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) { private int findRegexStart(char[] data, int offset) {
int pos = offset; int pos = offset;
while (pos<data.length) { while (pos < data.length) {
if (data[pos] == ':') { if (data[pos] == ':') {
return pos + 1; return pos + 1;
} }
@ -383,7 +412,8 @@ public class PathPatternParser {
capturedVariableNames = new ArrayList<>(); capturedVariableNames = new ArrayList<>();
} }
if (capturedVariableNames.contains(variableName)) { if (capturedVariableNames.contains(variableName)) {
throw new PatternParseException(pos, this.pathPatternData, PatternMessage.ILLEGAL_DOUBLE_CAPTURE, variableName); throw new PatternParseException(pos, this.pathPatternData,
PatternMessage.ILLEGAL_DOUBLE_CAPTURE, variableName);
} }
capturedVariableNames.add(variableName); capturedVariableNames.add(variableName);
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2016 the original author or authors. * Copyright 2002-2017 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.web.util.patterns; package org.springframework.web.util.patterns;
import java.util.Comparator; import java.util.Comparator;
@ -20,13 +21,14 @@ import java.util.Comparator;
/** /**
* Similar to {@link PathPatternComparator} but this takes account of a specified path and * Similar to {@link PathPatternComparator} but this takes account of a specified path and
* sorts anything that exactly matches it to be first. * sorts anything that exactly matches it to be first.
* *
* @author Andy Clement * @author Andy Clement
* @since 5.0
*/ */
public class PatternComparatorConsideringPath implements Comparator<PathPattern> { public class PatternComparatorConsideringPath implements Comparator<PathPattern> {
private String path; private String path;
public PatternComparatorConsideringPath(String path) { public PatternComparatorConsideringPath(String path) {
this.path = path; this.path = path;
} }
@ -35,13 +37,15 @@ public class PatternComparatorConsideringPath implements Comparator<PathPattern>
public int compare(PathPattern o1, PathPattern o2) { public int compare(PathPattern o1, PathPattern o2) {
// Nulls get sorted to the end // Nulls get sorted to the end
if (o1 == null) { if (o1 == null) {
return (o2==null?0:+1); return (o2 == null ? 0 : +1);
} else if (o2 == null) { }
else if (o2 == null) {
return -1; return -1;
} }
if (o1.getPatternString().equals(path)) { if (o1.getPatternString().equals(path)) {
return (o2.getPatternString().equals(path))?0:-1; return (o2.getPatternString().equals(path)) ? 0 : -1;
} else if (o2.getPatternString().equals(path)) { }
else if (o2.getPatternString().equals(path)) {
return +1; return +1;
} }
return o1.compareTo(o2); return o1.compareTo(o2);

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2016 the original author or authors. * Copyright 2002-2017 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -13,20 +13,22 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.web.util.patterns; package org.springframework.web.util.patterns;
import java.text.MessageFormat; import java.text.MessageFormat;
/** /**
* The messages that can be included in a {@link PatternParseException} when there is a parse failure. * The messages that can be included in a {@link PatternParseException} when there is a parse failure.
* *
* @author Andy Clement * @author Andy Clement
* @since 5.0
*/ */
public enum PatternMessage { public enum PatternMessage {
// @formatter:off // @formatter:off
MISSING_CLOSE_CAPTURE("Expected close capture character after variable name '}'"), MISSING_CLOSE_CAPTURE("Expected close capture character after variable name '}'"),
MISSING_OPEN_CAPTURE("Missing preceeding open capture character before variable name'{'"), MISSING_OPEN_CAPTURE("Missing preceeding open capture character before variable name'{'"),
ILLEGAL_NESTED_CAPTURE("Not allowed to nest variable captures"), ILLEGAL_NESTED_CAPTURE("Not allowed to nest variable captures"),
CANNOT_HAVE_ADJACENT_CAPTURES("Adjacent captures are not allowed"), 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_AT_START_OF_CAPTURE_DESCRIPTOR("Character ''{0}'' is not allowed at start of captured variable name"),

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2016 the original author or authors. * Copyright 2002-2017 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -13,12 +13,14 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.web.util.patterns; package org.springframework.web.util.patterns;
/** /**
* Exception that is thrown when there is a problem with the pattern being parsed. * Exception that is thrown when there is a problem with the pattern being parsed.
* *
* @author Andy Clement * @author Andy Clement
* @since 5.0
*/ */
public class PatternParseException extends RuntimeException { public class PatternParseException extends RuntimeException {
@ -41,7 +43,7 @@ public class PatternParseException extends RuntimeException {
} }
public PatternParseException(Throwable cause, int pos, char[] patternText, PatternMessage message, Object... inserts) { public PatternParseException(Throwable cause, int pos, char[] patternText, PatternMessage message, Object... inserts) {
super(message.formatMessage(inserts),cause); super(message.formatMessage(inserts), cause);
this.pos = pos; this.pos = pos;
this.patternText = patternText; this.patternText = patternText;
this.message = message; this.message = message;

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2016 the original author or authors. * Copyright 2002-2017 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.web.util.patterns; package org.springframework.web.util.patterns;
import java.util.LinkedList; import java.util.LinkedList;
@ -26,8 +27,9 @@ import org.springframework.web.util.patterns.PathPattern.MatchingContext;
* A regex path element. Used to represent any complicated element of the path. * A regex path element. Used to represent any complicated element of the path.
* For example in '<tt>/foo/&ast;_&ast;/&ast;_{foobar}</tt>' both <tt>*_*</tt> and <tt>*_{foobar}</tt> * For example in '<tt>/foo/&ast;_&ast;/&ast;_{foobar}</tt>' both <tt>*_*</tt> and <tt>*_{foobar}</tt>
* are {@link RegexPathElement} path elements. Derived from the general {@link AntPathMatcher} approach. * are {@link RegexPathElement} path elements. Derived from the general {@link AntPathMatcher} approach.
* *
* @author Andy Clement * @author Andy Clement
* @since 5.0
*/ */
class RegexPathElement extends PathElement { class RegexPathElement extends PathElement {
@ -63,10 +65,12 @@ class RegexPathElement extends PathElement {
String match = matcher.group(); String match = matcher.group();
if ("?".equals(match)) { if ("?".equals(match)) {
patternBuilder.append('.'); patternBuilder.append('.');
} else if ("*".equals(match)) { }
else if ("*".equals(match)) {
patternBuilder.append(".*"); patternBuilder.append(".*");
wildcardCount++; wildcardCount++;
} else if (match.startsWith("{") && match.endsWith("}")) { }
else if (match.startsWith("{") && match.endsWith("}")) {
int colonIdx = match.indexOf(':'); int colonIdx = match.indexOf(':');
if (colonIdx == -1) { if (colonIdx == -1) {
patternBuilder.append(DEFAULT_VARIABLE_PATTERN); patternBuilder.append(DEFAULT_VARIABLE_PATTERN);
@ -76,7 +80,8 @@ class RegexPathElement extends PathElement {
variableName); variableName);
} }
this.variableNames.add(variableName); this.variableNames.add(variableName);
} else { }
else {
String variablePattern = match.substring(colonIdx + 1, match.length() - 1); String variablePattern = match.substring(colonIdx + 1, match.length() - 1);
patternBuilder.append('('); patternBuilder.append('(');
patternBuilder.append(variablePattern); patternBuilder.append(variablePattern);
@ -94,7 +99,8 @@ class RegexPathElement extends PathElement {
patternBuilder.append(quote(text, end, text.length())); patternBuilder.append(quote(text, end, text.length()));
if (caseSensitive) { if (caseSensitive) {
pattern = java.util.regex.Pattern.compile(patternBuilder.toString()); pattern = java.util.regex.Pattern.compile(patternBuilder.toString());
} else { }
else {
pattern = java.util.regex.Pattern.compile(patternBuilder.toString(), pattern = java.util.regex.Pattern.compile(patternBuilder.toString(),
java.util.regex.Pattern.CASE_INSENSITIVE); java.util.regex.Pattern.CASE_INSENSITIVE);
} }
@ -120,7 +126,8 @@ class RegexPathElement extends PathElement {
if (next == null) { if (next == null) {
// No more pattern, is there more data? // No more pattern, is there more data?
matches = (p == matchingContext.candidateLength); matches = (p == matchingContext.candidateLength);
} else { }
else {
if (matchingContext.isMatchStartMatching && p == matchingContext.candidateLength) { if (matchingContext.isMatchStartMatching && p == matchingContext.candidateLength) {
return true; // no more data but matches up to this point return true; // no more data but matches up to this point
} }
@ -165,10 +172,10 @@ class RegexPathElement extends PathElement {
public int getWildcardCount() { public int getWildcardCount() {
return wildcardCount; return wildcardCount;
} }
@Override @Override
public int getScore() { public int getScore() {
return getCaptureCount()*CAPTURE_VARIABLE_WEIGHT + getWildcardCount()*WILDCARD_WEIGHT; return getCaptureCount() * CAPTURE_VARIABLE_WEIGHT + getWildcardCount() * WILDCARD_WEIGHT;
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2016 the original author or authors. * Copyright 2002-2017 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.web.util.patterns; package org.springframework.web.util.patterns;
import org.springframework.web.util.patterns.PathPattern.MatchingContext; import org.springframework.web.util.patterns.PathPattern.MatchingContext;
@ -21,8 +22,9 @@ import org.springframework.web.util.patterns.PathPattern.MatchingContext;
* A separator path element. In the pattern '/foo/bar' the two occurrences * A separator path element. In the pattern '/foo/bar' the two occurrences
* of '/' will be represented by a SeparatorPathElement (if the default * of '/' will be represented by a SeparatorPathElement (if the default
* separator of '/' is being used). * separator of '/' is being used).
* *
* @author Andy Clement * @author Andy Clement
* @since 5.0
*/ */
class SeparatorPathElement extends PathElement { class SeparatorPathElement extends PathElement {
@ -44,13 +46,14 @@ class SeparatorPathElement extends PathElement {
if (matchingContext.candidate[candidateIndex] == separator) { if (matchingContext.candidate[candidateIndex] == separator) {
// Skip further separators in the path (they are all 'matched' // Skip further separators in the path (they are all 'matched'
// by a single SeparatorPathElement) // by a single SeparatorPathElement)
while ((candidateIndex+1)<matchingContext.candidateLength && while ((candidateIndex + 1) < matchingContext.candidateLength &&
matchingContext.candidate[candidateIndex+1] == separator) { matchingContext.candidate[candidateIndex + 1] == separator) {
candidateIndex++; candidateIndex++;
} }
if (next == null) { if (next == null) {
matched = ((candidateIndex + 1) == matchingContext.candidateLength); matched = ((candidateIndex + 1) == matchingContext.candidateLength);
} else { }
else {
candidateIndex++; candidateIndex++;
if (matchingContext.isMatchStartMatching && candidateIndex == matchingContext.candidateLength) { if (matchingContext.isMatchStartMatching && candidateIndex == matchingContext.candidateLength) {
return true; // no more data but matches up to this point return true; // no more data but matches up to this point
@ -61,7 +64,7 @@ class SeparatorPathElement extends PathElement {
} }
return matched; return matched;
} }
public String toString() { public String toString() {
return "Separator(" + separator + ")"; return "Separator(" + separator + ")";
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2016 the original author or authors. * Copyright 2002-2017 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.web.util.patterns; package org.springframework.web.util.patterns;
import org.springframework.web.util.patterns.PathPattern.MatchingContext; import org.springframework.web.util.patterns.PathPattern.MatchingContext;
@ -20,17 +21,18 @@ import org.springframework.web.util.patterns.PathPattern.MatchingContext;
/** /**
* A literal path element that does includes the single character wildcard '?' one * A literal path element that does includes the single character wildcard '?' one
* or more times (to basically many any character at that position). * or more times (to basically many any character at that position).
* *
* @author Andy Clement * @author Andy Clement
* @since 5.0
*/ */
class SingleCharWildcardedPathElement extends PathElement { class SingleCharWildcardedPathElement extends PathElement {
private char[] text; private char[] text;
private int len; private int len;
private int questionMarkCount; private int questionMarkCount;
private boolean caseSensitive; private boolean caseSensitive;
public SingleCharWildcardedPathElement(int pos, char[] literalText, int questionMarkCount, boolean caseSensitive) { public SingleCharWildcardedPathElement(int pos, char[] literalText, int questionMarkCount, boolean caseSensitive) {
@ -40,14 +42,15 @@ class SingleCharWildcardedPathElement extends PathElement {
this.caseSensitive = caseSensitive; this.caseSensitive = caseSensitive;
if (caseSensitive) { if (caseSensitive) {
this.text = literalText; this.text = literalText;
} else { }
else {
this.text = new char[literalText.length]; this.text = new char[literalText.length];
for (int i = 0; i < len; i++) { for (int i = 0; i < len; i++) {
this.text[i] = Character.toLowerCase(literalText[i]); this.text[i] = Character.toLowerCase(literalText[i]);
} }
} }
} }
@Override @Override
public boolean matches(int candidateIndex, MatchingContext matchingContext) { public boolean matches(int candidateIndex, MatchingContext matchingContext) {
if (matchingContext.candidateLength < (candidateIndex + len)) { if (matchingContext.candidateLength < (candidateIndex + len)) {
@ -62,7 +65,8 @@ class SingleCharWildcardedPathElement extends PathElement {
} }
candidateIndex++; candidateIndex++;
} }
} else { }
else {
for (int i = 0; i < len; i++) { for (int i = 0; i < len; i++) {
char t = text[i]; char t = text[i];
if (t != '?' && Character.toLowerCase(candidate[candidateIndex]) != t) { if (t != '?' && Character.toLowerCase(candidate[candidateIndex]) != t) {
@ -73,14 +77,15 @@ class SingleCharWildcardedPathElement extends PathElement {
} }
if (next == null) { if (next == null) {
return candidateIndex == matchingContext.candidateLength; return candidateIndex == matchingContext.candidateLength;
} else { }
else {
if (matchingContext.isMatchStartMatching && candidateIndex == matchingContext.candidateLength) { if (matchingContext.isMatchStartMatching && candidateIndex == matchingContext.candidateLength) {
return true; // no more data but matches up to this point return true; // no more data but matches up to this point
} }
return next.matches(candidateIndex, matchingContext); return next.matches(candidateIndex, matchingContext);
} }
} }
@Override @Override
public int getWildcardCount() { public int getWildcardCount() {
return questionMarkCount; return questionMarkCount;

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2016 the original author or authors. * Copyright 2002-2017 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -13,18 +13,21 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.web.util.patterns; package org.springframework.web.util.patterns;
/** /**
* Used to represent a subsection of an array, useful when wanting to pass that subset of data * 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 * to another method (e.g. a java regex matcher) but not wanting to create a new string object to hold
* all that data. * all that data.
* *
* @author Andy Clement * @author Andy Clement
* @since 5.0
*/ */
class SubSequence implements CharSequence { class SubSequence implements CharSequence {
private char[] chars; private char[] chars;
private int start, end; private int start, end;
SubSequence(char[] chars, int start, int end) { SubSequence(char[] chars, int start, int end) {
@ -47,9 +50,9 @@ class SubSequence implements CharSequence {
public CharSequence subSequence(int start, int end) { public CharSequence subSequence(int start, int end) {
return new SubSequence(chars, this.start + start, this.start + end); return new SubSequence(chars, this.start + start, this.start + end);
} }
public String toString() { public String toString() {
return new String(chars,start,end-start); return new String(chars, start, end - start);
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2016 the original author or authors. * Copyright 2002-2017 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.web.util.patterns; package org.springframework.web.util.patterns;
import org.springframework.web.util.patterns.PathPattern.MatchingContext; import org.springframework.web.util.patterns.PathPattern.MatchingContext;
@ -20,8 +21,9 @@ import org.springframework.web.util.patterns.PathPattern.MatchingContext;
/** /**
* A wildcard path element. In the pattern '/foo/&ast;/goo' the * is * A wildcard path element. In the pattern '/foo/&ast;/goo' the * is
* represented by a WildcardPathElement. * represented by a WildcardPathElement.
* *
* @author Andy Clement * @author Andy Clement
* @since 5.0
*/ */
class WildcardPathElement extends PathElement { class WildcardPathElement extends PathElement {
@ -39,7 +41,8 @@ class WildcardPathElement extends PathElement {
int nextPos = matchingContext.scanAhead(candidateIndex); int nextPos = matchingContext.scanAhead(candidateIndex);
if (next == null) { if (next == null) {
return (nextPos == matchingContext.candidateLength); return (nextPos == matchingContext.candidateLength);
} else { }
else {
if (matchingContext.isMatchStartMatching && nextPos == matchingContext.candidateLength) { if (matchingContext.isMatchStartMatching && nextPos == matchingContext.candidateLength) {
return true; // no more data but matches up to this point return true; // no more data but matches up to this point
} }
@ -60,7 +63,7 @@ class WildcardPathElement extends PathElement {
public int getWildcardCount() { public int getWildcardCount() {
return 1; return 1;
} }
@Override @Override
public int getScore() { public int getScore() {
return WILDCARD_WEIGHT; return WILDCARD_WEIGHT;

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2016 the original author or authors. * Copyright 2002-2017 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.web.util.patterns; package org.springframework.web.util.patterns;
import org.springframework.web.util.patterns.PathPattern.MatchingContext; import org.springframework.web.util.patterns.PathPattern.MatchingContext;
@ -20,13 +21,14 @@ import org.springframework.web.util.patterns.PathPattern.MatchingContext;
/** /**
* A path element representing wildcarding the rest of a path. In the pattern * A path element representing wildcarding the rest of a path. In the pattern
* '/foo/**' the /** is represented as a {@link WildcardTheRestPathElement}. * '/foo/**' the /** is represented as a {@link WildcardTheRestPathElement}.
* *
* @author Andy Clement * @author Andy Clement
* @since 5.0
*/ */
class WildcardTheRestPathElement extends PathElement { class WildcardTheRestPathElement extends PathElement {
private char separator; private char separator;
WildcardTheRestPathElement(int pos, char separator) { WildcardTheRestPathElement(int pos, char separator) {
super(pos); super(pos);
this.separator = separator; this.separator = separator;
@ -36,14 +38,14 @@ class WildcardTheRestPathElement extends PathElement {
public boolean matches(int candidateIndex, MatchingContext matchingContext) { public boolean matches(int candidateIndex, MatchingContext matchingContext) {
// If there is more data, it must start with the separator // If there is more data, it must start with the separator
if (candidateIndex < matchingContext.candidateLength && if (candidateIndex < matchingContext.candidateLength &&
matchingContext.candidate[candidateIndex] != separator) { matchingContext.candidate[candidateIndex] != separator) {
return false; return false;
} }
return true; return true;
} }
public String toString() { public String toString() {
return "WildcardTheRest("+separator+"**)"; return "WildcardTheRest(" + separator + "**)";
} }
@Override @Override

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2016 the original author or authors. * Copyright 2002-2017 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.web.util.patterns; package org.springframework.web.util.patterns;
import java.util.ArrayList; import java.util.ArrayList;
@ -31,7 +32,7 @@ import static org.junit.Assert.*;
/** /**
* Exercise matching of {@link PathPattern} objects. * Exercise matching of {@link PathPattern} objects.
* *
* @author Andy Clement * @author Andy Clement
*/ */
public class PathPatternMatcherTests { public class PathPatternMatcherTests {
@ -58,7 +59,7 @@ public class PathPatternMatcherTests {
checkMatches("/foo/bar", "/foo/bar"); checkMatches("/foo/bar", "/foo/bar");
checkNoMatch("/foo/bar", "/foo/baz"); checkNoMatch("/foo/bar", "/foo/baz");
// TODO Need more tests for escaped separators in path patterns and paths? // 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) checkMatches("/foo\\/bar", "/foo\\/bar"); // chain string is Separator(/) Literal(foo\) Separator(/) Literal(bar)
} }
@Test @Test
@ -97,27 +98,27 @@ public class PathPatternMatcherTests {
@Test @Test
public void multipleSelectorsInPattern() { public void multipleSelectorsInPattern() {
checkMatches("///abc","/abc"); checkMatches("///abc", "/abc");
checkMatches("//","/"); checkMatches("//", "/");
checkMatches("abc","abc"); checkMatches("abc", "abc");
checkMatches("///abc//d/e","/abc/d/e"); checkMatches("///abc//d/e", "/abc/d/e");
checkMatches("///abc//{def}//////xyz","/abc/foo/xyz"); checkMatches("///abc//{def}//////xyz", "/abc/foo/xyz");
} }
@Test @Test
public void multipleSelectorsInPath() { public void multipleSelectorsInPath() {
checkMatches("/abc","////abc"); checkMatches("/abc", "////abc");
checkMatches("/","//"); checkMatches("/", "//");
checkMatches("/abc//def///ghi","/abc/def/ghi"); checkMatches("/abc//def///ghi", "/abc/def/ghi");
} }
@Test @Test
public void multipleSelectorsInPatternAndPath() { public void multipleSelectorsInPatternAndPath() {
checkMatches("///one///two///three","//one/////two///////three"); checkMatches("///one///two///three", "//one/////two///////three");
checkMatches("//one//two//three","/one/////two/three"); checkMatches("//one//two//three", "/one/////two/three");
checkCapture("///{foo}///bar","/one/bar","foo","one"); checkCapture("///{foo}///bar", "/one/bar", "foo", "one");
} }
@Test @Test
public void wildcards() { public void wildcards() {
checkMatches("/*/bar", "/foo/bar"); checkMatches("/*/bar", "/foo/bar");
@ -126,10 +127,10 @@ public class PathPatternMatcherTests {
checkMatches("/*/bar", "/foo/bar"); checkMatches("/*/bar", "/foo/bar");
checkMatches("/a*b*c*d/bar", "/abcd/bar"); checkMatches("/a*b*c*d/bar", "/abcd/bar");
checkMatches("*a*", "testa"); checkMatches("*a*", "testa");
checkMatches("a/*","a/"); checkMatches("a/*", "a/");
checkMatches("a/*","a/a"); checkMatches("a/*", "a/a");
checkNoMatch("a/*","a/a/"); checkNoMatch("a/*", "a/a/");
checkMatches("/resource/**", "/resource"); checkMatches("/resource/**", "/resource");
checkNoMatch("/resource/**", "/resourceX"); checkNoMatch("/resource/**", "/resourceX");
checkNoMatch("/resource/**", "/resourceX/foobar"); checkNoMatch("/resource/**", "/resourceX/foobar");
@ -140,19 +141,19 @@ public class PathPatternMatcherTests {
public void trailingSeparators() { public void trailingSeparators() {
checkNoMatch("aaa/", "aaa"); checkNoMatch("aaa/", "aaa");
} }
@Test @Test
public void constrainedMatches() { public void constrainedMatches() {
checkCapture("{foo:[0-9]*}", "123", "foo", "123"); checkCapture("{foo:[0-9]*}", "123", "foo", "123");
checkNoMatch("{foo:[0-9]*}", "abc"); checkNoMatch("{foo:[0-9]*}", "abc");
checkNoMatch("/{foo:[0-9]*}", "abc"); checkNoMatch("/{foo:[0-9]*}", "abc");
checkCapture("/*/{foo:....}/**","/foo/barg/foo","foo","barg"); checkCapture("/*/{foo:....}/**", "/foo/barg/foo", "foo", "barg");
checkCapture("/*/{foo:....}/**","/foo/barg/abc/def/ghi","foo","barg"); checkCapture("/*/{foo:....}/**", "/foo/barg/abc/def/ghi", "foo", "barg");
checkNoMatch("{foo:....}", "99"); checkNoMatch("{foo:....}", "99");
checkMatches("{foo:..}", "99"); checkMatches("{foo:..}", "99");
checkCapture("/{abc:\\{\\}}","/{}","abc","{}"); checkCapture("/{abc:\\{\\}}", "/{}", "abc", "{}");
checkCapture("/{abc:\\[\\]}","/[]","abc","[]"); checkCapture("/{abc:\\[\\]}", "/[]", "abc", "[]");
checkCapture("/{abc:\\\\\\\\}","/\\\\"); // this is fun... checkCapture("/{abc:\\\\\\\\}", "/\\\\"); // this is fun...
} }
@Test @Test
@ -301,20 +302,20 @@ public class PathPatternMatcherTests {
checkStartNoMatch("/????", "/bala/bla"); checkStartNoMatch("/????", "/bala/bla");
checkStartMatches("/*bla*/*/bla/**", checkStartMatches("/*bla*/*/bla/**",
"/XXXblaXXXX/testing/bla/testing/testing/"); "/XXXblaXXXX/testing/bla/testing/testing/");
checkStartMatches("/*bla*/*/bla/*", checkStartMatches("/*bla*/*/bla/*",
"/XXXblaXXXX/testing/bla/testing"); "/XXXblaXXXX/testing/bla/testing");
checkStartMatches("/*bla*/*/bla/**", checkStartMatches("/*bla*/*/bla/**",
"/XXXblaXXXX/testing/bla/testing/testing"); "/XXXblaXXXX/testing/bla/testing/testing");
checkStartMatches("/*bla*/*/bla/**", checkStartMatches("/*bla*/*/bla/**",
"/XXXblaXXXX/testing/bla/testing/testing.jpg"); "/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("/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("", "");
checkStartMatches("", null); checkStartMatches("", null);
checkStartMatches("/abc", null); checkStartMatches("/abc", null);
@ -465,29 +466,29 @@ public class PathPatternMatcherTests {
separator = PathPatternParser.DEFAULT_SEPARATOR; separator = PathPatternParser.DEFAULT_SEPARATOR;
} }
} }
@Test @Test
public void extractPathWithinPattern() throws Exception { public void extractPathWithinPattern() throws Exception {
checkExtractPathWithinPattern("/welcome*/", "/welcome/","welcome"); checkExtractPathWithinPattern("/welcome*/", "/welcome/", "welcome");
checkExtractPathWithinPattern("/docs/commit.html","/docs/commit.html",""); checkExtractPathWithinPattern("/docs/commit.html", "/docs/commit.html", "");
checkExtractPathWithinPattern("/docs/*","/docs/cvs/commit","cvs/commit"); checkExtractPathWithinPattern("/docs/*", "/docs/cvs/commit", "cvs/commit");
checkExtractPathWithinPattern("/docs/cvs/*.html","/docs/cvs/commit.html","commit.html"); checkExtractPathWithinPattern("/docs/cvs/*.html", "/docs/cvs/commit.html", "commit.html");
checkExtractPathWithinPattern("/docs/**","/docs/cvs/commit","cvs/commit"); checkExtractPathWithinPattern("/docs/**", "/docs/cvs/commit", "cvs/commit");
checkExtractPathWithinPattern("/doo/{*foobar}","/doo/customer.html","customer.html"); checkExtractPathWithinPattern("/doo/{*foobar}", "/doo/customer.html", "customer.html");
checkExtractPathWithinPattern("/doo/{*foobar}","/doo/daa/customer.html","daa/customer.html"); checkExtractPathWithinPattern("/doo/{*foobar}", "/doo/daa/customer.html", "daa/customer.html");
checkExtractPathWithinPattern("/*.html","/commit.html","commit.html"); checkExtractPathWithinPattern("/*.html", "/commit.html", "commit.html");
checkExtractPathWithinPattern("/docs/*/*/*/*","/docs/cvs/other/commit.html","cvs/other/commit.html"); checkExtractPathWithinPattern("/docs/*/*/*/*", "/docs/cvs/other/commit.html", "cvs/other/commit.html");
checkExtractPathWithinPattern("/d?cs/**","/docs/cvs/commit","docs/cvs/commit"); checkExtractPathWithinPattern("/d?cs/**", "/docs/cvs/commit", "docs/cvs/commit");
checkExtractPathWithinPattern("/docs/c?s/*.html","/docs/cvs/commit.html","cvs/commit.html"); 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("/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/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}/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("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//*///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("aaa/c*/ddd/", "aaa/ccc///ddd///", "ccc/ddd");
checkExtractPathWithinPattern("", "", ""); checkExtractPathWithinPattern("", "", "");
checkExtractPathWithinPattern("/", "", ""); checkExtractPathWithinPattern("/", "", "");
checkExtractPathWithinPattern("", "/", ""); checkExtractPathWithinPattern("", "/", "");
@ -500,27 +501,29 @@ public class PathPatternMatcherTests {
@Test @Test
public void extractUriTemplateVariables() throws Exception { public void extractUriTemplateVariables() throws Exception {
checkCapture("/hotels/{hotel}", "/hotels/1", "hotel","1"); checkCapture("/hotels/{hotel}", "/hotels/1", "hotel", "1");
checkCapture("/h?tels/{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}/bookings/{booking}", "/hotels/1/bookings/2", "hotel", "1", "booking", "2");
checkCapture("/*/hotels/*/{hotel}","/foo/hotels/bar/1","hotel","1"); checkCapture("/*/hotels/*/{hotel}", "/foo/hotels/bar/1", "hotel", "1");
checkCapture("/{page}.html","/42.html","page","42"); checkCapture("/{page}.html", "/42.html", "page", "42");
checkCapture("/{page}.*","/42.html","page","42"); checkCapture("/{page}.*", "/42.html", "page", "42");
checkCapture("/A-{B}-C","/A-b-C","B","b"); checkCapture("/A-{B}-C", "/A-b-C", "B", "b");
checkCapture("/{name}.{extension}","/test.html","name","test","extension","html"); checkCapture("/{name}.{extension}", "/test.html", "name", "test", "extension", "html");
try { try {
checkCapture("/{one}/", "//", "one", ""); checkCapture("/{one}/", "//", "one", "");
fail("Expected exception"); fail("Expected exception");
} catch (IllegalStateException e) { }
assertEquals("Pattern \"/{one}/\" is not a match for \"//\"",e.getMessage()); catch (IllegalStateException e) {
assertEquals("Pattern \"/{one}/\" is not a match for \"//\"", e.getMessage());
} }
try { try {
checkCapture("", "/abc"); checkCapture("", "/abc");
fail("Expected exception"); fail("Expected exception");
} catch (IllegalStateException e) {
assertEquals("Pattern \"\" is not a match for \"/abc\"",e.getMessage());
} }
assertEquals(0,checkCapture("", "").size()); catch (IllegalStateException e) {
assertEquals("Pattern \"\" is not a match for \"/abc\"", e.getMessage());
}
assertEquals(0, checkCapture("", "").size());
checkCapture("{id}", "99", "id", "99"); checkCapture("{id}", "99", "id", "99");
checkCapture("/customer/{customerId}", "/customer/78", "customerId", "78"); checkCapture("/customer/{customerId}", "/customer/78", "customerId", "78");
checkCapture("/customer/{customerId}/banana", "/customer/42/banana", "customerId", checkCapture("/customer/{customerId}/banana", "/customer/42/banana", "customerId",
@ -529,8 +532,8 @@ public class PathPatternMatcherTests {
checkCapture("/foo/{bar}/boo/{baz}", "/foo/plum/boo/apple", "bar", "plum", "baz", checkCapture("/foo/{bar}/boo/{baz}", "/foo/plum/boo/apple", "bar", "plum", "baz",
"apple"); "apple");
checkCapture("/{bla}.*", "/testing.html", "bla", "testing"); checkCapture("/{bla}.*", "/testing.html", "bla", "testing");
Map<String,String> extracted = checkCapture("/abc","/abc"); Map<String, String> extracted = checkCapture("/abc", "/abc");
assertEquals(0,extracted.size()); assertEquals(0, extracted.size());
} }
@Test @Test
@ -565,7 +568,7 @@ public class PathPatternMatcherTests {
assertEquals("2010", result.get("year")); assertEquals("2010", result.get("year"));
assertEquals("02", result.get("month")); assertEquals("02", result.get("month"));
assertEquals("20", result.get("day")); assertEquals("20", result.get("day"));
p = pp.parse("{symbolicName:[\\p{L}\\.]+}-sources-{version:[\\p{N}\\.\\{\\}]+}.jar"); p = pp.parse("{symbolicName:[\\p{L}\\.]+}-sources-{version:[\\p{N}\\.\\{\\}]+}.jar");
result = p.matchAndExtract("com.example-sources-1.0.0.{12}.jar"); result = p.matchAndExtract("com.example-sources-1.0.0.{12}.jar");
assertEquals("com.example", result.get("symbolicName")); assertEquals("com.example", result.get("symbolicName"));
@ -580,7 +583,7 @@ public class PathPatternMatcherTests {
exception.expectMessage(containsString("The number of capturing groups in the pattern")); exception.expectMessage(containsString("The number of capturing groups in the pattern"));
pathMatcher.matchAndExtract("/web/foobar_goo"); pathMatcher.matchAndExtract("/web/foobar_goo");
} }
@Rule @Rule
public final ExpectedException exception = ExpectedException.none(); public final ExpectedException exception = ExpectedException.none();
@ -619,14 +622,14 @@ public class PathPatternMatcherTests {
// SPR-10554 // SPR-10554
assertEquals("/hotel", pathMatcher.combine("/", "/hotel")); // SPR-12975 assertEquals("/hotel", pathMatcher.combine("/", "/hotel")); // SPR-12975
assertEquals("/hotel/booking", pathMatcher.combine("/hotel/", "/booking")); // SPR-12975 assertEquals("/hotel/booking", pathMatcher.combine("/hotel/", "/booking")); // SPR-12975
assertEquals("",pathMatcher.combine(null, null)); assertEquals("", pathMatcher.combine(null, null));
assertEquals("",pathMatcher.combine(null, "")); assertEquals("", pathMatcher.combine(null, ""));
assertEquals("",pathMatcher.combine("",null)); assertEquals("", pathMatcher.combine("", null));
assertEquals("",pathMatcher.combine(null, null)); assertEquals("", pathMatcher.combine(null, null));
assertEquals("",pathMatcher.combine("", "")); assertEquals("", pathMatcher.combine("", ""));
assertEquals("/hotel",pathMatcher.combine("", "/hotel")); assertEquals("/hotel", pathMatcher.combine("", "/hotel"));
assertEquals("/hotel",pathMatcher.combine("/hotel", null)); assertEquals("/hotel", pathMatcher.combine("/hotel", null));
assertEquals("/hotel",pathMatcher.combine("/hotel", "")); assertEquals("/hotel", pathMatcher.combine("/hotel", ""));
// TODO Do we need special handling when patterns contain multiple dots? // TODO Do we need special handling when patterns contain multiple dots?
} }
@ -703,21 +706,21 @@ public class PathPatternMatcherTests {
assertEquals(-1, comparator.compare(parse("*"), parse("*/**"))); assertEquals(-1, comparator.compare(parse("*"), parse("*/**")));
assertEquals(1, comparator.compare(parse("*/**"), parse("*"))); assertEquals(1, comparator.compare(parse("*/**"), parse("*")));
} }
@Test @Test
public void pathPatternComparator() { public void pathPatternComparator() {
PathPatternComparator ppc = new PathPatternComparator(); PathPatternComparator ppc = new PathPatternComparator();
assertEquals(0,ppc.compare(null, null)); assertEquals(0, ppc.compare(null, null));
assertEquals(1,ppc.compare(null, parse(""))); assertEquals(1, ppc.compare(null, parse("")));
assertEquals(-1,ppc.compare(parse(""), null)); assertEquals(-1, ppc.compare(parse(""), null));
assertEquals(0,ppc.compare(parse(""), parse(""))); assertEquals(0, ppc.compare(parse(""), parse("")));
} }
@Test @Test
public void patternCompareTo() { public void patternCompareTo() {
PathPatternParser p = new PathPatternParser(); PathPatternParser p = new PathPatternParser();
PathPattern pp = p.parse("/abc"); PathPattern pp = p.parse("/abc");
assertEquals(-1,pp.compareTo(null)); assertEquals(-1, pp.compareTo(null));
} }
@Test @Test
@ -816,7 +819,7 @@ public class PathPatternMatcherTests {
assertEquals("/*/login.*", paths.get(1).getPatternString()); assertEquals("/*/login.*", paths.get(1).getPatternString());
paths.clear(); paths.clear();
} }
@Test // SPR-13286 @Test // SPR-13286
public void caseInsensitive() { public void caseInsensitive() {
PathPatternParser pp = new PathPatternParser(); PathPatternParser pp = new PathPatternParser();
@ -831,12 +834,12 @@ public class PathPatternMatcherTests {
public void patternmessage() { public void patternmessage() {
PatternMessage[] values = PatternMessage.values(); PatternMessage[] values = PatternMessage.values();
assertNotNull(values); assertNotNull(values);
for (PatternMessage pm: values) { for (PatternMessage pm : values) {
String name = pm.toString(); String name = pm.toString();
assertEquals(pm.ordinal(),PatternMessage.valueOf(name).ordinal()); assertEquals(pm.ordinal(), PatternMessage.valueOf(name).ordinal());
} }
} }
private PathPattern parse(String path) { private PathPattern parse(String path) {
PathPatternParser pp = new PathPatternParser(); PathPatternParser pp = new PathPatternParser();
return pp.parse(path); return pp.parse(path);
@ -869,7 +872,7 @@ public class PathPatternMatcherTests {
assertFalse(pattern.matches(path)); assertFalse(pattern.matches(path));
} }
private Map<String,String> checkCapture(String uriTemplate, String path, String... keyValues) { private Map<String, String> checkCapture(String uriTemplate, String path, String... keyValues) {
PathPatternParser parser = new PathPatternParser(); PathPatternParser parser = new PathPatternParser();
PathPattern pattern = parser.parse(uriTemplate); PathPattern pattern = parser.parse(uriTemplate);
Map<String, String> matchResults = pattern.matchAndExtract(path); Map<String, String> matchResults = pattern.matchAndExtract(path);
@ -898,7 +901,7 @@ public class PathPatternMatcherTests {
PathPatternParser ppp = new PathPatternParser(); PathPatternParser ppp = new PathPatternParser();
PathPattern pp = ppp.parse(pattern); PathPattern pp = ppp.parse(pattern);
String s = pp.extractPathWithinPattern(path); String s = pp.extractPathWithinPattern(path);
assertEquals(expected,s); assertEquals(expected, s);
} }
static class TestPathCombiner { static class TestPathCombiner {

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2016 the original author or authors. * Copyright 2002-2017 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.web.util.patterns; package org.springframework.web.util.patterns;
import java.util.ArrayList; import java.util.ArrayList;
@ -26,13 +27,13 @@ import static org.junit.Assert.*;
/** /**
* Exercise the {@link PathPatternParser}. * Exercise the {@link PathPatternParser}.
* *
* @author Andy Clement * @author Andy Clement
*/ */
public class PathPatternParserTests { public class PathPatternParserTests {
private PathPattern p; private PathPattern p;
@Test @Test
public void basicPatterns() { public void basicPatterns() {
checkStructure("/"); checkStructure("/");
@ -42,23 +43,23 @@ public class PathPatternParserTests {
checkStructure("/foo/"); checkStructure("/foo/");
checkStructure("//"); checkStructure("//");
} }
@Test @Test
public void singleCharWildcardPatterns() { public void singleCharWildcardPatterns() {
p = checkStructure("?"); p = checkStructure("?");
assertPathElements(p , SingleCharWildcardedPathElement.class); assertPathElements(p, SingleCharWildcardedPathElement.class);
checkStructure("/?/"); checkStructure("/?/");
checkStructure("//?abc?/"); checkStructure("//?abc?/");
} }
@Test @Test
public void multiwildcardPattern() { public void multiwildcardPattern() {
p = checkStructure("/**"); p = checkStructure("/**");
assertPathElements(p,WildcardTheRestPathElement.class); assertPathElements(p, WildcardTheRestPathElement.class);
p = checkStructure("/**acb"); // this is not double wildcard use, it is / then **acb (an odd, unnecessary use of double *) 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); assertPathElements(p, SeparatorPathElement.class, RegexPathElement.class);
} }
@Test @Test
public void toStringTests() { public void toStringTests() {
assertEquals("CaptureTheRest(/{*foobar})", checkStructure("/{*foobar}").toChainString()); assertEquals("CaptureTheRest(/{*foobar})", checkStructure("/{*foobar}").toChainString());
@ -70,7 +71,7 @@ public class PathPatternParserTests {
assertEquals("Wildcard(*)", checkStructure("*").toChainString()); assertEquals("Wildcard(*)", checkStructure("*").toChainString());
assertEquals("WildcardTheRest(/**)", checkStructure("/**").toChainString()); assertEquals("WildcardTheRest(/**)", checkStructure("/**").toChainString());
} }
@Test @Test
public void captureTheRestPatterns() { public void captureTheRestPatterns() {
checkError("/{*foobar}x{abc}", 10, PatternMessage.NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST); checkError("/{*foobar}x{abc}", 10, PatternMessage.NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST);
@ -81,13 +82,13 @@ public class PathPatternParserTests {
checkError("/{*foobar}/", 10, PatternMessage.NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST); 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("/{*foobar}abc", 10, PatternMessage.NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST);
checkError("/{*f%obar}", 4, PatternMessage.ILLEGAL_CHARACTER_IN_CAPTURE_DESCRIPTOR); checkError("/{*f%obar}", 4, PatternMessage.ILLEGAL_CHARACTER_IN_CAPTURE_DESCRIPTOR);
checkError("/{*foobar}abc",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*oobar}",3,PatternMessage.ILLEGAL_CHARACTER_IN_CAPTURE_DESCRIPTOR); checkError("/{f*oobar}", 3, PatternMessage.ILLEGAL_CHARACTER_IN_CAPTURE_DESCRIPTOR);
checkError("/{*foobar}/abc",10,PatternMessage.NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST); 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}", 1, PatternMessage.CAPTURE_ALL_IS_STANDALONE_CONSTRUCT);
checkError("/{abc}{*foobar}{foo}",15,PatternMessage.NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST); checkError("/{abc}{*foobar}{foo}", 15, PatternMessage.NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST);
} }
@Test @Test
public void equalsAndHashcode() { public void equalsAndHashcode() {
PathPatternParser caseInsensitiveParser = new PathPatternParser(); PathPatternParser caseInsensitiveParser = new PathPatternParser();
@ -96,23 +97,23 @@ public class PathPatternParserTests {
PathPattern pp1 = caseInsensitiveParser.parse("/abc"); PathPattern pp1 = caseInsensitiveParser.parse("/abc");
PathPattern pp2 = caseInsensitiveParser.parse("/abc"); PathPattern pp2 = caseInsensitiveParser.parse("/abc");
PathPattern pp3 = caseInsensitiveParser.parse("/def"); PathPattern pp3 = caseInsensitiveParser.parse("/def");
assertEquals(pp1,pp2); assertEquals(pp1, pp2);
assertEquals(pp1.hashCode(),pp2.hashCode()); assertEquals(pp1.hashCode(), pp2.hashCode());
assertNotEquals(pp1, pp3); assertNotEquals(pp1, pp3);
assertFalse(pp1.equals("abc")); assertFalse(pp1.equals("abc"));
pp1 = caseInsensitiveParser.parse("/abc"); pp1 = caseInsensitiveParser.parse("/abc");
pp2 = caseSensitiveParser.parse("/abc"); pp2 = caseSensitiveParser.parse("/abc");
assertFalse(pp1.equals(pp2)); assertFalse(pp1.equals(pp2));
assertNotEquals(pp1.hashCode(),pp2.hashCode()); assertNotEquals(pp1.hashCode(), pp2.hashCode());
PathPatternParser alternateSeparatorParser = new PathPatternParser(':'); PathPatternParser alternateSeparatorParser = new PathPatternParser(':');
pp1 = caseInsensitiveParser.parse("abc"); pp1 = caseInsensitiveParser.parse("abc");
pp2 = alternateSeparatorParser.parse("abc"); pp2 = alternateSeparatorParser.parse("abc");
assertFalse(pp1.equals(pp2)); assertFalse(pp1.equals(pp2));
assertNotEquals(pp1.hashCode(),pp2.hashCode()); assertNotEquals(pp1.hashCode(), pp2.hashCode());
} }
@Test @Test
public void regexPathElementPatterns() { public void regexPathElementPatterns() {
checkError("/{var:[^/]*}", 8, PatternMessage.MISSING_CLOSE_CAPTURE); checkError("/{var:[^/]*}", 8, PatternMessage.MISSING_CLOSE_CAPTURE);
@ -120,78 +121,77 @@ public class PathPatternParserTests {
checkError("/{var:a{{1,2}}}", 6, PatternMessage.JDK_PATTERN_SYNTAX_EXCEPTION); checkError("/{var:a{{1,2}}}", 6, PatternMessage.JDK_PATTERN_SYNTAX_EXCEPTION);
p = checkStructure("/{var:\\\\}"); p = checkStructure("/{var:\\\\}");
assertEquals(CaptureVariablePathElement.class.getName(),p.getHeadSection().next.getClass().getName()); assertEquals(CaptureVariablePathElement.class.getName(), p.getHeadSection().next.getClass().getName());
assertTrue(p.matches("/\\")); assertTrue(p.matches("/\\"));
p = checkStructure("/{var:\\/}"); p = checkStructure("/{var:\\/}");
assertEquals(CaptureVariablePathElement.class.getName(),p.getHeadSection().next.getClass().getName()); assertEquals(CaptureVariablePathElement.class.getName(), p.getHeadSection().next.getClass().getName());
assertFalse(p.matches("/aaa")); assertFalse(p.matches("/aaa"));
p = checkStructure("/{var:a{1,2}}",1); p = checkStructure("/{var:a{1,2}}", 1);
assertEquals(CaptureVariablePathElement.class.getName(),p.getHeadSection().next.getClass().getName()); assertEquals(CaptureVariablePathElement.class.getName(), p.getHeadSection().next.getClass().getName());
p = checkStructure("/{var:[^\\/]*}",1); p = checkStructure("/{var:[^\\/]*}", 1);
assertEquals(CaptureVariablePathElement.class.getName(),p.getHeadSection().next.getClass().getName()); assertEquals(CaptureVariablePathElement.class.getName(), p.getHeadSection().next.getClass().getName());
Map<String, String> result = p.matchAndExtract("/foo"); Map<String, String> result = p.matchAndExtract("/foo");
assertEquals("foo",result.get("var")); assertEquals("foo", result.get("var"));
p = checkStructure("/{var:\\[*}",1); p = checkStructure("/{var:\\[*}", 1);
assertEquals(CaptureVariablePathElement.class.getName(),p.getHeadSection().next.getClass().getName()); assertEquals(CaptureVariablePathElement.class.getName(), p.getHeadSection().next.getClass().getName());
result = p.matchAndExtract("/[[["); result = p.matchAndExtract("/[[[");
assertEquals("[[[",result.get("var")); assertEquals("[[[", result.get("var"));
p = checkStructure("/{var:[\\{]*}",1); p = checkStructure("/{var:[\\{]*}", 1);
assertEquals(CaptureVariablePathElement.class.getName(),p.getHeadSection().next.getClass().getName()); assertEquals(CaptureVariablePathElement.class.getName(), p.getHeadSection().next.getClass().getName());
result = p.matchAndExtract("/{{{"); result = p.matchAndExtract("/{{{");
assertEquals("{{{",result.get("var")); assertEquals("{{{", result.get("var"));
p = checkStructure("/{var:[\\}]*}",1); p = checkStructure("/{var:[\\}]*}", 1);
assertEquals(CaptureVariablePathElement.class.getName(),p.getHeadSection().next.getClass().getName()); assertEquals(CaptureVariablePathElement.class.getName(), p.getHeadSection().next.getClass().getName());
result = p.matchAndExtract("/}}}"); result = p.matchAndExtract("/}}}");
assertEquals("}}}",result.get("var")); assertEquals("}}}", result.get("var"));
p = checkStructure("*"); p = checkStructure("*");
assertEquals(WildcardPathElement.class.getName(),p.getHeadSection().getClass().getName()); assertEquals(WildcardPathElement.class.getName(), p.getHeadSection().getClass().getName());
checkStructure("/*"); checkStructure("/*");
checkStructure("/*/"); checkStructure("/*/");
checkStructure("*/"); checkStructure("*/");
checkStructure("/*/"); checkStructure("/*/");
p = checkStructure("/*a*/"); p = checkStructure("/*a*/");
assertEquals(RegexPathElement.class.getName(),p.getHeadSection().next.getClass().getName()); assertEquals(RegexPathElement.class.getName(), p.getHeadSection().next.getClass().getName());
p = checkStructure("*/"); p = checkStructure("*/");
assertEquals(WildcardPathElement.class.getName(),p.getHeadSection().getClass().getName()); assertEquals(WildcardPathElement.class.getName(), p.getHeadSection().getClass().getName());
checkError("{foo}_{foo}", 0, PatternMessage.ILLEGAL_DOUBLE_CAPTURE, "foo"); checkError("{foo}_{foo}", 0, PatternMessage.ILLEGAL_DOUBLE_CAPTURE, "foo");
checkError("/{bar}/{bar}", 7, PatternMessage.ILLEGAL_DOUBLE_CAPTURE, "bar"); checkError("/{bar}/{bar}", 7, PatternMessage.ILLEGAL_DOUBLE_CAPTURE, "bar");
checkError("/{bar}/{bar}_{foo}", 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"); p = checkStructure("{symbolicName:[\\p{L}\\.]+}-sources-{version:[\\p{N}\\.]+}.jar");
assertEquals(RegexPathElement.class.getName(),p.getHeadSection().getClass().getName()); assertEquals(RegexPathElement.class.getName(), p.getHeadSection().getClass().getName());
} }
@Test @Test
public void completeCapturingPatterns() { public void completeCapturingPatterns() {
p = checkStructure("{foo}"); p = checkStructure("{foo}");
assertEquals(CaptureVariablePathElement.class.getName(),p.getHeadSection().getClass().getName()); assertEquals(CaptureVariablePathElement.class.getName(), p.getHeadSection().getClass().getName());
checkStructure("/{foo}"); checkStructure("/{foo}");
checkStructure("//{f}/"); checkStructure("//{f}/");
checkStructure("/{foo}/{bar}/{wibble}"); checkStructure("/{foo}/{bar}/{wibble}");
} }
@Test @Test
public void completeCaptureWithConstraints() { public void completeCaptureWithConstraints() {
p = checkStructure("{foo:...}"); p = checkStructure("{foo:...}");
assertPathElements(p, CaptureVariablePathElement.class); assertPathElements(p, CaptureVariablePathElement.class);
p = checkStructure("{foo:[0-9]*}"); p = checkStructure("{foo:[0-9]*}");
assertPathElements(p, CaptureVariablePathElement.class); assertPathElements(p, CaptureVariablePathElement.class);
checkError("{foo:}",5,PatternMessage.MISSING_REGEX_CONSTRAINT); checkError("{foo:}", 5, PatternMessage.MISSING_REGEX_CONSTRAINT);
} }
@Test @Test
public void partialCapturingPatterns() { public void partialCapturingPatterns() {
p = checkStructure("{foo}abc"); p = checkStructure("{foo}abc");
assertEquals(RegexPathElement.class.getName(),p.getHeadSection().getClass().getName()); assertEquals(RegexPathElement.class.getName(), p.getHeadSection().getClass().getName());
checkStructure("abc{foo}"); checkStructure("abc{foo}");
checkStructure("/abc{foo}"); checkStructure("/abc{foo}");
checkStructure("{foo}def/"); checkStructure("{foo}def/");
@ -203,177 +203,179 @@ public class PathPatternParserTests {
@Test @Test
public void illegalCapturePatterns() { public void illegalCapturePatterns() {
checkError("{abc/",4,PatternMessage.MISSING_CLOSE_CAPTURE); checkError("{abc/", 4, PatternMessage.MISSING_CLOSE_CAPTURE);
checkError("{abc:}/",5,PatternMessage.MISSING_REGEX_CONSTRAINT); checkError("{abc:}/", 5, PatternMessage.MISSING_REGEX_CONSTRAINT);
checkError("{",1,PatternMessage.MISSING_CLOSE_CAPTURE); checkError("{", 1, PatternMessage.MISSING_CLOSE_CAPTURE);
checkError("{abc",4,PatternMessage.MISSING_CLOSE_CAPTURE); checkError("{abc", 4, PatternMessage.MISSING_CLOSE_CAPTURE);
checkError("{/}",1,PatternMessage.MISSING_CLOSE_CAPTURE); checkError("{/}", 1, PatternMessage.MISSING_CLOSE_CAPTURE);
checkError("//{",3,PatternMessage.MISSING_CLOSE_CAPTURE); checkError("//{", 3, PatternMessage.MISSING_CLOSE_CAPTURE);
checkError("}",0,PatternMessage.MISSING_OPEN_CAPTURE); checkError("}", 0, PatternMessage.MISSING_OPEN_CAPTURE);
checkError("/}",1,PatternMessage.MISSING_OPEN_CAPTURE); checkError("/}", 1, PatternMessage.MISSING_OPEN_CAPTURE);
checkError("def}",3,PatternMessage.MISSING_OPEN_CAPTURE); checkError("def}", 3, PatternMessage.MISSING_OPEN_CAPTURE);
checkError("//{/}",3,PatternMessage.MISSING_CLOSE_CAPTURE); checkError("//{/}", 3, PatternMessage.MISSING_CLOSE_CAPTURE);
checkError("//{{/}",3,PatternMessage.ILLEGAL_NESTED_CAPTURE); checkError("//{{/}", 3, PatternMessage.ILLEGAL_NESTED_CAPTURE);
checkError("//{abc{/}",6,PatternMessage.ILLEGAL_NESTED_CAPTURE); checkError("//{abc{/}", 6, PatternMessage.ILLEGAL_NESTED_CAPTURE);
checkError("/{0abc}/abc",2,PatternMessage.ILLEGAL_CHARACTER_AT_START_OF_CAPTURE_DESCRIPTOR); checkError("/{0abc}/abc", 2, PatternMessage.ILLEGAL_CHARACTER_AT_START_OF_CAPTURE_DESCRIPTOR);
checkError("/{a?bc}/abc",3,PatternMessage.ILLEGAL_CHARACTER_IN_CAPTURE_DESCRIPTOR); checkError("/{a?bc}/abc", 3, PatternMessage.ILLEGAL_CHARACTER_IN_CAPTURE_DESCRIPTOR);
checkError("/{abc}_{abc}",1,PatternMessage.ILLEGAL_DOUBLE_CAPTURE); 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);
checkError("/foobar/{abc:..}_{abc:..}",8,PatternMessage.ILLEGAL_DOUBLE_CAPTURE); checkError("/foobar/{abc:..}_{abc:..}", 8, PatternMessage.ILLEGAL_DOUBLE_CAPTURE);
PathPattern pp = parse("/{abc:foo(bar)}"); PathPattern pp = parse("/{abc:foo(bar)}");
try { try {
pp.matchAndExtract("/foo"); pp.matchAndExtract("/foo");
fail("Should have raised exception"); fail("Should have raised exception");
} catch (IllegalArgumentException iae) { }
assertEquals("No capture groups allowed in the constraint regex: foo(bar)",iae.getMessage()); catch (IllegalArgumentException iae) {
assertEquals("No capture groups allowed in the constraint regex: foo(bar)", iae.getMessage());
} }
try { try {
pp.matchAndExtract("/foobar"); pp.matchAndExtract("/foobar");
fail("Should have raised exception"); fail("Should have raised exception");
} catch (IllegalArgumentException iae) { }
assertEquals("No capture groups allowed in the constraint regex: foo(bar)",iae.getMessage()); catch (IllegalArgumentException iae) {
assertEquals("No capture groups allowed in the constraint regex: foo(bar)", iae.getMessage());
} }
} }
@Test @Test
public void badPatterns() { public void badPatterns() {
// checkError("/{foo}{bar}/",6,PatternMessage.CANNOT_HAVE_ADJACENT_CAPTURES); // checkError("/{foo}{bar}/",6,PatternMessage.CANNOT_HAVE_ADJACENT_CAPTURES);
checkError("/{?}/",2,PatternMessage.ILLEGAL_CHARACTER_AT_START_OF_CAPTURE_DESCRIPTOR,"?"); checkError("/{?}/", 2, PatternMessage.ILLEGAL_CHARACTER_AT_START_OF_CAPTURE_DESCRIPTOR, "?");
checkError("/{a?b}/",3,PatternMessage.ILLEGAL_CHARACTER_IN_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("/{ }",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,"%"); checkError("/{%:[0-9]*}", 2, PatternMessage.ILLEGAL_CHARACTER_AT_START_OF_CAPTURE_DESCRIPTOR, "%");
} }
@Test @Test
public void patternPropertyGetCaptureCountTests() { public void patternPropertyGetCaptureCountTests() {
// Test all basic section types // Test all basic section types
assertEquals(1,parse("{foo}").getCapturedVariableCount()); assertEquals(1, parse("{foo}").getCapturedVariableCount());
assertEquals(0,parse("foo").getCapturedVariableCount()); assertEquals(0, parse("foo").getCapturedVariableCount());
assertEquals(1,parse("{*foobar}").getCapturedVariableCount()); assertEquals(1, parse("{*foobar}").getCapturedVariableCount());
assertEquals(1,parse("/{*foobar}").getCapturedVariableCount()); assertEquals(1, parse("/{*foobar}").getCapturedVariableCount());
assertEquals(0,parse("/**").getCapturedVariableCount()); assertEquals(0, parse("/**").getCapturedVariableCount());
assertEquals(1,parse("{abc}asdf").getCapturedVariableCount()); assertEquals(1, parse("{abc}asdf").getCapturedVariableCount());
assertEquals(1,parse("{abc}_*").getCapturedVariableCount()); assertEquals(1, parse("{abc}_*").getCapturedVariableCount());
assertEquals(2,parse("{abc}_{def}").getCapturedVariableCount()); assertEquals(2, parse("{abc}_{def}").getCapturedVariableCount());
assertEquals(0,parse("/").getCapturedVariableCount()); assertEquals(0, parse("/").getCapturedVariableCount());
assertEquals(0,parse("a?b").getCapturedVariableCount()); assertEquals(0, parse("a?b").getCapturedVariableCount());
assertEquals(0,parse("*").getCapturedVariableCount()); assertEquals(0, parse("*").getCapturedVariableCount());
// Test on full templates // Test on full templates
assertEquals(0,parse("/foo/bar").getCapturedVariableCount()); assertEquals(0, parse("/foo/bar").getCapturedVariableCount());
assertEquals(1,parse("/{foo}").getCapturedVariableCount()); assertEquals(1, parse("/{foo}").getCapturedVariableCount());
assertEquals(2,parse("/{foo}/{bar}").getCapturedVariableCount()); assertEquals(2, parse("/{foo}/{bar}").getCapturedVariableCount());
assertEquals(4,parse("/{foo}/{bar}_{goo}_{wibble}/abc/bar").getCapturedVariableCount()); assertEquals(4, parse("/{foo}/{bar}_{goo}_{wibble}/abc/bar").getCapturedVariableCount());
} }
@Test @Test
public void patternPropertyGetWildcardCountTests() { public void patternPropertyGetWildcardCountTests() {
// Test all basic section types // Test all basic section types
assertEquals(computeScore(1,0),parse("{foo}").getScore()); assertEquals(computeScore(1, 0), parse("{foo}").getScore());
assertEquals(computeScore(0,0),parse("foo").getScore()); assertEquals(computeScore(0, 0), parse("foo").getScore());
assertEquals(computeScore(0,0),parse("{*foobar}").getScore()); assertEquals(computeScore(0, 0), parse("{*foobar}").getScore());
// assertEquals(1,parse("/**").getScore()); // assertEquals(1,parse("/**").getScore());
assertEquals(computeScore(1,0),parse("{abc}asdf").getScore()); assertEquals(computeScore(1, 0), parse("{abc}asdf").getScore());
assertEquals(computeScore(1,1),parse("{abc}_*").getScore()); assertEquals(computeScore(1, 1), parse("{abc}_*").getScore());
assertEquals(computeScore(2,0),parse("{abc}_{def}").getScore()); assertEquals(computeScore(2, 0), parse("{abc}_{def}").getScore());
assertEquals(computeScore(0,0),parse("/").getScore()); assertEquals(computeScore(0, 0), parse("/").getScore());
assertEquals(computeScore(0,0),parse("a?b").getScore()); // currently deliberate assertEquals(computeScore(0, 0), parse("a?b").getScore()); // currently deliberate
assertEquals(computeScore(0,1),parse("*").getScore()); assertEquals(computeScore(0, 1), parse("*").getScore());
// Test on full templates // Test on full templates
assertEquals(computeScore(0,0),parse("/foo/bar").getScore()); assertEquals(computeScore(0, 0), parse("/foo/bar").getScore());
assertEquals(computeScore(1,0),parse("/{foo}").getScore()); assertEquals(computeScore(1, 0), parse("/{foo}").getScore());
assertEquals(computeScore(2,0),parse("/{foo}/{bar}").getScore()); assertEquals(computeScore(2, 0), parse("/{foo}/{bar}").getScore());
assertEquals(computeScore(4,0),parse("/{foo}/{bar}_{goo}_{wibble}/abc/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()); assertEquals(computeScore(4, 3), parse("/{foo}/*/*_*/{bar}_{goo}_{wibble}/abc/bar").getScore());
} }
@Test @Test
public void multipleSeparatorPatterns() { public void multipleSeparatorPatterns() {
p = checkStructure("///aaa"); p = checkStructure("///aaa");
assertEquals(4,p.getNormalizedLength()); assertEquals(4, p.getNormalizedLength());
assertPathElements(p,SeparatorPathElement.class,LiteralPathElement.class); assertPathElements(p, SeparatorPathElement.class, LiteralPathElement.class);
p = checkStructure("///aaa////aaa/b"); p = checkStructure("///aaa////aaa/b");
assertEquals(10,p.getNormalizedLength()); assertEquals(10, p.getNormalizedLength());
assertPathElements(p,SeparatorPathElement.class, LiteralPathElement.class, assertPathElements(p, SeparatorPathElement.class, LiteralPathElement.class,
SeparatorPathElement.class, LiteralPathElement.class, SeparatorPathElement.class, LiteralPathElement.class); SeparatorPathElement.class, LiteralPathElement.class, SeparatorPathElement.class, LiteralPathElement.class);
p = checkStructure("/////**"); p = checkStructure("/////**");
assertEquals(1,p.getNormalizedLength()); assertEquals(1, p.getNormalizedLength());
assertPathElements(p,WildcardTheRestPathElement.class); assertPathElements(p, WildcardTheRestPathElement.class);
} }
@Test @Test
public void patternPropertyGetLengthTests() { public void patternPropertyGetLengthTests() {
// Test all basic section types // Test all basic section types
assertEquals(1,parse("{foo}").getNormalizedLength()); assertEquals(1, parse("{foo}").getNormalizedLength());
assertEquals(3,parse("foo").getNormalizedLength()); assertEquals(3, parse("foo").getNormalizedLength());
assertEquals(1,parse("{*foobar}").getNormalizedLength()); assertEquals(1, parse("{*foobar}").getNormalizedLength());
assertEquals(1,parse("/{*foobar}").getNormalizedLength()); assertEquals(1, parse("/{*foobar}").getNormalizedLength());
assertEquals(1,parse("/**").getNormalizedLength()); assertEquals(1, parse("/**").getNormalizedLength());
assertEquals(5,parse("{abc}asdf").getNormalizedLength()); assertEquals(5, parse("{abc}asdf").getNormalizedLength());
assertEquals(3,parse("{abc}_*").getNormalizedLength()); assertEquals(3, parse("{abc}_*").getNormalizedLength());
assertEquals(3,parse("{abc}_{def}").getNormalizedLength()); assertEquals(3, parse("{abc}_{def}").getNormalizedLength());
assertEquals(1,parse("/").getNormalizedLength()); assertEquals(1, parse("/").getNormalizedLength());
assertEquals(3,parse("a?b").getNormalizedLength()); assertEquals(3, parse("a?b").getNormalizedLength());
assertEquals(1,parse("*").getNormalizedLength()); assertEquals(1, parse("*").getNormalizedLength());
// Test on full templates // Test on full templates
assertEquals(8,parse("/foo/bar").getNormalizedLength()); assertEquals(8, parse("/foo/bar").getNormalizedLength());
assertEquals(2,parse("/{foo}").getNormalizedLength()); assertEquals(2, parse("/{foo}").getNormalizedLength());
assertEquals(4,parse("/{foo}/{bar}").getNormalizedLength()); assertEquals(4, parse("/{foo}/{bar}").getNormalizedLength());
assertEquals(16,parse("/{foo}/{bar}_{goo}_{wibble}/abc/bar").getNormalizedLength()); assertEquals(16, parse("/{foo}/{bar}_{goo}_{wibble}/abc/bar").getNormalizedLength());
} }
@Test @Test
public void compareTests() { public void compareTests() {
PathPattern p1,p2,p3; PathPattern p1, p2, p3;
// Based purely on number of captures // Based purely on number of captures
p1 = parse("{a}"); p1 = parse("{a}");
p2 = parse("{a}/{b}"); p2 = parse("{a}/{b}");
p3 = parse("{a}/{b}/{c}"); p3 = parse("{a}/{b}/{c}");
assertEquals(-1,p1.compareTo(p2)); // Based on number of captures assertEquals(-1, p1.compareTo(p2)); // Based on number of captures
List<PathPattern> patterns = new ArrayList<>(); List<PathPattern> patterns = new ArrayList<>();
patterns.add(p2); patterns.add(p2);
patterns.add(p3); patterns.add(p3);
patterns.add(p1); patterns.add(p1);
Collections.sort(patterns,new PathPatternComparator()); Collections.sort(patterns, new PathPatternComparator());
assertEquals(p1,patterns.get(0)); assertEquals(p1, patterns.get(0));
// Based purely on length // Based purely on length
p1 = parse("/a/b/c"); p1 = parse("/a/b/c");
p2 = parse("/a/boo/c/doo"); p2 = parse("/a/boo/c/doo");
p3 = parse("/asdjflaksjdfjasdf"); p3 = parse("/asdjflaksjdfjasdf");
assertEquals(1,p1.compareTo(p2)); assertEquals(1, p1.compareTo(p2));
patterns = new ArrayList<>(); patterns = new ArrayList<>();
patterns.add(p2); patterns.add(p2);
patterns.add(p3); patterns.add(p3);
patterns.add(p1); patterns.add(p1);
Collections.sort(patterns,new PathPatternComparator()); Collections.sort(patterns, new PathPatternComparator());
assertEquals(p3,patterns.get(0)); assertEquals(p3, patterns.get(0));
// Based purely on 'wildness' // Based purely on 'wildness'
p1 = parse("/*"); p1 = parse("/*");
p2 = parse("/*/*"); p2 = parse("/*/*");
p3 = parse("/*/*/*_*"); p3 = parse("/*/*/*_*");
assertEquals(-1,p1.compareTo(p2)); assertEquals(-1, p1.compareTo(p2));
patterns = new ArrayList<>(); patterns = new ArrayList<>();
patterns.add(p2); patterns.add(p2);
patterns.add(p3); patterns.add(p3);
patterns.add(p1); patterns.add(p1);
Collections.sort(patterns,new PathPatternComparator()); Collections.sort(patterns, new PathPatternComparator());
assertEquals(p1,patterns.get(0)); assertEquals(p1, patterns.get(0));
// Based purely on catchAll // Based purely on catchAll
p1 = parse("{*foobar}"); p1 = parse("{*foobar}");
p2 = parse("{*goo}"); p2 = parse("{*goo}");
assertEquals(0,p1.compareTo(p2)); assertEquals(0, p1.compareTo(p2));
p1 = parse("/{*foobar}"); p1 = parse("/{*foobar}");
p2 = parse("/abc/{*ww}"); p2 = parse("/abc/{*ww}");
assertEquals(+1,p1.compareTo(p2)); assertEquals(+1, p1.compareTo(p2));
assertEquals(-1,p2.compareTo(p1)); assertEquals(-1, p2.compareTo(p1));
p3 = parse("/this/that/theother"); p3 = parse("/this/that/theother");
assertTrue(p1.isCatchAll()); assertTrue(p1.isCatchAll());
@ -383,20 +385,20 @@ public class PathPatternParserTests {
patterns.add(p2); patterns.add(p2);
patterns.add(p3); patterns.add(p3);
patterns.add(p1); patterns.add(p1);
Collections.sort(patterns,new PathPatternComparator()); Collections.sort(patterns, new PathPatternComparator());
assertEquals(p3,patterns.get(0)); assertEquals(p3, patterns.get(0));
assertEquals(p2,patterns.get(1)); assertEquals(p2, patterns.get(1));
patterns = new ArrayList<>(); patterns = new ArrayList<>();
patterns.add(parse("/abc")); patterns.add(parse("/abc"));
patterns.add(null); patterns.add(null);
patterns.add(parse("/def")); patterns.add(parse("/def"));
Collections.sort(patterns,new PathPatternComparator()); Collections.sort(patterns, new PathPatternComparator());
assertNull(patterns.get(2)); assertNull(patterns.get(2));
} }
// --- // ---
private PathPattern parse(String pattern) { private PathPattern parse(String pattern) {
PathPatternParser patternParser = new PathPatternParser(); PathPatternParser patternParser = new PathPatternParser();
return patternParser.parse(pattern); return patternParser.parse(pattern);
@ -408,22 +410,22 @@ public class PathPatternParserTests {
*/ */
private PathPattern checkStructure(String pattern) { private PathPattern checkStructure(String pattern) {
int count = 0; int count = 0;
for (int i=0;i<pattern.length();i++) { for (int i = 0; i < pattern.length(); i++) {
if (pattern.charAt(i)=='/') { if (pattern.charAt(i) == '/') {
// if (peekDoubleWildcard(pattern,i)) { // if (peekDoubleWildcard(pattern,i)) {
// // it is /** // // it is /**
// i+=2; // i+=2;
// } else { // } else {
count++; count++;
// } // }
} }
} }
return checkStructure(pattern,count); return checkStructure(pattern, count);
} }
private PathPattern checkStructure(String pattern, int expectedSeparatorCount) { private PathPattern checkStructure(String pattern, int expectedSeparatorCount) {
p = parse(pattern); p = parse(pattern);
assertEquals(pattern,p.getPatternString()); assertEquals(pattern, p.getPatternString());
// assertEquals(expectedSeparatorCount,p.getSeparatorCount()); // assertEquals(expectedSeparatorCount,p.getSeparatorCount());
return p; return p;
} }
@ -432,14 +434,15 @@ public class PathPatternParserTests {
try { try {
p = parse(pattern); p = parse(pattern);
fail("Expected to fail"); fail("Expected to fail");
} catch (PatternParseException ppe) { }
catch (PatternParseException ppe) {
// System.out.println(ppe.toDetailedString()); // System.out.println(ppe.toDetailedString());
assertEquals(ppe.toDetailedString(), expectedPos, ppe.getPosition()); assertEquals(ppe.toDetailedString(), expectedPos, ppe.getPosition());
assertEquals(ppe.toDetailedString(), expectedMessage, ppe.getMessageType()); assertEquals(ppe.toDetailedString(), expectedMessage, ppe.getMessageType());
if (expectedInserts.length!=0) { if (expectedInserts.length != 0) {
assertEquals(ppe.getInserts().length,expectedInserts.length); assertEquals(ppe.getInserts().length, expectedInserts.length);
for (int i=0;i<expectedInserts.length;i++) { for (int i = 0; i < expectedInserts.length; i++) {
assertEquals("Insert at position "+i+" is wrong",expectedInserts[i],ppe.getInserts()[i]); assertEquals("Insert at position " + i + " is wrong", expectedInserts[i], ppe.getInserts()[i]);
} }
} }
} }
@ -448,18 +451,18 @@ public class PathPatternParserTests {
@SafeVarargs @SafeVarargs
private final void assertPathElements(PathPattern p, Class<? extends PathElement>... sectionClasses) { private final void assertPathElements(PathPattern p, Class<? extends PathElement>... sectionClasses) {
PathElement head = p.getHeadSection(); PathElement head = p.getHeadSection();
for (int i=0;i<sectionClasses.length;i++) { for (int i = 0; i < sectionClasses.length; i++) {
if (head == null) { if (head == null) {
fail("Ran out of data in parsed pattern. Pattern is: "+p.toChainString()); 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()); assertEquals("Not expected section type. Pattern is: " + p.toChainString(), sectionClasses[i].getSimpleName(), head.getClass().getSimpleName());
head = head.next; head = head.next;
} }
} }
// Mirrors the score computation logic in PathPattern // Mirrors the score computation logic in PathPattern
private int computeScore(int capturedVariableCount, int wildcardCount) { private int computeScore(int capturedVariableCount, int wildcardCount) {
return capturedVariableCount+wildcardCount*100; return capturedVariableCount + wildcardCount * 100;
} }
} }