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");
* 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
* limitations under the License.
*/
package org.springframework.web.util;
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
* @since 5.0
* @see PathPattern
*/
public class ParsingPathMatcher implements PathMatcher {
Map<String,PathPattern> cache = new HashMap<>();
Map<String, PathPattern> cache = new HashMap<>();
PathPatternParser parser;
public ParsingPathMatcher() {
parser = new PathPatternParser();
}
@@ -74,27 +81,28 @@ public class ParsingPathMatcher implements PathMatcher {
public Comparator<String> getPatternComparator(String path) {
return new PathPatternStringComparatorConsideringPath(path);
}
class PathPatternStringComparatorConsideringPath implements Comparator<String> {
PatternComparatorConsideringPath ppcp;
public PathPatternStringComparatorConsideringPath(String path) {
ppcp = new PatternComparatorConsideringPath(path);
}
@Override
public int compare(String o1, String o2) {
if (o1 == null) {
return (o2==null?0:+1);
} else if (o2 == null) {
return (o2 == null ? 0 : +1);
}
else if (o2 == null) {
return -1;
}
PathPattern p1 = getPathPattern(o1);
PathPattern p2 = getPathPattern(o2);
return ppcp.compare(p1,p2);
return ppcp.compare(p1, p2);
}
}
@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");
* 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
* limitations under the License.
*/
package org.springframework.web.util.patterns;
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
* '/foo/{*foobar}' the /{*foobar} is represented as a {@link CaptureTheRestPathElement}.
*
*
* @author Andy Clement
*/
class CaptureTheRestPathElement extends PathElement {
private String variableName;
private char separator;
/**
@@ -45,14 +46,14 @@ class CaptureTheRestPathElement extends PathElement {
// No need to handle 'match start' checking as this captures everything
// anyway and cannot be followed by anything else
// assert next == null
// If there is more data, it must start with the separator
if (candidateIndex<matchingContext.candidateLength &&
matchingContext.candidate[candidateIndex] != separator) {
if (candidateIndex < matchingContext.candidateLength &&
matchingContext.candidate[candidateIndex] != separator) {
return false;
}
while ((candidateIndex+1)<matchingContext.candidateLength &&
matchingContext.candidate[candidateIndex+1] == separator) {
while ((candidateIndex + 1) < matchingContext.candidateLength &&
matchingContext.candidate[candidateIndex + 1] == separator) {
candidateIndex++;
}
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");
* 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
* limitations under the License.
*/
package org.springframework.web.util.patterns;
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
* '/foo/{bar}/goo' the {bar} is represented as a {@link CaptureVariablePathElement}.
*
*
* @author Andy Clement
*/
class CaptureVariablePathElement extends PathElement {
private String variableName;
private java.util.regex.Pattern constraintPattern;
/**
* @param pos the position in the pattern of this capture element
* @param captureDescriptor is of the form {AAAAA[:pattern]}
@@ -47,12 +48,14 @@ class CaptureVariablePathElement extends PathElement {
if (colon == -1) {
// no constraint
variableName = new String(captureDescriptor, 1, captureDescriptor.length - 2);
} else {
}
else {
variableName = new String(captureDescriptor, 1, colon - 1);
if (caseSensitive) {
constraintPattern = java.util.regex.Pattern
.compile(new String(captureDescriptor, colon + 1, captureDescriptor.length - colon - 2));
} else {
}
else {
constraintPattern = java.util.regex.Pattern.compile(
new String(captureDescriptor, colon + 1, captureDescriptor.length - colon - 2),
java.util.regex.Pattern.CASE_INSENSITIVE);
@@ -69,7 +72,7 @@ class CaptureVariablePathElement extends PathElement {
candidateCapture = new SubSequence(matchingContext.candidate, candidateIndex, nextPos);
Matcher m = constraintPattern.matcher(candidateCapture);
if (m.groupCount() != 0) {
throw new IllegalArgumentException("No capture groups allowed in the constraint regex: "+constraintPattern.pattern());
throw new IllegalArgumentException("No capture groups allowed in the constraint regex: " + constraintPattern.pattern());
}
if (!m.matches()) {
return false;
@@ -78,10 +81,12 @@ class CaptureVariablePathElement extends PathElement {
boolean match = false;
if (next == null) {
match = (nextPos == matchingContext.candidateLength);
} else {
}
else {
if (matchingContext.isMatchStartMatching && nextPos == matchingContext.candidateLength) {
match = true; // no more data but matches up to this point
} else {
}
else {
match = next.matches(nextPos, matchingContext);
}
}
@@ -90,7 +95,7 @@ class CaptureVariablePathElement extends PathElement {
}
return match;
}
public String getVariableName() {
return this.variableName;
}
@@ -98,7 +103,7 @@ class CaptureVariablePathElement extends PathElement {
public String toString() {
return "CaptureVariable({" + variableName + (constraintPattern == null ? "" : ":" + constraintPattern.pattern()) + "})";
}
@Override
public int getNormalizedLength() {
return 1;
@@ -113,7 +118,7 @@ class CaptureVariablePathElement extends PathElement {
public int getCaptureCount() {
return 1;
}
@Override
public int getScore() {
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");
* 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
* limitations under the License.
*/
package org.springframework.web.util.patterns;
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
* literal path elements 'foo', 'bar' and 'goo'.
*
*
* @author Andy Clement
*/
class LiteralPathElement extends PathElement {
private char[] text;
private int len;
private boolean caseSensitive;
public LiteralPathElement(int pos, char[] literalText, boolean caseSensitive) {
@@ -37,7 +38,8 @@ class LiteralPathElement extends PathElement {
this.caseSensitive = caseSensitive;
if (caseSensitive) {
this.text = literalText;
} else {
}
else {
// Force all the text lower case to make matching faster
this.text = new char[literalText.length];
for (int i = 0; i < len; i++) {
@@ -57,7 +59,8 @@ class LiteralPathElement extends PathElement {
return false;
}
}
} else {
}
else {
for (int i = 0; i < len; i++) {
// TODO revisit performance if doing a lot of case insensitive matching
if (Character.toLowerCase(matchingContext.candidate[candidateIndex++]) != text[i]) {
@@ -67,7 +70,8 @@ class LiteralPathElement extends PathElement {
}
if (next == null) {
return candidateIndex == matchingContext.candidateLength;
} else {
}
else {
if (matchingContext.isMatchStartMatching && candidateIndex == matchingContext.candidateLength) {
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");
* 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
* limitations under the License.
*/
package org.springframework.web.util.patterns;
import org.springframework.web.util.patterns.PathPattern.MatchingContext;
/**
* Common supertype for the Ast nodes created to represent a path pattern.
*
*
* @author Andy Clement
* @since 5.0
*/
abstract class PathElement {
// Score related
protected static final int WILDCARD_WEIGHT = 100;
protected static final int CAPTURE_VARIABLE_WEIGHT = 1;
/**
* Position in the pattern where this path element starts
*/
protected int pos;
/**
* The next path element in the chain
*/
protected PathElement next;
/**
* The previous path element in the chain
*/
@@ -53,7 +56,7 @@ abstract class PathElement {
/**
* Attempt to match this path element.
*
*
* @param candidatePos the current position within the candidate path
* @param matchingContext encapsulates context for the match including the candidate
* @return true if matches, otherwise false
@@ -64,7 +67,7 @@ abstract class PathElement {
* @return the length of the path element where captures are considered to be one character long
*/
public abstract int getNormalizedLength();
/**
* @return the number of variables captured by the path element
*/
@@ -78,7 +81,7 @@ abstract class PathElement {
public int getWildcardCount() {
return 0;
}
/**
* @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");
* 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
* limitations under the License.
*/
package org.springframework.web.util.patterns;
import java.util.Collections;
@@ -25,19 +26,49 @@ import org.springframework.util.PathMatcher;
* Represents a parsed path pattern. Includes a chain of path elements
* for fast matching and accumulates computed state for quick comparison of
* patterns.
*
*
* <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
* @since 5.0
*/
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 */
private PathElement head;
/** The text of the parsed pattern */
private String patternString;
/** The separator used when parsing the pattern */
private char separator;
@@ -54,7 +85,7 @@ public class PathPattern implements Comparable<PathPattern> {
* Useful when comparing two patterns.
*/
int normalizedLength;
/**
* 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) {
this.isCatchAll = true;
}
if (s instanceof SeparatorPathElement && s.next!=null && s.next instanceof WildcardPathElement && s.next.next == null) {
this.endsWithSeparatorWildcard=true;
if (s instanceof SeparatorPathElement && s.next != null
&& s.next instanceof WildcardPathElement && s.next.next == null) {
this.endsWithSeparatorWildcard = true;
}
s = s.next;
}
@@ -101,28 +133,31 @@ public class PathPattern implements Comparable<PathPattern> {
public boolean matches(String path) {
if (head == null) {
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) {
path = ""; // Will allow CaptureTheRest to bind the variable to empty
} else {
}
else {
return false;
}
}
MatchingContext matchingContext = new MatchingContext(path,false);
MatchingContext matchingContext = new MatchingContext(path, false);
return head.matches(0, matchingContext);
}
/**
* @param path the path to check against the pattern
* @return true if the pattern matches as much of the path as is supplied
*/
public boolean matchStart(String path) {
if (head == null) {
return (path==null || path.length() == 0);
} else if (path == null || path.length() == 0) {
return (path == null || path.length() == 0);
}
else if (path == null || path.length() == 0) {
return true;
}
MatchingContext matchingContext = new MatchingContext(path,false);
MatchingContext matchingContext = new MatchingContext(path, false);
matchingContext.setMatchStartMatching(true);
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.
*/
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)) {
return matchingContext.getExtractedVariables();
} else {
if (path== null || path.length()==0) {
}
else {
if (path == null || path.length() == 0) {
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() {
return patternString;
}
public PathElement getHeadSection() {
return head;
}
@@ -168,17 +206,18 @@ public class PathPattern implements Comparable<PathPattern> {
* the returned path.
* @param path a path that matches this pattern
* @return the subset of the path that is matched by pattern or "" if none of it is matched by pattern elements
*/
*/
public String extractPathWithinPattern(String path) {
// assert this.matches(path)
PathElement s = head;
int separatorCount = 0;
// Find first path element that is pattern based
while (s != null) {
if (s instanceof SeparatorPathElement || s instanceof CaptureTheRestPathElement || s instanceof WildcardTheRestPathElement) {
if (s instanceof SeparatorPathElement || s instanceof CaptureTheRestPathElement
|| s instanceof WildcardTheRestPathElement) {
separatorCount++;
}
if (s.getWildcardCount()!=0 || s.getCaptureCount()!=0) {
if (s.getWildcardCount() != 0 || s.getCaptureCount() != 0) {
break;
}
s = s.next;
@@ -201,7 +240,7 @@ public class PathPattern implements Comparable<PathPattern> {
}
int end = len;
// Trim trailing separators
while (path.charAt(end-1) == separator) {
while (path.charAt(end - 1) == separator) {
end--;
}
// 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
int c = pos;
StringBuilder stringWithDuplicateSeparatorsRemoved = null;
while (c<end) {
while (c < end) {
char ch = path.charAt(c);
if (ch == separator) {
if ((c+1)<end && path.charAt(c+1)==separator) {
if ((c + 1) < end && path.charAt(c + 1) == separator) {
// multiple separators
if (stringWithDuplicateSeparatorsRemoved == null) {
// first time seen, need to capture all data up to this point
stringWithDuplicateSeparatorsRemoved = new StringBuilder();
stringWithDuplicateSeparatorsRemoved.append(path.substring(pos,c));
stringWithDuplicateSeparatorsRemoved.append(path.substring(pos, c));
}
do {
c++;
} while ((c+1)<end && path.charAt(c+1)==separator);
c++;
} while ((c + 1) < end && path.charAt(c + 1) == separator);
}
}
if (stringWithDuplicateSeparatorsRemoved != null) {
@@ -232,9 +271,9 @@ public class PathPattern implements Comparable<PathPattern> {
if (stringWithDuplicateSeparatorsRemoved != null) {
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
* 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) {
return (lenDifference < 0) ? +1 : -1;
}
} else {
}
else {
return +1;
}
} else if (p.isCatchAll()) {
}
else if (p.isCatchAll()) {
return -1;
}
// 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() {
StringBuilder buf = new StringBuilder();
PathElement pe = head;
while (pe!=null) {
while (pe != null) {
buf.append(pe.toString()).append(" ");
pe = pe.next;
}
@@ -320,7 +361,7 @@ public class PathPattern implements Comparable<PathPattern> {
public int getCapturedVariableCount() {
return capturedVariableCount;
}
public String toString() {
return patternString;
}
@@ -340,7 +381,7 @@ public class PathPattern implements Comparable<PathPattern> {
boolean isMatchStartMatching = false;
private Map<String,String> extractedVariables;
private Map<String, String> extractedVariables;
public boolean extractingVariables;
@@ -361,10 +402,11 @@ public class PathPattern implements Comparable<PathPattern> {
extractedVariables.put(key, value);
}
public Map<String,String> getExtractedVariables() {
public Map<String, String> getExtractedVariables() {
if (this.extractedVariables == null) {
return NO_VARIABLES_MAP;
} else {
}
else {
return this.extractedVariables;
}
}
@@ -372,7 +414,7 @@ public class PathPattern implements Comparable<PathPattern> {
/**
* Scan ahead from the specified position for either the next separator
* character or the end of the candidate.
*
*
* @param pos the starting position for the scan
* @return the position of the next separator or the end of the candidate
*/
@@ -392,62 +434,65 @@ public class PathPattern implements Comparable<PathPattern> {
*/
public String combine(String pattern2string) {
// If one of them is empty the result is the other. If both empty the result is ""
if (patternString == null || patternString.length()==0) {
if (pattern2string == null || pattern2string.length()==0) {
if (patternString == null || patternString.length() == 0) {
if (pattern2string == null || pattern2string.length() == 0) {
return "";
} else {
}
else {
return pattern2string;
}
} else if (pattern2string == null || pattern2string.length()==0) {
}
else if (pattern2string == null || pattern2string.length() == 0) {
return patternString;
}
// /* + /hotel => /hotel
// /*.* + /*.html => /*.html
// However:
// /usr + /user => /usr/user
// /{foo} + /bar => /{foo}/bar
if (!patternString.equals(pattern2string) && capturedVariableCount==0 && matches(pattern2string)) {
if (!patternString.equals(pattern2string) && capturedVariableCount == 0 && matches(pattern2string)) {
return pattern2string;
}
// /hotels/* + /booking => /hotels/booking
// /hotels/* + booking => /hotels/booking
if (endsWithSeparatorWildcard) {
return concat(patternString.substring(0,patternString.length()-2), pattern2string);
return concat(patternString.substring(0, patternString.length() - 2), pattern2string);
}
// /hotels + /booking => /hotels/booking
// /hotels + booking => /hotels/booking
int starDotPos1 = patternString.indexOf("*."); // Are there any file prefix/suffix things to consider?
if (capturedVariableCount!=0 || starDotPos1 == -1 || separator=='.') {
return concat(patternString, pattern2string);
if (capturedVariableCount != 0 || starDotPos1 == -1 || separator == '.') {
return concat(patternString, pattern2string);
}
// /*.html + /hotel => /hotel.html
// /*.html + /hotel.* => /hotel.html
String firstExtension = patternString.substring(starDotPos1+1); // looking for the first extension
String firstExtension = patternString.substring(starDotPos1 + 1); // looking for the first extension
int dotPos2 = pattern2string.indexOf('.');
String file2 = (dotPos2==-1?pattern2string:pattern2string.substring(0,dotPos2));
String secondExtension = (dotPos2 == -1?"":pattern2string.substring(dotPos2));
String file2 = (dotPos2 == -1 ? pattern2string : pattern2string.substring(0, dotPos2));
String secondExtension = (dotPos2 == -1 ? "" : pattern2string.substring(dotPos2));
boolean firstExtensionWild = (firstExtension.equals(".*") || firstExtension.equals(""));
boolean secondExtensionWild = (secondExtension.equals(".*") || secondExtension.equals(""));
if (!firstExtensionWild && !secondExtensionWild) {
throw new IllegalArgumentException("Cannot combine patterns: " + patternString + " and " + pattern2string);
}
return file2 + (firstExtensionWild?secondExtension:firstExtension);
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).
* @param path1 First path
* @param path2 Second path
* @return joined path that may include separator if necessary
*/
private String concat(String path1, String path2) {
boolean path1EndsWithSeparator = path1.charAt(path1.length()-1)==separator;
boolean path2StartsWithSeparator = path2.charAt(0)==separator;
boolean path1EndsWithSeparator = path1.charAt(path1.length() - 1) == separator;
boolean path2StartsWithSeparator = path2.charAt(0) == separator;
if (path1EndsWithSeparator && path2StartsWithSeparator) {
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");
* 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
* limitations under the License.
*/
package org.springframework.web.util.patterns;
import java.util.Comparator;
/**
* Basic PathPattern comparator.
*
*
* @author Andy Clement
*/
public class PathPatternComparator implements Comparator<PathPattern> {
@@ -28,8 +29,9 @@ public class PathPatternComparator implements Comparator<PathPattern> {
public int compare(PathPattern o1, PathPattern o2) {
// Nulls get sorted to the end
if (o1 == null) {
return (o2==null?0:+1);
} else if (o2 == null) {
return (o2 == null ? 0 : +1);
}
else if (o2 == null) {
return -1;
}
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");
* 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
* limitations under the License.
*/
package org.springframework.web.util.patterns;
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
* path elements in a linked list.
*
* {@link PathElement}s in a linked list.
*
* @author Andy Clement
* @since 5.0
*/
public class PathPatternParser {
@@ -85,7 +87,7 @@ public class PathPatternParser {
/**
* Create a PatternParser that will use the specified separator instead of
* the default.
*
*
* @param separator the path separator to look for when parsing.
*/
public PathPatternParser(char separator) {
@@ -101,7 +103,7 @@ public class PathPatternParser {
* path elements around separator boundaries and verifying the structure at each
* stage. Produces a PathPattern object that can be used for fast matching
* against paths.
*
*
* @param pathPattern the input path pattern, e.g. /foo/{bar}
* @return a PathPattern for quickly matching paths against the specified path pattern
*/
@@ -128,32 +130,37 @@ public class PathPatternParser {
pushPathElement(createPathElement());
}
// Skip over multiple separators
while ((pos+1) < pathPatternLength && pathPatternData[pos+1] == separator) {
while ((pos + 1) < pathPatternLength && pathPatternData[pos + 1] == separator) {
pos++;
}
if (peekDoubleWildcard()) {
pushPathElement(new WildcardTheRestPathElement(pos,separator));
pos+=2;
} else {
pushPathElement(new WildcardTheRestPathElement(pos, separator));
pos += 2;
}
else {
pushPathElement(new SeparatorPathElement(pos, separator));
}
} else {
}
else {
if (pathElementStart == -1) {
pathElementStart = pos;
}
if (ch == '?') {
singleCharWildcardCount++;
} else if (ch == '{') {
}
else if (ch == '{') {
if (insideVariableCapture) {
throw new PatternParseException(pos, pathPatternData, PatternMessage.ILLEGAL_NESTED_CAPTURE);
// If we enforced that adjacent captures weren't allowed, this would do it (this would be an error: /foo/{bar}{boo}/)
// If we enforced that adjacent captures weren't allowed,
// // this would do it (this would be an error: /foo/{bar}{boo}/)
// } else if (pos > 0 && pathPatternData[pos - 1] == '}') {
// throw new PatternParseException(pos, pathPatternData,
// PatternMessage.CANNOT_HAVE_ADJACENT_CAPTURES);
}
insideVariableCapture = true;
variableCaptureStart = pos;
} else if (ch == '}') {
}
else if (ch == '}') {
if (!insideVariableCapture) {
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);
}
variableCaptureCount++;
} else if (ch == ':') {
}
else if (ch == ':') {
if (insideVariableCapture) {
skipCaptureRegex();
insideVariableCapture = false;
variableCaptureCount++;
}
} else if (ch == '*') {
}
else if (ch == '*') {
if (insideVariableCapture) {
if (variableCaptureStart == pos - 1) {
isCaptureTheRestVariable = true;
@@ -185,7 +194,8 @@ public class PathPatternParser {
PatternMessage.ILLEGAL_CHARACTER_AT_START_OF_CAPTURE_DESCRIPTOR,
Character.toString(ch));
} else if ((pos > (variableCaptureStart + 1 + (isCaptureTheRestVariable ? 1 : 0))
}
else if ((pos > (variableCaptureStart + 1 + (isCaptureTheRestVariable ? 1 : 0))
&& !Character.isJavaIdentifierPart(ch))) {
throw new PatternParseException(pos, pathPatternData,
PatternMessage.ILLEGAL_CHARACTER_IN_CAPTURE_DESCRIPTOR, Character.toString(ch));
@@ -219,10 +229,11 @@ public class PathPatternParser {
pos++;
previousBackslash = true;
continue;
}
}
if (ch == '{' && !previousBackslash) {
curlyBracketDepth++;
} else if (ch == '}' && !previousBackslash) {
}
else if (ch == '}' && !previousBackslash) {
if (curlyBracketDepth == 0) {
if (regexStart == pos) {
throw new PatternParseException(regexStart, pathPatternData,
@@ -236,7 +247,7 @@ public class PathPatternParser {
throw new PatternParseException(pos, pathPatternData, PatternMessage.MISSING_CLOSE_CAPTURE);
}
pos++;
previousBackslash=false;
previousBackslash = false;
}
throw new PatternParseException(pos - 1, pathPatternData, PatternMessage.MISSING_CLOSE_CAPTURE);
}
@@ -265,25 +276,30 @@ public class PathPatternParser {
if (currentPE == null) {
headPE = newPathElement;
currentPE = newPathElement;
} else if (currentPE instanceof SeparatorPathElement) {
}
else if (currentPE instanceof SeparatorPathElement) {
PathElement peBeforeSeparator = currentPE.prev;
if (peBeforeSeparator == null) {
// /{*foobar} is at the start
headPE = newPathElement;
newPathElement.prev = peBeforeSeparator;
} else {
}
else {
peBeforeSeparator.next = newPathElement;
newPathElement.prev = peBeforeSeparator;
}
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) {
headPE = newPathElement;
currentPE = newPathElement;
} else {
}
else {
currentPE.next = newPathElement;
newPathElement.prev = currentPE;
currentPE = newPathElement;
@@ -305,39 +321,51 @@ public class PathPatternParser {
System.arraycopy(pathPatternData, pathElementStart, pathElementText, 0, pos - pathElementStart);
PathElement newPE = null;
if (variableCaptureCount > 0) {
if (variableCaptureCount == 1 && pathElementStart == variableCaptureStart && pathPatternData[pos - 1] == '}') {
if (variableCaptureCount == 1
&& pathElementStart == variableCaptureStart && pathPatternData[pos - 1] == '}') {
if (isCaptureTheRestVariable) {
// It is {*....}
newPE = new CaptureTheRestPathElement(pathElementStart, pathElementText, separator);
} else {
}
else {
// It is a full capture of this element (possibly with constraint), for example: /foo/{abc}/
try {
newPE = new CaptureVariablePathElement(pathElementStart, pathElementText, caseSensitive);
} catch (PatternSyntaxException pse) {
throw new PatternParseException(pse, findRegexStart(pathPatternData,pathElementStart)+pse.getIndex(), pathPatternData, PatternMessage.JDK_PATTERN_SYNTAX_EXCEPTION);
}
catch (PatternSyntaxException pse) {
throw new PatternParseException(pse, findRegexStart(pathPatternData, pathElementStart)
+ pse.getIndex(), pathPatternData, PatternMessage.JDK_PATTERN_SYNTAX_EXCEPTION);
}
recordCapturedVariable(pathElementStart, ((CaptureVariablePathElement) newPE).getVariableName());
}
} else {
}
else {
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()) {
recordCapturedVariable(pathElementStart, variableName);
}
newPE = newRegexSection;
}
} else {
}
else {
if (wildcard) {
if (pos - 1 == pathElementStart) {
newPE = new WildcardPathElement(pathElementStart);
} else {
}
else {
newPE = new RegexPathElement(pathElementStart, pathElementText, caseSensitive, pathPatternData);
}
} else if (singleCharWildcardCount!=0) {
newPE = new SingleCharWildcardedPathElement(pathElementStart, pathElementText, singleCharWildcardCount, caseSensitive);
} else {
}
else if (singleCharWildcardCount != 0) {
newPE = new SingleCharWildcardedPathElement(pathElementStart, pathElementText,
singleCharWildcardCount, caseSensitive);
}
else {
newPE = new LiteralPathElement(pathElementStart, pathElementText, caseSensitive);
}
}
@@ -349,11 +377,12 @@ public class PathPatternParser {
* Assumes there is a constraint pattern.
* @param data a complete path expression, e.g. /aaa/bbb/{ccc:...}
* @param offset the start of the capture pattern of interest
* @return the index of the character after the ':' within the pattern expression relative to the start of the whole expression
* @return the index of the character after the ':' within
* the pattern expression relative to the start of the whole expression
*/
private int findRegexStart(char[] data, int offset) {
int pos = offset;
while (pos<data.length) {
while (pos < data.length) {
if (data[pos] == ':') {
return pos + 1;
}
@@ -383,7 +412,8 @@ public class PathPatternParser {
capturedVariableNames = new ArrayList<>();
}
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);
}

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");
* 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
* limitations under the License.
*/
package org.springframework.web.util.patterns;
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
* sorts anything that exactly matches it to be first.
*
*
* @author Andy Clement
* @since 5.0
*/
public class PatternComparatorConsideringPath implements Comparator<PathPattern> {
private String path;
public PatternComparatorConsideringPath(String path) {
this.path = path;
}
@@ -35,13 +37,15 @@ public class PatternComparatorConsideringPath implements Comparator<PathPattern>
public int compare(PathPattern o1, PathPattern o2) {
// Nulls get sorted to the end
if (o1 == null) {
return (o2==null?0:+1);
} else if (o2 == null) {
return (o2 == null ? 0 : +1);
}
else if (o2 == null) {
return -1;
}
if (o1.getPatternString().equals(path)) {
return (o2.getPatternString().equals(path))?0:-1;
} else if (o2.getPatternString().equals(path)) {
return (o2.getPatternString().equals(path)) ? 0 : -1;
}
else if (o2.getPatternString().equals(path)) {
return +1;
}
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");
* 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
* limitations under the License.
*/
package org.springframework.web.util.patterns;
import java.text.MessageFormat;
/**
* The messages that can be included in a {@link PatternParseException} when there is a parse failure.
*
*
* @author Andy Clement
* @since 5.0
*/
public enum PatternMessage {
// @formatter:off
MISSING_CLOSE_CAPTURE("Expected close capture character after variable name '}'"),
MISSING_OPEN_CAPTURE("Missing preceeding open capture character before variable name'{'"),
MISSING_OPEN_CAPTURE("Missing preceeding open capture character before variable name'{'"),
ILLEGAL_NESTED_CAPTURE("Not allowed to nest variable captures"),
CANNOT_HAVE_ADJACENT_CAPTURES("Adjacent captures are not allowed"),
ILLEGAL_CHARACTER_AT_START_OF_CAPTURE_DESCRIPTOR("Character ''{0}'' is not allowed at start of captured variable name"),

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");
* 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
* limitations under the License.
*/
package org.springframework.web.util.patterns;
/**
* Exception that is thrown when there is a problem with the pattern being parsed.
*
*
* @author Andy Clement
* @since 5.0
*/
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) {
super(message.formatMessage(inserts),cause);
super(message.formatMessage(inserts), cause);
this.pos = pos;
this.patternText = patternText;
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");
* 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
* limitations under the License.
*/
package org.springframework.web.util.patterns;
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.
* For example in '<tt>/foo/&ast;_&ast;/&ast;_{foobar}</tt>' both <tt>*_*</tt> and <tt>*_{foobar}</tt>
* are {@link RegexPathElement} path elements. Derived from the general {@link AntPathMatcher} approach.
*
*
* @author Andy Clement
* @since 5.0
*/
class RegexPathElement extends PathElement {
@@ -63,10 +65,12 @@ class RegexPathElement extends PathElement {
String match = matcher.group();
if ("?".equals(match)) {
patternBuilder.append('.');
} else if ("*".equals(match)) {
}
else if ("*".equals(match)) {
patternBuilder.append(".*");
wildcardCount++;
} else if (match.startsWith("{") && match.endsWith("}")) {
}
else if (match.startsWith("{") && match.endsWith("}")) {
int colonIdx = match.indexOf(':');
if (colonIdx == -1) {
patternBuilder.append(DEFAULT_VARIABLE_PATTERN);
@@ -76,7 +80,8 @@ class RegexPathElement extends PathElement {
variableName);
}
this.variableNames.add(variableName);
} else {
}
else {
String variablePattern = match.substring(colonIdx + 1, match.length() - 1);
patternBuilder.append('(');
patternBuilder.append(variablePattern);
@@ -94,7 +99,8 @@ class RegexPathElement extends PathElement {
patternBuilder.append(quote(text, end, text.length()));
if (caseSensitive) {
pattern = java.util.regex.Pattern.compile(patternBuilder.toString());
} else {
}
else {
pattern = java.util.regex.Pattern.compile(patternBuilder.toString(),
java.util.regex.Pattern.CASE_INSENSITIVE);
}
@@ -120,7 +126,8 @@ class RegexPathElement extends PathElement {
if (next == null) {
// No more pattern, is there more data?
matches = (p == matchingContext.candidateLength);
} else {
}
else {
if (matchingContext.isMatchStartMatching && p == matchingContext.candidateLength) {
return true; // no more data but matches up to this point
}
@@ -165,10 +172,10 @@ class RegexPathElement extends PathElement {
public int getWildcardCount() {
return wildcardCount;
}
@Override
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");
* 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
* limitations under the License.
*/
package org.springframework.web.util.patterns;
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
* of '/' will be represented by a SeparatorPathElement (if the default
* separator of '/' is being used).
*
*
* @author Andy Clement
* @since 5.0
*/
class SeparatorPathElement extends PathElement {
@@ -44,13 +46,14 @@ class SeparatorPathElement extends PathElement {
if (matchingContext.candidate[candidateIndex] == separator) {
// Skip further separators in the path (they are all 'matched'
// by a single SeparatorPathElement)
while ((candidateIndex+1)<matchingContext.candidateLength &&
matchingContext.candidate[candidateIndex+1] == separator) {
while ((candidateIndex + 1) < matchingContext.candidateLength &&
matchingContext.candidate[candidateIndex + 1] == separator) {
candidateIndex++;
}
if (next == null) {
matched = ((candidateIndex + 1) == matchingContext.candidateLength);
} else {
}
else {
candidateIndex++;
if (matchingContext.isMatchStartMatching && candidateIndex == matchingContext.candidateLength) {
return true; // no more data but matches up to this point
@@ -61,7 +64,7 @@ class SeparatorPathElement extends PathElement {
}
return matched;
}
public String toString() {
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");
* 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
* limitations under the License.
*/
package org.springframework.web.util.patterns;
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
* or more times (to basically many any character at that position).
*
*
* @author Andy Clement
* @since 5.0
*/
class SingleCharWildcardedPathElement extends PathElement {
private char[] text;
private int len;
private int questionMarkCount;
private boolean caseSensitive;
public SingleCharWildcardedPathElement(int pos, char[] literalText, int questionMarkCount, boolean caseSensitive) {
@@ -40,14 +42,15 @@ class SingleCharWildcardedPathElement extends PathElement {
this.caseSensitive = caseSensitive;
if (caseSensitive) {
this.text = literalText;
} else {
}
else {
this.text = new char[literalText.length];
for (int i = 0; i < len; i++) {
this.text[i] = Character.toLowerCase(literalText[i]);
}
}
}
@Override
public boolean matches(int candidateIndex, MatchingContext matchingContext) {
if (matchingContext.candidateLength < (candidateIndex + len)) {
@@ -62,7 +65,8 @@ class SingleCharWildcardedPathElement extends PathElement {
}
candidateIndex++;
}
} else {
}
else {
for (int i = 0; i < len; i++) {
char t = text[i];
if (t != '?' && Character.toLowerCase(candidate[candidateIndex]) != t) {
@@ -73,14 +77,15 @@ class SingleCharWildcardedPathElement extends PathElement {
}
if (next == null) {
return candidateIndex == matchingContext.candidateLength;
} else {
}
else {
if (matchingContext.isMatchStartMatching && candidateIndex == matchingContext.candidateLength) {
return true; // no more data but matches up to this point
}
return next.matches(candidateIndex, matchingContext);
}
}
@Override
public int getWildcardCount() {
return questionMarkCount;

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");
* 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
* limitations under the License.
*/
package org.springframework.web.util.patterns;
/**
* Used to represent a subsection of an array, useful when wanting to pass that subset of data
* to another method (e.g. a java regex matcher) but not wanting to create a new string object to hold
* all that data.
*
*
* @author Andy Clement
* @since 5.0
*/
class SubSequence implements CharSequence {
private char[] chars;
private int start, end;
SubSequence(char[] chars, int start, int end) {
@@ -47,9 +50,9 @@ class SubSequence implements CharSequence {
public CharSequence subSequence(int start, int end) {
return new SubSequence(chars, this.start + start, this.start + end);
}
public String toString() {
return new String(chars,start,end-start);
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");
* 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
* limitations under the License.
*/
package org.springframework.web.util.patterns;
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
* represented by a WildcardPathElement.
*
*
* @author Andy Clement
* @since 5.0
*/
class WildcardPathElement extends PathElement {
@@ -39,7 +41,8 @@ class WildcardPathElement extends PathElement {
int nextPos = matchingContext.scanAhead(candidateIndex);
if (next == null) {
return (nextPos == matchingContext.candidateLength);
} else {
}
else {
if (matchingContext.isMatchStartMatching && nextPos == matchingContext.candidateLength) {
return true; // no more data but matches up to this point
}
@@ -60,7 +63,7 @@ class WildcardPathElement extends PathElement {
public int getWildcardCount() {
return 1;
}
@Override
public int getScore() {
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");
* 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
* limitations under the License.
*/
package org.springframework.web.util.patterns;
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
* '/foo/**' the /** is represented as a {@link WildcardTheRestPathElement}.
*
*
* @author Andy Clement
* @since 5.0
*/
class WildcardTheRestPathElement extends PathElement {
private char separator;
WildcardTheRestPathElement(int pos, char separator) {
super(pos);
this.separator = separator;
@@ -36,14 +38,14 @@ class WildcardTheRestPathElement extends PathElement {
public boolean matches(int candidateIndex, MatchingContext matchingContext) {
// If there is more data, it must start with the separator
if (candidateIndex < matchingContext.candidateLength &&
matchingContext.candidate[candidateIndex] != separator) {
matchingContext.candidate[candidateIndex] != separator) {
return false;
}
return true;
}
public String toString() {
return "WildcardTheRest("+separator+"**)";
return "WildcardTheRest(" + separator + "**)";
}
@Override