From 67881a57262c98fee38eec47452afd8f1e4347fa Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 17 May 2017 17:40:25 +0200 Subject: [PATCH] Polish PathPattern parser (including package change to web.util.pattern) Issue: SPR-15531 --- .../UrlBasedCorsConfigurationSource.java | 4 +- .../CaptureTheRestPathElement.java | 23 +- .../CaptureVariablePathElement.java | 62 +-- .../InternalPathPatternParser.java | 273 ++++++------ .../LiteralPathElement.java | 31 +- .../{ => pattern}/ParsingPathMatcher.java | 93 +++-- .../{patterns => pattern}/PathElement.java | 47 +-- .../{patterns => pattern}/PathPattern.java | 387 ++++++++++-------- .../PathPatternParser.java | 65 +-- .../util/pattern/PatternParseException.java | 121 ++++++ .../RegexPathElement.java | 105 ++--- .../SeparatorPathElement.java | 20 +- .../SingleCharWildcardedPathElement.java | 57 +-- .../{patterns => pattern}/SubSequence.java | 26 +- .../WildcardPathElement.java | 28 +- .../WildcardTheRestPathElement.java | 18 +- .../web/util/pattern/package-info.java | 4 + .../util/patterns/PathPatternComparator.java | 40 -- .../util/patterns/PathRemainingMatchInfo.java | 58 --- .../PatternComparatorConsideringPath.java | 54 --- .../web/util/patterns/PatternMessage.java | 54 --- .../util/patterns/PatternParseException.java | 88 ---- .../PathPatternMatcherTests.java | 67 +-- .../PathPatternParserTests.java | 163 ++++---- .../reactive/config/PathMatchConfigurer.java | 4 +- .../config/ResourceHandlerRegistry.java | 7 +- .../server/PathResourceLookupFunction.java | 4 +- .../function/server/RequestPredicates.java | 4 +- .../handler/AbstractHandlerMapping.java | 6 +- .../handler/AbstractUrlHandlerMapping.java | 7 +- .../handler/SimpleUrlHandlerMapping.java | 12 +- .../resource/ResourceUrlProvider.java | 20 +- .../condition/PatternsRequestCondition.java | 4 +- .../server/RequestPredicatesTests.java | 2 +- .../method/HandlerMethodMappingTests.java | 6 +- 35 files changed, 958 insertions(+), 1006 deletions(-) rename spring-web/src/main/java/org/springframework/web/util/{patterns => pattern}/CaptureTheRestPathElement.java (87%) rename spring-web/src/main/java/org/springframework/web/util/{patterns => pattern}/CaptureVariablePathElement.java (70%) rename spring-web/src/main/java/org/springframework/web/util/{patterns => pattern}/InternalPathPatternParser.java (53%) rename spring-web/src/main/java/org/springframework/web/util/{patterns => pattern}/LiteralPathElement.java (76%) rename spring-web/src/main/java/org/springframework/web/util/{ => pattern}/ParsingPathMatcher.java (66%) rename spring-web/src/main/java/org/springframework/web/util/{patterns => pattern}/PathElement.java (67%) rename spring-web/src/main/java/org/springframework/web/util/{patterns => pattern}/PathPattern.java (72%) rename spring-web/src/main/java/org/springframework/web/util/{patterns => pattern}/PathPatternParser.java (68%) create mode 100644 spring-web/src/main/java/org/springframework/web/util/pattern/PatternParseException.java rename spring-web/src/main/java/org/springframework/web/util/{patterns => pattern}/RegexPathElement.java (61%) rename spring-web/src/main/java/org/springframework/web/util/{patterns => pattern}/SeparatorPathElement.java (87%) rename spring-web/src/main/java/org/springframework/web/util/{patterns => pattern}/SingleCharWildcardedPathElement.java (68%) rename spring-web/src/main/java/org/springframework/web/util/{patterns => pattern}/SubSequence.java (73%) rename spring-web/src/main/java/org/springframework/web/util/{patterns => pattern}/WildcardPathElement.java (79%) rename spring-web/src/main/java/org/springframework/web/util/{patterns => pattern}/WildcardTheRestPathElement.java (85%) create mode 100644 spring-web/src/main/java/org/springframework/web/util/pattern/package-info.java delete mode 100644 spring-web/src/main/java/org/springframework/web/util/patterns/PathPatternComparator.java delete mode 100644 spring-web/src/main/java/org/springframework/web/util/patterns/PathRemainingMatchInfo.java delete mode 100644 spring-web/src/main/java/org/springframework/web/util/patterns/PatternComparatorConsideringPath.java delete mode 100644 spring-web/src/main/java/org/springframework/web/util/patterns/PatternMessage.java delete mode 100644 spring-web/src/main/java/org/springframework/web/util/patterns/PatternParseException.java rename spring-web/src/test/java/org/springframework/web/util/{patterns => pattern}/PathPatternMatcherTests.java (96%) rename spring-web/src/test/java/org/springframework/web/util/{patterns => pattern}/PathPatternParserTests.java (73%) diff --git a/spring-web/src/main/java/org/springframework/web/cors/reactive/UrlBasedCorsConfigurationSource.java b/spring-web/src/main/java/org/springframework/web/cors/reactive/UrlBasedCorsConfigurationSource.java index 3dcad864dab..9fb2c74d547 100644 --- a/spring-web/src/main/java/org/springframework/web/cors/reactive/UrlBasedCorsConfigurationSource.java +++ b/spring-web/src/main/java/org/springframework/web/cors/reactive/UrlBasedCorsConfigurationSource.java @@ -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. @@ -25,7 +25,7 @@ import org.springframework.util.PathMatcher; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.support.HttpRequestPathHelper; -import org.springframework.web.util.ParsingPathMatcher; +import org.springframework.web.util.pattern.ParsingPathMatcher; /** * Provide a per reactive request {@link CorsConfiguration} instance based on a diff --git a/spring-web/src/main/java/org/springframework/web/util/patterns/CaptureTheRestPathElement.java b/spring-web/src/main/java/org/springframework/web/util/pattern/CaptureTheRestPathElement.java similarity index 87% rename from spring-web/src/main/java/org/springframework/web/util/patterns/CaptureTheRestPathElement.java rename to spring-web/src/main/java/org/springframework/web/util/pattern/CaptureTheRestPathElement.java index 21d466096f0..3ac6bdaf081 100644 --- a/spring-web/src/main/java/org/springframework/web/util/patterns/CaptureTheRestPathElement.java +++ b/spring-web/src/main/java/org/springframework/web/util/pattern/CaptureTheRestPathElement.java @@ -14,19 +14,21 @@ * limitations under the License. */ -package org.springframework.web.util.patterns; +package org.springframework.web.util.pattern; -import org.springframework.web.util.patterns.PathPattern.MatchingContext; +import org.springframework.web.util.pattern.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 + * @since 5.0 */ class CaptureTheRestPathElement extends PathElement { - private String variableName; + private final String variableName; + /** * @param pos position of the path element within the path pattern text @@ -35,9 +37,10 @@ class CaptureTheRestPathElement extends PathElement { */ CaptureTheRestPathElement(int pos, char[] captureDescriptor, char separator) { super(pos, separator); - variableName = new String(captureDescriptor, 2, captureDescriptor.length - 3); + this.variableName = new String(captureDescriptor, 2, captureDescriptor.length - 3); } + @Override public boolean matches(int candidateIndex, MatchingContext matchingContext) { // No need to handle 'match start' checking as this captures everything @@ -59,10 +62,6 @@ class CaptureTheRestPathElement extends PathElement { return true; } - public String toString() { - return "CaptureTheRest(/{*" + variableName + "})"; - } - @Override public int getNormalizedLength() { return 1; @@ -77,4 +76,10 @@ class CaptureTheRestPathElement extends PathElement { public int getCaptureCount() { return 1; } -} \ No newline at end of file + + + public String toString() { + return "CaptureTheRest(/{*" + this.variableName + "})"; + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/util/patterns/CaptureVariablePathElement.java b/spring-web/src/main/java/org/springframework/web/util/pattern/CaptureVariablePathElement.java similarity index 70% rename from spring-web/src/main/java/org/springframework/web/util/patterns/CaptureVariablePathElement.java rename to spring-web/src/main/java/org/springframework/web/util/pattern/CaptureVariablePathElement.java index b26660d0f67..43a3c85f2a8 100644 --- a/spring-web/src/main/java/org/springframework/web/util/patterns/CaptureVariablePathElement.java +++ b/spring-web/src/main/java/org/springframework/web/util/pattern/CaptureVariablePathElement.java @@ -14,11 +14,10 @@ * limitations under the License. */ -package org.springframework.web.util.patterns; +package org.springframework.web.util.pattern; import java.util.regex.Matcher; - -import org.springframework.web.util.patterns.PathPattern.MatchingContext; +import java.util.regex.Pattern; /** * A path element representing capturing a piece of the path as a variable. In the pattern @@ -26,12 +25,14 @@ import org.springframework.web.util.patterns.PathPattern.MatchingContext; * must be at least one character to bind to the variable. * * @author Andy Clement + * @since 5.0 */ class CaptureVariablePathElement extends PathElement { - private String variableName; + private final String variableName; + + private Pattern constraintPattern; - private java.util.regex.Pattern constraintPattern; /** * @param pos the position in the pattern of this capture element @@ -48,43 +49,47 @@ class CaptureVariablePathElement extends PathElement { } if (colon == -1) { // no constraint - variableName = new String(captureDescriptor, 1, captureDescriptor.length - 2); + this.variableName = new String(captureDescriptor, 1, captureDescriptor.length - 2); } else { - variableName = new String(captureDescriptor, 1, colon - 1); + this.variableName = new String(captureDescriptor, 1, colon - 1); if (caseSensitive) { - constraintPattern = java.util.regex.Pattern - .compile(new String(captureDescriptor, colon + 1, captureDescriptor.length - colon - 2)); + this.constraintPattern = Pattern.compile( + new String(captureDescriptor, colon + 1, captureDescriptor.length - colon - 2)); } else { - constraintPattern = java.util.regex.Pattern.compile( + this.constraintPattern = Pattern.compile( new String(captureDescriptor, colon + 1, captureDescriptor.length - colon - 2), - java.util.regex.Pattern.CASE_INSENSITIVE); + Pattern.CASE_INSENSITIVE); } } } + @Override - public boolean matches(int candidateIndex, MatchingContext matchingContext) { + public boolean matches(int candidateIndex, PathPattern.MatchingContext matchingContext) { int nextPos = matchingContext.scanAhead(candidateIndex); // There must be at least one character to capture: if (nextPos == candidateIndex) { return false; } + CharSequence candidateCapture = null; - if (constraintPattern != null) { + if (this.constraintPattern != null) { // TODO possible optimization - only regex match if rest of pattern matches? Benefit likely to vary pattern to pattern candidateCapture = new SubSequence(matchingContext.candidate, candidateIndex, nextPos); - Matcher m = constraintPattern.matcher(candidateCapture); - if (m.groupCount() != 0) { - throw new IllegalArgumentException("No capture groups allowed in the constraint regex: " + constraintPattern.pattern()); + Matcher matcher = constraintPattern.matcher(candidateCapture); + if (matcher.groupCount() != 0) { + throw new IllegalArgumentException( + "No capture groups allowed in the constraint regex: " + this.constraintPattern.pattern()); } - if (!m.matches()) { + if (!matcher.matches()) { return false; } } + boolean match = false; - if (next == null) { + if (this.next == null) { if (matchingContext.determineRemainingPath && nextPos > candidateIndex) { matchingContext.remainingPathIndex = nextPos; match = true; @@ -101,14 +106,16 @@ class CaptureVariablePathElement extends PathElement { } else { 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 { - match = next.matches(nextPos, matchingContext); + match = this.next.matches(nextPos, matchingContext); } } + if (match && matchingContext.extractingVariables) { - matchingContext.set(variableName, new String(matchingContext.candidate, candidateIndex, nextPos - candidateIndex)); + matchingContext.set(this.variableName, + new String(matchingContext.candidate, candidateIndex, nextPos - candidateIndex)); } return match; } @@ -117,10 +124,6 @@ class CaptureVariablePathElement extends PathElement { return this.variableName; } - public String toString() { - return "CaptureVariable({" + variableName + (constraintPattern == null ? "" : ":" + constraintPattern.pattern()) + "})"; - } - @Override public int getNormalizedLength() { return 1; @@ -140,4 +143,11 @@ class CaptureVariablePathElement extends PathElement { public int getScore() { return CAPTURE_VARIABLE_WEIGHT; } -} \ No newline at end of file + + + public String toString() { + return "CaptureVariable({" + this.variableName + + (this.constraintPattern != null ? ":" + this.constraintPattern.pattern() : "") + "})"; + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/util/patterns/InternalPathPatternParser.java b/spring-web/src/main/java/org/springframework/web/util/pattern/InternalPathPatternParser.java similarity index 53% rename from spring-web/src/main/java/org/springframework/web/util/patterns/InternalPathPatternParser.java rename to spring-web/src/main/java/org/springframework/web/util/pattern/InternalPathPatternParser.java index 1c090a34e5c..5223254b89e 100644 --- a/spring-web/src/main/java/org/springframework/web/util/patterns/InternalPathPatternParser.java +++ b/spring-web/src/main/java/org/springframework/web/util/pattern/InternalPathPatternParser.java @@ -14,12 +14,14 @@ * limitations under the License. */ -package org.springframework.web.util.patterns; +package org.springframework.web.util.pattern; import java.util.ArrayList; import java.util.List; import java.util.regex.PatternSyntaxException; +import org.springframework.web.util.pattern.PatternParseException.PatternMessage; + /** * 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. @@ -27,7 +29,7 @@ import java.util.regex.PatternSyntaxException; * @author Andy Clement * @since 5.0 */ -public class InternalPathPatternParser { +class InternalPathPatternParser { // The expected path separator to split path elements during parsing char separator = PathPatternParser.DEFAULT_SEPARATOR; @@ -80,13 +82,12 @@ public class InternalPathPatternParser { // 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 * @param caseSensitive true if PathPatterns should be sensitive to case - * @param matchOptionalTrailingSlash true if patterns without a trailing slash can match paths that do have a trailing slash + * @param matchOptionalTrailingSlash true if patterns without a trailing slash + * can match paths that do have a trailing slash */ public InternalPathPatternParser(char separator, boolean caseSensitive, boolean matchOptionalTrailingSlash) { this.separator = separator; @@ -94,108 +95,111 @@ public class InternalPathPatternParser { this.matchOptionalTrailingSlash = matchOptionalTrailingSlash; } + /** * 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 + * @throws PatternParseException in case of parse errors */ - public PathPattern parse(String pathPattern) { - if (pathPattern == null) { - pathPattern = ""; - } - pathPatternData = pathPattern.toCharArray(); - pathPatternLength = pathPatternData.length; - headPE = null; - currentPE = null; - capturedVariableNames = null; - pathElementStart = -1; - pos = 0; + public PathPattern parse(String pathPattern) throws PatternParseException { + this.pathPatternData = pathPattern.toCharArray(); + this.pathPatternLength = pathPatternData.length; + this.headPE = null; + this.currentPE = null; + this.capturedVariableNames = null; + this.pathElementStart = -1; + this.pos = 0; resetPathElementState(); - while (pos < pathPatternLength) { - char ch = pathPatternData[pos]; - if (ch == separator) { - if (pathElementStart != -1) { + + while (this.pos < this.pathPatternLength) { + char ch = this.pathPatternData[this.pos]; + if (ch == this.separator) { + if (this.pathElementStart != -1) { pushPathElement(createPathElement()); } if (peekDoubleWildcard()) { - pushPathElement(new WildcardTheRestPathElement(pos, separator)); - pos += 2; + pushPathElement(new WildcardTheRestPathElement(this.pos, this.separator)); + this.pos += 2; } else { - pushPathElement(new SeparatorPathElement(pos, separator)); + pushPathElement(new SeparatorPathElement(this.pos, this.separator)); } } else { - if (pathElementStart == -1) { - pathElementStart = pos; + if (this.pathElementStart == -1) { + this.pathElementStart = this.pos; } if (ch == '?') { - singleCharWildcardCount++; + this.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); + if (this.insideVariableCapture) { + throw new PatternParseException(this.pos, this.pathPatternData, + PatternMessage.ILLEGAL_NESTED_CAPTURE); } - insideVariableCapture = true; - variableCaptureStart = pos; + // 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); + this.insideVariableCapture = true; + this.variableCaptureStart = pos; } else if (ch == '}') { - if (!insideVariableCapture) { - throw new PatternParseException(pos, pathPatternData, PatternMessage.MISSING_OPEN_CAPTURE); + if (!this.insideVariableCapture) { + throw new PatternParseException(this.pos, this.pathPatternData, + PatternMessage.MISSING_OPEN_CAPTURE); } - insideVariableCapture = false; - if (isCaptureTheRestVariable && (pos + 1) < pathPatternLength) { - throw new PatternParseException(pos + 1, pathPatternData, + this.insideVariableCapture = false; + if (this.isCaptureTheRestVariable && (this.pos + 1) < this.pathPatternLength) { + throw new PatternParseException(this.pos + 1, this.pathPatternData, PatternMessage.NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST); } - variableCaptureCount++; + this.variableCaptureCount++; } else if (ch == ':') { - if (insideVariableCapture) { + if (this.insideVariableCapture) { skipCaptureRegex(); - insideVariableCapture = false; - variableCaptureCount++; + this.insideVariableCapture = false; + this.variableCaptureCount++; } } else if (ch == '*') { - if (insideVariableCapture) { - if (variableCaptureStart == pos - 1) { - isCaptureTheRestVariable = true; + if (this.insideVariableCapture) { + if (this.variableCaptureStart == pos - 1) { + this.isCaptureTheRestVariable = true; } } - wildcard = true; + this.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, + if (this.insideVariableCapture) { + if ((this.variableCaptureStart + 1 + (this.isCaptureTheRestVariable ? 1 : 0)) == this.pos && + !Character.isJavaIdentifierStart(ch)) { + throw new PatternParseException(this.pos, this.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)); + else if ((this.pos > (this.variableCaptureStart + 1 + (this.isCaptureTheRestVariable ? 1 : 0)) && + !Character.isJavaIdentifierPart(ch))) { + throw new PatternParseException(this.pos, this.pathPatternData, + PatternMessage.ILLEGAL_CHARACTER_IN_CAPTURE_DESCRIPTOR, + Character.toString(ch)); } } } - pos++; + this.pos++; } - if (pathElementStart != -1) { + if (this.pathElementStart != -1) { pushPathElement(createPathElement()); } - return new PathPattern(pathPattern, headPE, separator, caseSensitive, matchOptionalTrailingSlash); + return new PathPattern( + pathPattern, this.headPE, this.separator, this.caseSensitive, this.matchOptionalTrailingSlash); } /** @@ -207,14 +211,15 @@ public class InternalPathPatternParser { *

A separator that should not indicate the end of the regex can be escaped: */ private void skipCaptureRegex() { - pos++; - int regexStart = pos; + this.pos++; + int regexStart = this.pos; int curlyBracketDepth = 0; // how deep in nested {...} pairs boolean previousBackslash = false; - while (pos < pathPatternLength) { - char ch = pathPatternData[pos]; + + while (this.pos < this.pathPatternLength) { + char ch = this.pathPatternData[pos]; if (ch == '\\' && !previousBackslash) { - pos++; + this.pos++; previousBackslash = true; continue; } @@ -223,21 +228,24 @@ public class InternalPathPatternParser { } else if (ch == '}' && !previousBackslash) { if (curlyBracketDepth == 0) { - if (regexStart == pos) { - throw new PatternParseException(regexStart, pathPatternData, + if (regexStart == this.pos) { + throw new PatternParseException(regexStart, this.pathPatternData, PatternMessage.MISSING_REGEX_CONSTRAINT); } return; } curlyBracketDepth--; } - if (ch == separator && !previousBackslash) { - throw new PatternParseException(pos, pathPatternData, PatternMessage.MISSING_CLOSE_CAPTURE); + if (ch == this.separator && !previousBackslash) { + throw new PatternParseException(this.pos, this.pathPatternData, + PatternMessage.MISSING_CLOSE_CAPTURE); } - pos++; + this.pos++; previousBackslash = false; } - throw new PatternParseException(pos - 1, pathPatternData, PatternMessage.MISSING_CLOSE_CAPTURE); + + throw new PatternParseException(this.pos - 1, this.pathPatternData, + PatternMessage.MISSING_CLOSE_CAPTURE); } /** @@ -245,13 +253,13 @@ public class InternalPathPatternParser { * (and only ** before the end of the pattern or the next separator) */ private boolean peekDoubleWildcard() { - if ((pos + 2) >= pathPatternLength) { + if ((this.pos + 2) >= this.pathPatternLength) { return false; } - if (pathPatternData[pos + 1] != '*' || pathPatternData[pos + 2] != '*') { + if (this.pathPatternData[this.pos + 1] != '*' || this.pathPatternData[this.pos + 2] != '*') { return false; } - return (pos + 3 == pathPatternLength); + return (this.pos + 3 == this.pathPatternLength); } /** @@ -261,38 +269,39 @@ public class InternalPathPatternParser { 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; + if (this.currentPE == null) { + this.headPE = newPathElement; + this.currentPE = newPathElement; } - else if (currentPE instanceof SeparatorPathElement) { - PathElement peBeforeSeparator = currentPE.prev; + else if (this.currentPE instanceof SeparatorPathElement) { + PathElement peBeforeSeparator = this.currentPE.prev; if (peBeforeSeparator == null) { // /{*foobar} is at the start - headPE = newPathElement; - newPathElement.prev = peBeforeSeparator; + this.headPE = newPathElement; + newPathElement.prev = null; } else { peBeforeSeparator.next = newPathElement; newPathElement.prev = peBeforeSeparator; } - currentPE = newPathElement; + this.currentPE = newPathElement; } else { - throw new IllegalStateException("Expected SeparatorPathElement but was " + currentPE); + throw new IllegalStateException("Expected SeparatorPathElement but was " + this.currentPE); } } else { - if (headPE == null) { - headPE = newPathElement; - currentPE = newPathElement; + if (this.headPE == null) { + this.headPE = newPathElement; + this.currentPE = newPathElement; } else { - currentPE.next = newPathElement; - newPathElement.prev = currentPE; - currentPE = newPathElement; + this.currentPE.next = newPathElement; + newPathElement.prev = this.currentPE; + this.currentPE = newPathElement; } } + resetPathElementState(); } @@ -302,61 +311,70 @@ public class InternalPathPatternParser { * @return the new path element */ private PathElement createPathElement() { - if (insideVariableCapture) { - throw new PatternParseException(pos, pathPatternData, PatternMessage.MISSING_CLOSE_CAPTURE); + if (this.insideVariableCapture) { + throw new PatternParseException(this.pos, this.pathPatternData, PatternMessage.MISSING_CLOSE_CAPTURE); } - char[] pathElementText = new char[pos - pathElementStart]; - System.arraycopy(pathPatternData, pathElementStart, pathElementText, 0, pos - pathElementStart); + + char[] pathElementText = new char[this.pos - this.pathElementStart]; + System.arraycopy(this.pathPatternData, this.pathElementStart, pathElementText, 0, + this.pos - this.pathElementStart); PathElement newPE = null; - if (variableCaptureCount > 0) { - if (variableCaptureCount == 1 - && pathElementStart == variableCaptureStart && pathPatternData[pos - 1] == '}') { - if (isCaptureTheRestVariable) { + + if (this.variableCaptureCount > 0) { + if (this.variableCaptureCount == 1 && this.pathElementStart == this.variableCaptureStart && + this.pathPatternData[this.pos - 1] == '}') { + if (this.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, separator); + newPE = new CaptureVariablePathElement(this.pathElementStart, pathElementText, + this.caseSensitive, this.separator); } catch (PatternSyntaxException pse) { - throw new PatternParseException(pse, findRegexStart(pathPatternData, pathElementStart) - + pse.getIndex(), pathPatternData, PatternMessage.JDK_PATTERN_SYNTAX_EXCEPTION); + throw new PatternParseException(pse, + findRegexStart(this.pathPatternData, this.pathElementStart) + pse.getIndex(), + this.pathPatternData, PatternMessage.REGEX_PATTERN_SYNTAX_EXCEPTION); } - recordCapturedVariable(pathElementStart, ((CaptureVariablePathElement) newPE).getVariableName()); + recordCapturedVariable(this.pathElementStart, + ((CaptureVariablePathElement) newPE).getVariableName()); } } else { - if (isCaptureTheRestVariable) { - throw new PatternParseException(pathElementStart, pathPatternData, + if (this.isCaptureTheRestVariable) { + throw new PatternParseException(this.pathElementStart, this.pathPatternData, PatternMessage.CAPTURE_ALL_IS_STANDALONE_CONSTRUCT); } - RegexPathElement newRegexSection = new RegexPathElement(pathElementStart, pathElementText, - caseSensitive, pathPatternData, separator); + RegexPathElement newRegexSection = new RegexPathElement(this.pathElementStart, pathElementText, + this.caseSensitive, this.pathPatternData, this.separator); for (String variableName : newRegexSection.getVariableNames()) { - recordCapturedVariable(pathElementStart, variableName); + recordCapturedVariable(this.pathElementStart, variableName); } newPE = newRegexSection; } } else { - if (wildcard) { - if (pos - 1 == pathElementStart) { - newPE = new WildcardPathElement(pathElementStart, separator); + if (this.wildcard) { + if (this.pos - 1 == this.pathElementStart) { + newPE = new WildcardPathElement(this.pathElementStart, this.separator); } else { - newPE = new RegexPathElement(pathElementStart, pathElementText, caseSensitive, pathPatternData, separator); + newPE = new RegexPathElement(this.pathElementStart, pathElementText, + this.caseSensitive, this.pathPatternData, this.separator); } } - else if (singleCharWildcardCount != 0) { - newPE = new SingleCharWildcardedPathElement(pathElementStart, pathElementText, - singleCharWildcardCount, caseSensitive, separator); + else if (this.singleCharWildcardCount != 0) { + newPE = new SingleCharWildcardedPathElement(this.pathElementStart, pathElementText, + this.singleCharWildcardCount, this.caseSensitive, this.separator); } else { - newPE = new LiteralPathElement(pathElementStart, pathElementText, caseSensitive, separator); + newPE = new LiteralPathElement(this.pathElementStart, pathElementText, + this.caseSensitive, this.separator); } } + return newPE; } @@ -383,26 +401,27 @@ public class InternalPathPatternParser { * 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; + this.pathElementStart = -1; + this.singleCharWildcardCount = 0; + this.insideVariableCapture = false; + this.variableCaptureCount = 0; + this.wildcard = false; + this.isCaptureTheRestVariable = false; + this.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 (this.capturedVariableNames == null) { + this.capturedVariableNames = new ArrayList<>(); } - if (capturedVariableNames.contains(variableName)) { + if (this.capturedVariableNames.contains(variableName)) { throw new PatternParseException(pos, this.pathPatternData, PatternMessage.ILLEGAL_DOUBLE_CAPTURE, variableName); } - capturedVariableNames.add(variableName); + this.capturedVariableNames.add(variableName); } -} \ No newline at end of file + +} diff --git a/spring-web/src/main/java/org/springframework/web/util/patterns/LiteralPathElement.java b/spring-web/src/main/java/org/springframework/web/util/pattern/LiteralPathElement.java similarity index 76% rename from spring-web/src/main/java/org/springframework/web/util/patterns/LiteralPathElement.java rename to spring-web/src/main/java/org/springframework/web/util/pattern/LiteralPathElement.java index 5a07ce13f7f..c72e4550963 100644 --- a/spring-web/src/main/java/org/springframework/web/util/patterns/LiteralPathElement.java +++ b/spring-web/src/main/java/org/springframework/web/util/pattern/LiteralPathElement.java @@ -14,9 +14,9 @@ * limitations under the License. */ -package org.springframework.web.util.patterns; +package org.springframework.web.util.pattern; -import org.springframework.web.util.patterns.PathPattern.MatchingContext; +import org.springframework.web.util.pattern.PathPattern.MatchingContext; /** * A literal path element. In the pattern '/foo/bar/goo' there are three @@ -51,11 +51,12 @@ class LiteralPathElement extends PathElement { @Override public boolean matches(int candidateIndex, MatchingContext matchingContext) { if ((candidateIndex + text.length) > matchingContext.candidateLength) { - return false; // not enough data, cannot be a match + return false; // not enough data, cannot be a match } - if (caseSensitive) { + + if (this.caseSensitive) { for (int i = 0; i < len; i++) { - if (matchingContext.candidate[candidateIndex++] != text[i]) { + if (matchingContext.candidate[candidateIndex++] != this.text[i]) { return false; } } @@ -63,12 +64,13 @@ class LiteralPathElement extends PathElement { 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]) { + if (Character.toLowerCase(matchingContext.candidate[candidateIndex++]) != this.text[i]) { return false; } } } - if (next == null) { + + if (this.next == null) { if (matchingContext.determineRemainingPath && nextIfExistsIsSeparator(candidateIndex, matchingContext)) { matchingContext.remainingPathIndex = candidateIndex; return true; @@ -78,17 +80,17 @@ class LiteralPathElement extends PathElement { return true; } else { - return matchingContext.isAllowOptionalTrailingSlash() && - (candidateIndex + 1) == matchingContext.candidateLength && - matchingContext.candidate[candidateIndex] == separator; + return (matchingContext.isAllowOptionalTrailingSlash() && + (candidateIndex + 1) == matchingContext.candidateLength && + matchingContext.candidate[candidateIndex] == separator); } } } else { 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 } - return next.matches(candidateIndex, matchingContext); + return this.next.matches(candidateIndex, matchingContext); } } @@ -97,8 +99,9 @@ class LiteralPathElement extends PathElement { return len; } + public String toString() { - return "Literal(" + new String(text) + ")"; + return "Literal(" + String.valueOf(this.text) + ")"; } -} \ No newline at end of file +} diff --git a/spring-web/src/main/java/org/springframework/web/util/ParsingPathMatcher.java b/spring-web/src/main/java/org/springframework/web/util/pattern/ParsingPathMatcher.java similarity index 66% rename from spring-web/src/main/java/org/springframework/web/util/ParsingPathMatcher.java rename to spring-web/src/main/java/org/springframework/web/util/pattern/ParsingPathMatcher.java index 4344e7183c5..e78ce390165 100644 --- a/spring-web/src/main/java/org/springframework/web/util/ParsingPathMatcher.java +++ b/spring-web/src/main/java/org/springframework/web/util/pattern/ParsingPathMatcher.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.web.util; +package org.springframework.web.util.pattern; import java.util.Comparator; import java.util.Map; @@ -22,10 +22,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import org.springframework.util.PathMatcher; -import org.springframework.web.util.patterns.PathPattern; -import org.springframework.web.util.patterns.PathPatternParser; -import org.springframework.web.util.patterns.PatternComparatorConsideringPath; - /** * {@link PathMatcher} implementation for path patterns parsed @@ -35,38 +31,50 @@ import org.springframework.web.util.patterns.PatternComparatorConsideringPath; * and quick comparison. * * @author Andy Clement + * @author Juergen Hoeller * @since 5.0 * @see PathPattern */ public class ParsingPathMatcher implements PathMatcher { - private final ConcurrentMap cache = - new ConcurrentHashMap<>(64); - private final PathPatternParser parser = new PathPatternParser(); + private final ConcurrentMap cache = new ConcurrentHashMap<>(256); + + + @Override + public boolean isPattern(String path) { + // TODO crude, should be smarter, lookup pattern and ask it + return (path.indexOf('*') != -1 || path.indexOf('?') != -1); + } + @Override public boolean match(String pattern, String path) { - PathPattern p = getPathPattern(pattern); - return p.matches(path); + PathPattern pathPattern = getPathPattern(pattern); + return pathPattern.matches(path); } @Override public boolean matchStart(String pattern, String path) { - PathPattern p = getPathPattern(pattern); - return p.matchStart(path); + PathPattern pathPattern = getPathPattern(pattern); + return pathPattern.matchStart(path); } @Override public String extractPathWithinPattern(String pattern, String path) { - PathPattern p = getPathPattern(pattern); - return p.extractPathWithinPattern(path); + PathPattern pathPattern = getPathPattern(pattern); + return pathPattern.extractPathWithinPattern(path); } @Override public Map extractUriTemplateVariables(String pattern, String path) { - PathPattern p = getPathPattern(pattern); - return p.matchAndExtract(path); + PathPattern pathPattern = getPathPattern(pattern); + return pathPattern.matchAndExtract(path); + } + + @Override + public Comparator getPatternComparator(String path) { + return new PathPatternStringComparatorConsideringPath(path); } @Override @@ -75,12 +83,17 @@ public class ParsingPathMatcher implements PathMatcher { return pathPattern.combine(pattern2); } - @Override - public Comparator getPatternComparator(String path) { - return new PathPatternStringComparatorConsideringPath(path); + private PathPattern getPathPattern(String pattern) { + PathPattern pathPattern = this.cache.get(pattern); + if (pathPattern == null) { + pathPattern = this.parser.parse(pattern); + this.cache.put(pattern, pathPattern); + } + return pathPattern; } - class PathPatternStringComparatorConsideringPath implements Comparator { + + private class PathPatternStringComparatorConsideringPath implements Comparator { private final PatternComparatorConsideringPath ppcp; @@ -100,22 +113,38 @@ public class ParsingPathMatcher implements PathMatcher { PathPattern p2 = getPathPattern(o2); return this.ppcp.compare(p1, p2); } - } - @Override - public boolean isPattern(String path) { - // TODO crude, should be smarter, lookup pattern and ask it - return (path.indexOf('*') != -1 || path.indexOf('?') != -1); - } - private PathPattern getPathPattern(String pattern) { - PathPattern pathPattern = this.cache.get(pattern); - if (pathPattern == null) { - pathPattern = this.parser.parse(pattern); - this.cache.put(pattern, pathPattern); + /** + * {@link PathPattern} comparator that takes account of a specified + * path and sorts anything that exactly matches it to be first. + */ + static class PatternComparatorConsideringPath implements Comparator { + + private final String path; + + public PatternComparatorConsideringPath(String path) { + this.path = path; + } + + @Override + public int compare(PathPattern o1, PathPattern o2) { + // Nulls get sorted to the end + if (o1 == null) { + return (o2 == null ? 0 : +1); + } + else if (o2 == null) { + return -1; + } + if (o1.getPatternString().equals(this.path)) { + return (o2.getPatternString().equals(this.path)) ? 0 : -1; + } + else if (o2.getPatternString().equals(this.path)) { + return +1; + } + return o1.compareTo(o2); } - return pathPattern; } } diff --git a/spring-web/src/main/java/org/springframework/web/util/patterns/PathElement.java b/spring-web/src/main/java/org/springframework/web/util/pattern/PathElement.java similarity index 67% rename from spring-web/src/main/java/org/springframework/web/util/patterns/PathElement.java rename to spring-web/src/main/java/org/springframework/web/util/pattern/PathElement.java index fc308b3f7f3..83509a497e6 100644 --- a/spring-web/src/main/java/org/springframework/web/util/patterns/PathElement.java +++ b/spring-web/src/main/java/org/springframework/web/util/pattern/PathElement.java @@ -14,9 +14,9 @@ * limitations under the License. */ -package org.springframework.web.util.patterns; +package org.springframework.web.util.pattern; -import org.springframework.web.util.patterns.PathPattern.MatchingContext; +import org.springframework.web.util.pattern.PathPattern.MatchingContext; /** * Common supertype for the Ast nodes created to represent a path pattern. @@ -31,25 +31,19 @@ abstract class PathElement { 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 - */ + // Position in the pattern where this path element starts + protected final int pos; + + // The separator used in this path pattern + protected final char separator; + + // The next path element in the chain protected PathElement next; - /** - * The previous path element in the chain - */ + // The previous path element in the chain protected PathElement prev; - - /** - * The separator used in this path pattern - */ - protected char separator; + /** * Create a new path element. @@ -61,46 +55,47 @@ abstract class PathElement { this.separator = separator; } + /** * 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 + * @return {@code true} if it matches, otherwise {@code false} */ public abstract boolean matches(int candidatePos, MatchingContext matchingContext); /** - * @return the length of the path element where captures are considered to be one character long + * 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 + * Return the number of variables captured by the path element. */ public int getCaptureCount() { return 0; } /** - * @return the number of wildcard elements (*, ?) in the path element + * Return the number of wildcard elements (*, ?) in the path element. */ public int getWildcardCount() { return 0; } /** - * @return the score for this PathElement, combined score is used to compare parsed patterns. + * Return the score for this PathElement, combined score is used to compare parsed patterns. */ public int getScore() { return 0; } /** - * @return true if there is no next character, or if there is then it is a separator + * Return {@code true} if there is no next character, or if there is then it is a separator. */ protected boolean nextIfExistsIsSeparator(int nextIndex, MatchingContext matchingContext) { return (nextIndex >= matchingContext.candidateLength || - matchingContext.candidate[nextIndex] == separator); + matchingContext.candidate[nextIndex] == this.separator); } -} \ No newline at end of file + +} diff --git a/spring-web/src/main/java/org/springframework/web/util/patterns/PathPattern.java b/spring-web/src/main/java/org/springframework/web/util/pattern/PathPattern.java similarity index 72% rename from spring-web/src/main/java/org/springframework/web/util/patterns/PathPattern.java rename to spring-web/src/main/java/org/springframework/web/util/pattern/PathPattern.java index d31868057b3..9c17ce67966 100644 --- a/spring-web/src/main/java/org/springframework/web/util/patterns/PathPattern.java +++ b/spring-web/src/main/java/org/springframework/web/util/pattern/PathPattern.java @@ -14,14 +14,15 @@ * limitations under the License. */ -package org.springframework.web.util.patterns; +package org.springframework.web.util.pattern; import java.util.Collections; import java.util.HashMap; import java.util.Map; import org.springframework.util.PathMatcher; -import static org.springframework.util.StringUtils.hasLength; + +import static org.springframework.util.StringUtils.*; /** * Represents a parsed path pattern. Includes a chain of path elements @@ -62,8 +63,6 @@ import static org.springframework.util.StringUtils.hasLength; */ public class PathPattern implements Comparable { - private final static Map NO_VARIABLES_MAP = Collections.emptyMap(); - /** First path element in the parsed chain of path elements for this pattern */ private PathElement head; @@ -88,12 +87,12 @@ public class PathPattern implements Comparable { * your variable name lengths isn't going to change the length of the active part of the pattern. * Useful when comparing two patterns. */ - int normalizedLength; + private int normalizedLength; /** * Does the pattern end with '<separator>*' */ - boolean endsWithSeparatorWildcard = false; + private boolean endsWithSeparatorWildcard = false; /** * Score is used to quickly compare patterns. Different pattern components are given different @@ -106,41 +105,58 @@ public class PathPattern implements Comparable { private int score; /** Does the pattern end with {*...} */ - private boolean isCatchAll = false; + private boolean catchAll = false; + + + PathPattern(String patternText, PathElement head, char separator, boolean caseSensitive, + boolean allowOptionalTrailingSlash) { - public PathPattern(String patternText, PathElement head, char separator, boolean caseSensitive, boolean allowOptionalTrailingSlash) { - this.head = head; this.patternString = patternText; + this.head = head; this.separator = separator; this.caseSensitive = caseSensitive; this.allowOptionalTrailingSlash = allowOptionalTrailingSlash; + // Compute fields for fast comparison - PathElement s = head; - while (s != null) { - this.capturedVariableCount += s.getCaptureCount(); - this.normalizedLength += s.getNormalizedLength(); - this.score += s.getScore(); - if (s instanceof CaptureTheRestPathElement || s instanceof WildcardTheRestPathElement) { - this.isCatchAll = true; + PathElement elem = head; + while (elem != null) { + this.capturedVariableCount += elem.getCaptureCount(); + this.normalizedLength += elem.getNormalizedLength(); + this.score += elem.getScore(); + if (elem instanceof CaptureTheRestPathElement || elem instanceof WildcardTheRestPathElement) { + this.catchAll = true; } - if (s instanceof SeparatorPathElement && s.next != null - && s.next instanceof WildcardPathElement && s.next.next == null) { + if (elem instanceof SeparatorPathElement && elem.next != null && + elem.next instanceof WildcardPathElement && elem.next.next == null) { this.endsWithSeparatorWildcard = true; } - s = s.next; + elem = elem.next; } } + + /** + * Return the original pattern string that was parsed to create this PathPattern. + */ + public String getPatternString() { + return this.patternString; + } + + PathElement getHeadSection() { + return this.head; + } + + /** * @param path the candidate path to attempt to match against this pattern * @return true if the path matches this pattern */ public boolean matches(String path) { - if (head == null) { + if (this.head == null) { return !hasLength(path); } else if (!hasLength(path)) { - if (head instanceof WildcardTheRestPathElement || head instanceof CaptureTheRestPathElement) { + if (this.head instanceof WildcardTheRestPathElement || this.head instanceof CaptureTheRestPathElement) { path = ""; // Will allow CaptureTheRest to bind the variable to empty } else { @@ -148,31 +164,31 @@ public class PathPattern implements Comparable { } } MatchingContext matchingContext = new MatchingContext(path, false); - return head.matches(0, matchingContext); + return this.head.matches(0, matchingContext); } /** * For a given path return the remaining piece that is not covered by this PathPattern. - * * @param path a path that may or may not match this path pattern - * @return a {@link PathRemainingMatchInfo} describing the match result or null if the path does not match - * this pattern + * @return a {@link PathRemainingMatchInfo} describing the match result or null if + * the path does not match this pattern */ public PathRemainingMatchInfo getPathRemaining(String path) { - if (head == null) { + if (this.head == null) { if (path == null) { - return new PathRemainingMatchInfo(path); + return new PathRemainingMatchInfo(null); } else { - return new PathRemainingMatchInfo(hasLength(path)?path:""); + return new PathRemainingMatchInfo(hasLength(path) ? path : ""); } } else if (!hasLength(path)) { return null; } + MatchingContext matchingContext = new MatchingContext(path, true); matchingContext.setMatchAllowExtraPath(); - boolean matches = head.matches(0, matchingContext); + boolean matches = this.head.matches(0, matchingContext); if (!matches) { return null; } @@ -194,7 +210,7 @@ public class PathPattern implements Comparable { * @return true if the pattern matches as much of the path as is supplied */ public boolean matchStart(String path) { - if (head == null) { + if (this.head == null) { return !hasLength(path); } else if (!hasLength(path)) { @@ -202,7 +218,7 @@ public class PathPattern implements Comparable { } MatchingContext matchingContext = new MatchingContext(path, false); matchingContext.setMatchStartMatching(true); - return head.matches(0, matchingContext); + return this.head.matches(0, matchingContext); } /** @@ -212,31 +228,19 @@ public class PathPattern implements Comparable { */ public Map matchAndExtract(String path) { MatchingContext matchingContext = new MatchingContext(path, true); - if (head != null && head.matches(0, matchingContext)) { + if (this.head != null && this.head.matches(0, matchingContext)) { return matchingContext.getExtractedVariables(); } else { if (!hasLength(path)) { - return NO_VARIABLES_MAP; + return Collections.emptyMap(); } else { - throw new IllegalStateException("Pattern \"" + this.toString() - + "\" is not a match for \"" + path + "\""); + throw new IllegalStateException("Pattern \"" + this + "\" is not a match for \"" + path + "\""); } } } - /** - * @return the original pattern string that was parsed to create this PathPattern - */ - public String getPatternString() { - return patternString; - } - - public PathElement getHeadSection() { - return head; - } - /** * Given a full path, determine the pattern-mapped part.

For example: