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,9 +27,15 @@ 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 {
@ -87,7 +94,8 @@ public class ParsingPathMatcher implements PathMatcher {
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);

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;

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

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;
@ -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,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,11 +22,13 @@ 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;
/** /**

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;
@ -26,7 +27,37 @@ import org.springframework.util.PathMatcher;
* 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> {
@ -87,7 +118,8 @@ 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
&& s.next instanceof WildcardPathElement && s.next.next == null) {
this.endsWithSeparatorWildcard = true; this.endsWithSeparatorWildcard = true;
} }
s = s.next; s = s.next;
@ -101,10 +133,12 @@ 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;
} }
} }
@ -119,7 +153,8 @@ public class PathPattern implements Comparable<PathPattern> {
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);
@ -135,11 +170,14 @@ public class PathPattern implements Comparable<PathPattern> {
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 { }
else {
if (path == null || path.length() == 0) { 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 + "\"");
} }
} }
} }
@ -175,7 +213,8 @@ public class PathPattern implements Comparable<PathPattern> {
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) {
@ -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
@ -364,7 +405,8 @@ public class PathPattern implements Comparable<PathPattern> {
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;
} }
} }
@ -395,10 +437,12 @@ public class PathPattern implements Comparable<PathPattern> {
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;
} }
@ -439,7 +483,8 @@ public class PathPattern implements Comparable<PathPattern> {
} }
/** /**
* 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

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;
@ -29,7 +30,8 @@ public class PathPatternComparator implements Comparator<PathPattern> {
// 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 {
@ -134,26 +136,31 @@ public class PathPatternParser {
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));
@ -222,7 +232,8 @@ public class PathPatternParser {
} }
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,
@ -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 { }
else {
throw new IllegalStateException("Expected SeparatorPathElement but was " + currentPE); throw new IllegalStateException("Expected SeparatorPathElement but was " + currentPE);
} }
} else { }
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 {
if (isCaptureTheRestVariable) {
throw new PatternParseException(pathElementStart, pathPatternData, PatternMessage.CAPTURE_ALL_IS_STANDALONE_CONSTRUCT);
} }
RegexPathElement newRegexSection = new RegexPathElement(pathElementStart, pathElementText, caseSensitive, pathPatternData); else {
if (isCaptureTheRestVariable) {
throw new PatternParseException(pathElementStart, pathPatternData,
PatternMessage.CAPTURE_ALL_IS_STANDALONE_CONSTRUCT);
}
RegexPathElement newRegexSection = new RegexPathElement(pathElementStart, pathElementText,
caseSensitive, pathPatternData);
for (String variableName : newRegexSection.getVariableNames()) { 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,7 +377,8 @@ 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;
@ -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;
@ -22,6 +23,7 @@ import java.util.Comparator;
* 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> {
@ -36,12 +38,14 @@ public class PatternComparatorConsideringPath implements Comparator<PathPattern>
// 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,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.text.MessageFormat; import java.text.MessageFormat;
@ -21,6 +22,7 @@ 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 {

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 {

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;
@ -28,6 +29,7 @@ import org.springframework.web.util.patterns.PathPattern.MatchingContext;
* 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
} }

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;
@ -23,6 +24,7 @@ import org.springframework.web.util.patterns.PathPattern.MatchingContext;
* 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 {
@ -50,7 +52,8 @@ class SeparatorPathElement extends PathElement {
} }
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

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;
@ -22,6 +23,7 @@ import org.springframework.web.util.patterns.PathPattern.MatchingContext;
* 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 {
@ -40,7 +42,8 @@ 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]);
@ -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,7 +77,8 @@ 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
} }

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;
/** /**
@ -21,10 +22,12 @@ package org.springframework.web.util.patterns;
* 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) {

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;
@ -22,6 +23,7 @@ import org.springframework.web.util.patterns.PathPattern.MatchingContext;
* 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
} }

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;
@ -22,6 +23,7 @@ import org.springframework.web.util.patterns.PathPattern.MatchingContext;
* '/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 {

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;
@ -511,13 +512,15 @@ public class PathPatternMatcherTests {
try { try {
checkCapture("/{one}/", "//", "one", ""); checkCapture("/{one}/", "//", "one", "");
fail("Expected exception"); fail("Expected exception");
} catch (IllegalStateException e) { }
catch (IllegalStateException e) {
assertEquals("Pattern \"/{one}/\" is not a match for \"//\"", e.getMessage()); 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) { }
catch (IllegalStateException e) {
assertEquals("Pattern \"\" is not a match for \"/abc\"", e.getMessage()); assertEquals("Pattern \"\" is not a match for \"/abc\"", e.getMessage());
} }
assertEquals(0, checkCapture("", "").size()); assertEquals(0, checkCapture("", "").size());

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;
@ -167,7 +168,6 @@ public class PathPatternParserTests {
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
@ -224,13 +224,15 @@ public class PathPatternParserTests {
try { try {
pp.matchAndExtract("/foo"); pp.matchAndExtract("/foo");
fail("Should have raised exception"); fail("Should have raised exception");
} catch (IllegalArgumentException iae) { }
catch (IllegalArgumentException iae) {
assertEquals("No capture groups allowed in the constraint regex: foo(bar)", iae.getMessage()); 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) { }
catch (IllegalArgumentException iae) {
assertEquals("No capture groups allowed in the constraint regex: foo(bar)", iae.getMessage()); assertEquals("No capture groups allowed in the constraint regex: foo(bar)", iae.getMessage());
} }
} }
@ -432,7 +434,8 @@ 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());