Make PathPatternParser multi-threaded
With this change the original PathPatternParser is renamed InternalPathPatternParser and a new PathPatternParser class is added. This new PathPatternParser class is a very simple thread-safe wrapper for the InternalPathPatternParser. It achieves this by creating a new InternalPathPatternParser for each new parse request. This follows the model used for SpEL parsing.
This commit is contained in:
parent
8450c69497
commit
a0505bf1e7
|
@ -0,0 +1,408 @@
|
|||
/*
|
||||
* 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.util.patterns;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.PatternSyntaxException;
|
||||
|
||||
/**
|
||||
* Parser for URI template patterns. It breaks the path pattern into a number of
|
||||
* {@link PathElement}s in a linked list. Instances are reusable but are not thread-safe.
|
||||
*
|
||||
* @author Andy Clement
|
||||
* @since 5.0
|
||||
*/
|
||||
public class InternalPathPatternParser {
|
||||
|
||||
// The expected path separator to split path elements during parsing
|
||||
char separator = PathPatternParser.DEFAULT_SEPARATOR;
|
||||
|
||||
// Is the parser producing case sensitive PathPattern matchers
|
||||
boolean caseSensitive = true;
|
||||
|
||||
// The input data for parsing
|
||||
private char[] pathPatternData;
|
||||
|
||||
// The length of the input data
|
||||
private int pathPatternLength;
|
||||
|
||||
// Current parsing position
|
||||
int pos;
|
||||
|
||||
// How many ? characters in a particular path element
|
||||
private int singleCharWildcardCount;
|
||||
|
||||
// Is the path pattern using * characters in a particular path element
|
||||
private boolean wildcard = false;
|
||||
|
||||
// Is the construct {*...} being used in a particular path element
|
||||
private boolean isCaptureTheRestVariable = false;
|
||||
|
||||
// Has the parser entered a {...} variable capture block in a particular
|
||||
// path element
|
||||
private boolean insideVariableCapture = false;
|
||||
|
||||
// How many variable captures are occurring in a particular path element
|
||||
private int variableCaptureCount = 0;
|
||||
|
||||
// Start of the most recent path element in a particular path element
|
||||
int pathElementStart;
|
||||
|
||||
// Start of the most recent variable capture in a particular path element
|
||||
int variableCaptureStart;
|
||||
|
||||
// Variables captures in this path pattern
|
||||
List<String> capturedVariableNames;
|
||||
|
||||
// The head of the path element chain currently being built
|
||||
PathElement headPE;
|
||||
|
||||
// The most recently constructed path element in the chain
|
||||
PathElement currentPE;
|
||||
|
||||
/**
|
||||
* Create a PatternParser that will use the specified separator instead of
|
||||
* the default.
|
||||
*
|
||||
* @param separator the path separator to look for when parsing.
|
||||
*/
|
||||
public InternalPathPatternParser(char separator, boolean caseSensitive) {
|
||||
this.separator = separator;
|
||||
this.caseSensitive = caseSensitive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the path pattern data, a character at a time, breaking it into
|
||||
* path elements around separator boundaries and verifying the structure at each
|
||||
* stage. Produces a PathPattern object that can be used for fast matching
|
||||
* against paths.
|
||||
*
|
||||
* @param pathPattern the input path pattern, e.g. /foo/{bar}
|
||||
* @return a PathPattern for quickly matching paths against the specified path pattern
|
||||
*/
|
||||
public PathPattern parse(String pathPattern) {
|
||||
if (pathPattern == null) {
|
||||
pathPattern = "";
|
||||
}
|
||||
// int starstar = pathPattern.indexOf("**");
|
||||
// if (starstar!=-1 && starstar!=pathPattern.length()-2) {
|
||||
// throw new IllegalStateException("Not allowed ** unless at end of pattern: "+pathPattern);
|
||||
// }
|
||||
pathPatternData = pathPattern.toCharArray();
|
||||
pathPatternLength = pathPatternData.length;
|
||||
headPE = null;
|
||||
currentPE = null;
|
||||
capturedVariableNames = null;
|
||||
pathElementStart = -1;
|
||||
pos = 0;
|
||||
resetPathElementState();
|
||||
while (pos < pathPatternLength) {
|
||||
char ch = pathPatternData[pos];
|
||||
if (ch == separator) {
|
||||
if (pathElementStart != -1) {
|
||||
pushPathElement(createPathElement());
|
||||
}
|
||||
// Skip over multiple separators
|
||||
while ((pos + 1) < pathPatternLength && pathPatternData[pos + 1] == separator) {
|
||||
pos++;
|
||||
}
|
||||
if (peekDoubleWildcard()) {
|
||||
pushPathElement(new WildcardTheRestPathElement(pos, separator));
|
||||
pos += 2;
|
||||
}
|
||||
else {
|
||||
pushPathElement(new SeparatorPathElement(pos, separator));
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (pathElementStart == -1) {
|
||||
pathElementStart = pos;
|
||||
}
|
||||
if (ch == '?') {
|
||||
singleCharWildcardCount++;
|
||||
}
|
||||
else if (ch == '{') {
|
||||
if (insideVariableCapture) {
|
||||
throw new PatternParseException(pos, pathPatternData, PatternMessage.ILLEGAL_NESTED_CAPTURE);
|
||||
// If we enforced that adjacent captures weren't allowed,
|
||||
// // this would do it (this would be an error: /foo/{bar}{boo}/)
|
||||
// } else if (pos > 0 && pathPatternData[pos - 1] == '}') {
|
||||
// throw new PatternParseException(pos, pathPatternData,
|
||||
// PatternMessage.CANNOT_HAVE_ADJACENT_CAPTURES);
|
||||
}
|
||||
insideVariableCapture = true;
|
||||
variableCaptureStart = pos;
|
||||
}
|
||||
else if (ch == '}') {
|
||||
if (!insideVariableCapture) {
|
||||
throw new PatternParseException(pos, pathPatternData, PatternMessage.MISSING_OPEN_CAPTURE);
|
||||
}
|
||||
insideVariableCapture = false;
|
||||
if (isCaptureTheRestVariable && (pos + 1) < pathPatternLength) {
|
||||
throw new PatternParseException(pos + 1, pathPatternData,
|
||||
PatternMessage.NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST);
|
||||
}
|
||||
variableCaptureCount++;
|
||||
}
|
||||
else if (ch == ':') {
|
||||
if (insideVariableCapture) {
|
||||
skipCaptureRegex();
|
||||
insideVariableCapture = false;
|
||||
variableCaptureCount++;
|
||||
}
|
||||
}
|
||||
else if (ch == '*') {
|
||||
if (insideVariableCapture) {
|
||||
if (variableCaptureStart == pos - 1) {
|
||||
isCaptureTheRestVariable = true;
|
||||
}
|
||||
}
|
||||
wildcard = true;
|
||||
}
|
||||
// Check that the characters used for captured variable names are like java identifiers
|
||||
if (insideVariableCapture) {
|
||||
if ((variableCaptureStart + 1 + (isCaptureTheRestVariable ? 1 : 0)) == pos
|
||||
&& !Character.isJavaIdentifierStart(ch)) {
|
||||
throw new PatternParseException(pos, pathPatternData,
|
||||
PatternMessage.ILLEGAL_CHARACTER_AT_START_OF_CAPTURE_DESCRIPTOR,
|
||||
Character.toString(ch));
|
||||
|
||||
}
|
||||
else if ((pos > (variableCaptureStart + 1 + (isCaptureTheRestVariable ? 1 : 0))
|
||||
&& !Character.isJavaIdentifierPart(ch))) {
|
||||
throw new PatternParseException(pos, pathPatternData,
|
||||
PatternMessage.ILLEGAL_CHARACTER_IN_CAPTURE_DESCRIPTOR, Character.toString(ch));
|
||||
}
|
||||
}
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
if (pathElementStart != -1) {
|
||||
pushPathElement(createPathElement());
|
||||
}
|
||||
return new PathPattern(pathPattern, headPE, separator, caseSensitive);
|
||||
}
|
||||
|
||||
/**
|
||||
* Just hit a ':' and want to jump over the regex specification for this
|
||||
* variable. pos will be pointing at the ':', we want to skip until the }.
|
||||
* <p>
|
||||
* Nested {...} pairs don't have to be escaped: <tt>/abc/{var:x{1,2}}/def</tt>
|
||||
* <p>An escaped } will not be treated as the end of the regex: <tt>/abc/{var:x\\{y:}/def</tt>
|
||||
* <p>A separator that should not indicate the end of the regex can be escaped:
|
||||
*/
|
||||
private void skipCaptureRegex() {
|
||||
pos++;
|
||||
int regexStart = pos;
|
||||
int curlyBracketDepth = 0; // how deep in nested {...} pairs
|
||||
boolean previousBackslash = false;
|
||||
while (pos < pathPatternLength) {
|
||||
char ch = pathPatternData[pos];
|
||||
if (ch == '\\' && !previousBackslash) {
|
||||
pos++;
|
||||
previousBackslash = true;
|
||||
continue;
|
||||
}
|
||||
if (ch == '{' && !previousBackslash) {
|
||||
curlyBracketDepth++;
|
||||
}
|
||||
else if (ch == '}' && !previousBackslash) {
|
||||
if (curlyBracketDepth == 0) {
|
||||
if (regexStart == pos) {
|
||||
throw new PatternParseException(regexStart, pathPatternData,
|
||||
PatternMessage.MISSING_REGEX_CONSTRAINT);
|
||||
}
|
||||
return;
|
||||
}
|
||||
curlyBracketDepth--;
|
||||
}
|
||||
if (ch == separator && !previousBackslash) {
|
||||
throw new PatternParseException(pos, pathPatternData, PatternMessage.MISSING_CLOSE_CAPTURE);
|
||||
}
|
||||
pos++;
|
||||
previousBackslash = false;
|
||||
}
|
||||
throw new PatternParseException(pos - 1, pathPatternData, PatternMessage.MISSING_CLOSE_CAPTURE);
|
||||
}
|
||||
|
||||
/**
|
||||
* After processing a separator, a quick peek whether it is followed by **
|
||||
* (and only ** before the end of the pattern or the next separator)
|
||||
*/
|
||||
private boolean peekDoubleWildcard() {
|
||||
if ((pos + 2) >= pathPatternLength) {
|
||||
return false;
|
||||
}
|
||||
if (pathPatternData[pos + 1] != '*' || pathPatternData[pos + 2] != '*') {
|
||||
return false;
|
||||
}
|
||||
return (pos + 3 == pathPatternLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param newPathElement the new path element to add to the chain being built
|
||||
*/
|
||||
private void pushPathElement(PathElement newPathElement) {
|
||||
if (newPathElement instanceof CaptureTheRestPathElement) {
|
||||
// There must be a separator ahead of this thing
|
||||
// currentPE SHOULD be a SeparatorPathElement
|
||||
if (currentPE == null) {
|
||||
headPE = newPathElement;
|
||||
currentPE = newPathElement;
|
||||
}
|
||||
else if (currentPE instanceof SeparatorPathElement) {
|
||||
PathElement peBeforeSeparator = currentPE.prev;
|
||||
if (peBeforeSeparator == null) {
|
||||
// /{*foobar} is at the start
|
||||
headPE = newPathElement;
|
||||
newPathElement.prev = peBeforeSeparator;
|
||||
}
|
||||
else {
|
||||
peBeforeSeparator.next = newPathElement;
|
||||
newPathElement.prev = peBeforeSeparator;
|
||||
}
|
||||
currentPE = newPathElement;
|
||||
}
|
||||
else {
|
||||
throw new IllegalStateException("Expected SeparatorPathElement but was " + currentPE);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (headPE == null) {
|
||||
headPE = newPathElement;
|
||||
currentPE = newPathElement;
|
||||
}
|
||||
else {
|
||||
currentPE.next = newPathElement;
|
||||
newPathElement.prev = currentPE;
|
||||
currentPE = newPathElement;
|
||||
}
|
||||
}
|
||||
resetPathElementState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Used the knowledge built up whilst processing since the last path element to determine what kind of path
|
||||
* element to create.
|
||||
* @return the new path element
|
||||
*/
|
||||
private PathElement createPathElement() {
|
||||
if (insideVariableCapture) {
|
||||
throw new PatternParseException(pos, pathPatternData, PatternMessage.MISSING_CLOSE_CAPTURE);
|
||||
}
|
||||
char[] pathElementText = new char[pos - pathElementStart];
|
||||
System.arraycopy(pathPatternData, pathElementStart, pathElementText, 0, pos - pathElementStart);
|
||||
PathElement newPE = null;
|
||||
if (variableCaptureCount > 0) {
|
||||
if (variableCaptureCount == 1
|
||||
&& pathElementStart == variableCaptureStart && pathPatternData[pos - 1] == '}') {
|
||||
if (isCaptureTheRestVariable) {
|
||||
// It is {*....}
|
||||
newPE = new CaptureTheRestPathElement(pathElementStart, pathElementText, separator);
|
||||
}
|
||||
else {
|
||||
// It is a full capture of this element (possibly with constraint), for example: /foo/{abc}/
|
||||
try {
|
||||
newPE = new CaptureVariablePathElement(pathElementStart, pathElementText, caseSensitive);
|
||||
}
|
||||
catch (PatternSyntaxException pse) {
|
||||
throw new PatternParseException(pse, findRegexStart(pathPatternData, pathElementStart)
|
||||
+ pse.getIndex(), pathPatternData, PatternMessage.JDK_PATTERN_SYNTAX_EXCEPTION);
|
||||
}
|
||||
recordCapturedVariable(pathElementStart, ((CaptureVariablePathElement) newPE).getVariableName());
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (isCaptureTheRestVariable) {
|
||||
throw new PatternParseException(pathElementStart, pathPatternData,
|
||||
PatternMessage.CAPTURE_ALL_IS_STANDALONE_CONSTRUCT);
|
||||
}
|
||||
RegexPathElement newRegexSection = new RegexPathElement(pathElementStart, pathElementText,
|
||||
caseSensitive, pathPatternData);
|
||||
for (String variableName : newRegexSection.getVariableNames()) {
|
||||
recordCapturedVariable(pathElementStart, variableName);
|
||||
}
|
||||
newPE = newRegexSection;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (wildcard) {
|
||||
if (pos - 1 == pathElementStart) {
|
||||
newPE = new WildcardPathElement(pathElementStart);
|
||||
}
|
||||
else {
|
||||
newPE = new RegexPathElement(pathElementStart, pathElementText, caseSensitive, pathPatternData);
|
||||
}
|
||||
}
|
||||
else if (singleCharWildcardCount != 0) {
|
||||
newPE = new SingleCharWildcardedPathElement(pathElementStart, pathElementText,
|
||||
singleCharWildcardCount, caseSensitive);
|
||||
}
|
||||
else {
|
||||
newPE = new LiteralPathElement(pathElementStart, pathElementText, caseSensitive);
|
||||
}
|
||||
}
|
||||
return newPE;
|
||||
}
|
||||
|
||||
/**
|
||||
* For a path element representing a captured variable, locate the constraint pattern.
|
||||
* Assumes there is a constraint pattern.
|
||||
* @param data a complete path expression, e.g. /aaa/bbb/{ccc:...}
|
||||
* @param offset the start of the capture pattern of interest
|
||||
* @return the index of the character after the ':' within
|
||||
* the pattern expression relative to the start of the whole expression
|
||||
*/
|
||||
private int findRegexStart(char[] data, int offset) {
|
||||
int pos = offset;
|
||||
while (pos < data.length) {
|
||||
if (data[pos] == ':') {
|
||||
return pos + 1;
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all the flags and position markers computed during path element processing.
|
||||
*/
|
||||
private void resetPathElementState() {
|
||||
pathElementStart = -1;
|
||||
singleCharWildcardCount = 0;
|
||||
insideVariableCapture = false;
|
||||
variableCaptureCount = 0;
|
||||
wildcard = false;
|
||||
isCaptureTheRestVariable = false;
|
||||
variableCaptureStart = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a new captured variable. If it clashes with an existing one then report an error.
|
||||
*/
|
||||
private void recordCapturedVariable(int pos, String variableName) {
|
||||
if (capturedVariableNames == null) {
|
||||
capturedVariableNames = new ArrayList<>();
|
||||
}
|
||||
if (capturedVariableNames.contains(variableName)) {
|
||||
throw new PatternParseException(pos, this.pathPatternData,
|
||||
PatternMessage.ILLEGAL_DOUBLE_CAPTURE, variableName);
|
||||
}
|
||||
capturedVariableNames.add(variableName);
|
||||
}
|
||||
}
|
|
@ -16,14 +16,10 @@
|
|||
|
||||
package org.springframework.web.util.patterns;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.PatternSyntaxException;
|
||||
|
||||
/**
|
||||
/**
|
||||
* Parser for URI template patterns. It breaks the path pattern into a number of
|
||||
* {@link PathElement}s in a linked list.
|
||||
*
|
||||
*
|
||||
* @author Andy Clement
|
||||
* @since 5.0
|
||||
*/
|
||||
|
@ -31,64 +27,23 @@ public class PathPatternParser {
|
|||
|
||||
public final static char DEFAULT_SEPARATOR = '/';
|
||||
|
||||
// The expected path separator to split path elements during parsing
|
||||
char separator = DEFAULT_SEPARATOR;
|
||||
// Is the parser producing case sensitive PathPattern matchers, default true
|
||||
private boolean caseSensitive = true;
|
||||
|
||||
// Is the parser producing case sensitive PathPattern matchers
|
||||
boolean caseSensitive = true;
|
||||
|
||||
// The input data for parsing
|
||||
private char[] pathPatternData;
|
||||
|
||||
// The length of the input data
|
||||
private int pathPatternLength;
|
||||
|
||||
// Current parsing position
|
||||
int pos;
|
||||
|
||||
// How many ? characters in a particular path element
|
||||
private int singleCharWildcardCount;
|
||||
|
||||
// Is the path pattern using * characters in a particular path element
|
||||
private boolean wildcard = false;
|
||||
|
||||
// Is the construct {*...} being used in a particular path element
|
||||
private boolean isCaptureTheRestVariable = false;
|
||||
|
||||
// Has the parser entered a {...} variable capture block in a particular
|
||||
// path element
|
||||
private boolean insideVariableCapture = false;
|
||||
|
||||
// How many variable captures are occurring in a particular path element
|
||||
private int variableCaptureCount = 0;
|
||||
|
||||
// Start of the most recent path element in a particular path element
|
||||
int pathElementStart;
|
||||
|
||||
// Start of the most recent variable capture in a particular path element
|
||||
int variableCaptureStart;
|
||||
|
||||
// Variables captures in this path pattern
|
||||
List<String> capturedVariableNames;
|
||||
|
||||
// The head of the path element chain currently being built
|
||||
PathElement headPE;
|
||||
|
||||
// The most recently constructed path element in the chain
|
||||
PathElement currentPE;
|
||||
// The expected path separator to split path elements during parsing, default '/'
|
||||
private char separator = DEFAULT_SEPARATOR;
|
||||
|
||||
/**
|
||||
* Default constructor, will use the default path separator to identify
|
||||
* the elements of the path pattern.
|
||||
* Create a path pattern parser that will use the default separator '/' when
|
||||
* parsing patterns.
|
||||
*/
|
||||
public PathPatternParser() {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a PatternParser that will use the specified separator instead of
|
||||
* the default.
|
||||
*
|
||||
* @param separator the path separator to look for when parsing.
|
||||
* Create a path pattern parser that will use the supplied separator when
|
||||
* parsing patterns.
|
||||
* @param separator the separator expected to divide pattern elements parsed by this parser
|
||||
*/
|
||||
public PathPatternParser(char separator) {
|
||||
this.separator = separator;
|
||||
|
@ -102,319 +57,15 @@ public class PathPatternParser {
|
|||
* Process the path pattern data, a character at a time, breaking it into
|
||||
* path elements around separator boundaries and verifying the structure at each
|
||||
* stage. Produces a PathPattern object that can be used for fast matching
|
||||
* against paths.
|
||||
* against paths. Each invocation of this method delegates to a new instance of
|
||||
* the {@link InternalPathPatternParser} because that class is not thread-safe.
|
||||
*
|
||||
* @param pathPattern the input path pattern, e.g. /foo/{bar}
|
||||
* @return a PathPattern for quickly matching paths against the specified path pattern
|
||||
*/
|
||||
public PathPattern parse(String pathPattern) {
|
||||
if (pathPattern == null) {
|
||||
pathPattern = "";
|
||||
}
|
||||
// int starstar = pathPattern.indexOf("**");
|
||||
// if (starstar!=-1 && starstar!=pathPattern.length()-2) {
|
||||
// throw new IllegalStateException("Not allowed ** unless at end of pattern: "+pathPattern);
|
||||
// }
|
||||
pathPatternData = pathPattern.toCharArray();
|
||||
pathPatternLength = pathPatternData.length;
|
||||
headPE = null;
|
||||
currentPE = null;
|
||||
capturedVariableNames = null;
|
||||
pathElementStart = -1;
|
||||
pos = 0;
|
||||
resetPathElementState();
|
||||
while (pos < pathPatternLength) {
|
||||
char ch = pathPatternData[pos];
|
||||
if (ch == separator) {
|
||||
if (pathElementStart != -1) {
|
||||
pushPathElement(createPathElement());
|
||||
}
|
||||
// Skip over multiple separators
|
||||
while ((pos + 1) < pathPatternLength && pathPatternData[pos + 1] == separator) {
|
||||
pos++;
|
||||
}
|
||||
if (peekDoubleWildcard()) {
|
||||
pushPathElement(new WildcardTheRestPathElement(pos, separator));
|
||||
pos += 2;
|
||||
}
|
||||
else {
|
||||
pushPathElement(new SeparatorPathElement(pos, separator));
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (pathElementStart == -1) {
|
||||
pathElementStart = pos;
|
||||
}
|
||||
if (ch == '?') {
|
||||
singleCharWildcardCount++;
|
||||
}
|
||||
else if (ch == '{') {
|
||||
if (insideVariableCapture) {
|
||||
throw new PatternParseException(pos, pathPatternData, PatternMessage.ILLEGAL_NESTED_CAPTURE);
|
||||
// If we enforced that adjacent captures weren't allowed,
|
||||
// // this would do it (this would be an error: /foo/{bar}{boo}/)
|
||||
// } else if (pos > 0 && pathPatternData[pos - 1] == '}') {
|
||||
// throw new PatternParseException(pos, pathPatternData,
|
||||
// PatternMessage.CANNOT_HAVE_ADJACENT_CAPTURES);
|
||||
}
|
||||
insideVariableCapture = true;
|
||||
variableCaptureStart = pos;
|
||||
}
|
||||
else if (ch == '}') {
|
||||
if (!insideVariableCapture) {
|
||||
throw new PatternParseException(pos, pathPatternData, PatternMessage.MISSING_OPEN_CAPTURE);
|
||||
}
|
||||
insideVariableCapture = false;
|
||||
if (isCaptureTheRestVariable && (pos + 1) < pathPatternLength) {
|
||||
throw new PatternParseException(pos + 1, pathPatternData,
|
||||
PatternMessage.NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST);
|
||||
}
|
||||
variableCaptureCount++;
|
||||
}
|
||||
else if (ch == ':') {
|
||||
if (insideVariableCapture) {
|
||||
skipCaptureRegex();
|
||||
insideVariableCapture = false;
|
||||
variableCaptureCount++;
|
||||
}
|
||||
}
|
||||
else if (ch == '*') {
|
||||
if (insideVariableCapture) {
|
||||
if (variableCaptureStart == pos - 1) {
|
||||
isCaptureTheRestVariable = true;
|
||||
}
|
||||
}
|
||||
wildcard = true;
|
||||
}
|
||||
// Check that the characters used for captured variable names are like java identifiers
|
||||
if (insideVariableCapture) {
|
||||
if ((variableCaptureStart + 1 + (isCaptureTheRestVariable ? 1 : 0)) == pos
|
||||
&& !Character.isJavaIdentifierStart(ch)) {
|
||||
throw new PatternParseException(pos, pathPatternData,
|
||||
PatternMessage.ILLEGAL_CHARACTER_AT_START_OF_CAPTURE_DESCRIPTOR,
|
||||
Character.toString(ch));
|
||||
|
||||
}
|
||||
else if ((pos > (variableCaptureStart + 1 + (isCaptureTheRestVariable ? 1 : 0))
|
||||
&& !Character.isJavaIdentifierPart(ch))) {
|
||||
throw new PatternParseException(pos, pathPatternData,
|
||||
PatternMessage.ILLEGAL_CHARACTER_IN_CAPTURE_DESCRIPTOR, Character.toString(ch));
|
||||
}
|
||||
}
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
if (pathElementStart != -1) {
|
||||
pushPathElement(createPathElement());
|
||||
}
|
||||
return new PathPattern(pathPattern, headPE, separator, caseSensitive);
|
||||
public PathPattern parse(String pattern) {
|
||||
InternalPathPatternParser ippp = new InternalPathPatternParser(separator, caseSensitive);
|
||||
return ippp.parse(pattern);
|
||||
}
|
||||
|
||||
/**
|
||||
* Just hit a ':' and want to jump over the regex specification for this
|
||||
* variable. pos will be pointing at the ':', we want to skip until the }.
|
||||
* <p>
|
||||
* Nested {...} pairs don't have to be escaped: <tt>/abc/{var:x{1,2}}/def</tt>
|
||||
* <p>An escaped } will not be treated as the end of the regex: <tt>/abc/{var:x\\{y:}/def</tt>
|
||||
* <p>A separator that should not indicate the end of the regex can be escaped:
|
||||
*/
|
||||
private void skipCaptureRegex() {
|
||||
pos++;
|
||||
int regexStart = pos;
|
||||
int curlyBracketDepth = 0; // how deep in nested {...} pairs
|
||||
boolean previousBackslash = false;
|
||||
while (pos < pathPatternLength) {
|
||||
char ch = pathPatternData[pos];
|
||||
if (ch == '\\' && !previousBackslash) {
|
||||
pos++;
|
||||
previousBackslash = true;
|
||||
continue;
|
||||
}
|
||||
if (ch == '{' && !previousBackslash) {
|
||||
curlyBracketDepth++;
|
||||
}
|
||||
else if (ch == '}' && !previousBackslash) {
|
||||
if (curlyBracketDepth == 0) {
|
||||
if (regexStart == pos) {
|
||||
throw new PatternParseException(regexStart, pathPatternData,
|
||||
PatternMessage.MISSING_REGEX_CONSTRAINT);
|
||||
}
|
||||
return;
|
||||
}
|
||||
curlyBracketDepth--;
|
||||
}
|
||||
if (ch == separator && !previousBackslash) {
|
||||
throw new PatternParseException(pos, pathPatternData, PatternMessage.MISSING_CLOSE_CAPTURE);
|
||||
}
|
||||
pos++;
|
||||
previousBackslash = false;
|
||||
}
|
||||
throw new PatternParseException(pos - 1, pathPatternData, PatternMessage.MISSING_CLOSE_CAPTURE);
|
||||
}
|
||||
|
||||
/**
|
||||
* After processing a separator, a quick peek whether it is followed by **
|
||||
* (and only ** before the end of the pattern or the next separator)
|
||||
*/
|
||||
private boolean peekDoubleWildcard() {
|
||||
if ((pos + 2) >= pathPatternLength) {
|
||||
return false;
|
||||
}
|
||||
if (pathPatternData[pos + 1] != '*' || pathPatternData[pos + 2] != '*') {
|
||||
return false;
|
||||
}
|
||||
return (pos + 3 == pathPatternLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param newPathElement the new path element to add to the chain being built
|
||||
*/
|
||||
private void pushPathElement(PathElement newPathElement) {
|
||||
if (newPathElement instanceof CaptureTheRestPathElement) {
|
||||
// There must be a separator ahead of this thing
|
||||
// currentPE SHOULD be a SeparatorPathElement
|
||||
if (currentPE == null) {
|
||||
headPE = newPathElement;
|
||||
currentPE = newPathElement;
|
||||
}
|
||||
else if (currentPE instanceof SeparatorPathElement) {
|
||||
PathElement peBeforeSeparator = currentPE.prev;
|
||||
if (peBeforeSeparator == null) {
|
||||
// /{*foobar} is at the start
|
||||
headPE = newPathElement;
|
||||
newPathElement.prev = peBeforeSeparator;
|
||||
}
|
||||
else {
|
||||
peBeforeSeparator.next = newPathElement;
|
||||
newPathElement.prev = peBeforeSeparator;
|
||||
}
|
||||
currentPE = newPathElement;
|
||||
}
|
||||
else {
|
||||
throw new IllegalStateException("Expected SeparatorPathElement but was " + currentPE);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (headPE == null) {
|
||||
headPE = newPathElement;
|
||||
currentPE = newPathElement;
|
||||
}
|
||||
else {
|
||||
currentPE.next = newPathElement;
|
||||
newPathElement.prev = currentPE;
|
||||
currentPE = newPathElement;
|
||||
}
|
||||
}
|
||||
resetPathElementState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Used the knowledge built up whilst processing since the last path element to determine what kind of path
|
||||
* element to create.
|
||||
* @return the new path element
|
||||
*/
|
||||
private PathElement createPathElement() {
|
||||
if (insideVariableCapture) {
|
||||
throw new PatternParseException(pos, pathPatternData, PatternMessage.MISSING_CLOSE_CAPTURE);
|
||||
}
|
||||
char[] pathElementText = new char[pos - pathElementStart];
|
||||
System.arraycopy(pathPatternData, pathElementStart, pathElementText, 0, pos - pathElementStart);
|
||||
PathElement newPE = null;
|
||||
if (variableCaptureCount > 0) {
|
||||
if (variableCaptureCount == 1
|
||||
&& pathElementStart == variableCaptureStart && pathPatternData[pos - 1] == '}') {
|
||||
if (isCaptureTheRestVariable) {
|
||||
// It is {*....}
|
||||
newPE = new CaptureTheRestPathElement(pathElementStart, pathElementText, separator);
|
||||
}
|
||||
else {
|
||||
// It is a full capture of this element (possibly with constraint), for example: /foo/{abc}/
|
||||
try {
|
||||
newPE = new CaptureVariablePathElement(pathElementStart, pathElementText, caseSensitive);
|
||||
}
|
||||
catch (PatternSyntaxException pse) {
|
||||
throw new PatternParseException(pse, findRegexStart(pathPatternData, pathElementStart)
|
||||
+ pse.getIndex(), pathPatternData, PatternMessage.JDK_PATTERN_SYNTAX_EXCEPTION);
|
||||
}
|
||||
recordCapturedVariable(pathElementStart, ((CaptureVariablePathElement) newPE).getVariableName());
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (isCaptureTheRestVariable) {
|
||||
throw new PatternParseException(pathElementStart, pathPatternData,
|
||||
PatternMessage.CAPTURE_ALL_IS_STANDALONE_CONSTRUCT);
|
||||
}
|
||||
RegexPathElement newRegexSection = new RegexPathElement(pathElementStart, pathElementText,
|
||||
caseSensitive, pathPatternData);
|
||||
for (String variableName : newRegexSection.getVariableNames()) {
|
||||
recordCapturedVariable(pathElementStart, variableName);
|
||||
}
|
||||
newPE = newRegexSection;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (wildcard) {
|
||||
if (pos - 1 == pathElementStart) {
|
||||
newPE = new WildcardPathElement(pathElementStart);
|
||||
}
|
||||
else {
|
||||
newPE = new RegexPathElement(pathElementStart, pathElementText, caseSensitive, pathPatternData);
|
||||
}
|
||||
}
|
||||
else if (singleCharWildcardCount != 0) {
|
||||
newPE = new SingleCharWildcardedPathElement(pathElementStart, pathElementText,
|
||||
singleCharWildcardCount, caseSensitive);
|
||||
}
|
||||
else {
|
||||
newPE = new LiteralPathElement(pathElementStart, pathElementText, caseSensitive);
|
||||
}
|
||||
}
|
||||
return newPE;
|
||||
}
|
||||
|
||||
/**
|
||||
* For a path element representing a captured variable, locate the constraint pattern.
|
||||
* Assumes there is a constraint pattern.
|
||||
* @param data a complete path expression, e.g. /aaa/bbb/{ccc:...}
|
||||
* @param offset the start of the capture pattern of interest
|
||||
* @return the index of the character after the ':' within
|
||||
* the pattern expression relative to the start of the whole expression
|
||||
*/
|
||||
private int findRegexStart(char[] data, int offset) {
|
||||
int pos = offset;
|
||||
while (pos < data.length) {
|
||||
if (data[pos] == ':') {
|
||||
return pos + 1;
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all the flags and position markers computed during path element processing.
|
||||
*/
|
||||
private void resetPathElementState() {
|
||||
pathElementStart = -1;
|
||||
singleCharWildcardCount = 0;
|
||||
insideVariableCapture = false;
|
||||
variableCaptureCount = 0;
|
||||
wildcard = false;
|
||||
isCaptureTheRestVariable = false;
|
||||
variableCaptureStart = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a new captured variable. If it clashes with an existing one then report an error.
|
||||
*/
|
||||
private void recordCapturedVariable(int pos, String variableName) {
|
||||
if (capturedVariableNames == null) {
|
||||
capturedVariableNames = new ArrayList<>();
|
||||
}
|
||||
if (capturedVariableNames.contains(variableName)) {
|
||||
throw new PatternParseException(pos, this.pathPatternData,
|
||||
PatternMessage.ILLEGAL_DOUBLE_CAPTURE, variableName);
|
||||
}
|
||||
capturedVariableNames.add(variableName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue