From f58ffad9391a48a9d0d0815baea5171a1c840df2 Mon Sep 17 00:00:00 2001 From: Andy Clement Date: Tue, 11 Oct 2016 15:14:08 -0700 Subject: [PATCH] Introduce PathPatternParser for optimized path matching This commit introduces a PathPatternParser which parses request pattern strings into PathPattern objects which can then be used to fast match incoming string paths. The parser and matching supports the syntax as described in SPR-14544. The code is optimized around the common usages of request patterns and is designed to create very little transient garbage when matching. Issue: SPR-14544 --- .../cors/UrlBasedCorsConfigurationSource.java | 4 +- .../web/util/ParsingPathMatcher.java | 115 +++ .../patterns/CaptureTheRestPathElement.java | 77 ++ .../patterns/CaptureVariablePathElement.java | 121 +++ .../web/util/patterns/LiteralPathElement.java | 87 ++ .../web/util/patterns/PathElement.java | 88 ++ .../web/util/patterns/PathPattern.java | 462 +++++++++ .../util/patterns/PathPatternComparator.java | 38 + .../web/util/patterns/PathPatternParser.java | 390 ++++++++ .../PatternComparatorConsideringPath.java | 50 + .../web/util/patterns/PatternMessage.java | 52 + .../util/patterns/PatternParseException.java | 86 ++ .../web/util/patterns/RegexPathElement.java | 174 ++++ .../util/patterns/SeparatorPathElement.java | 74 ++ .../SingleCharWildcardedPathElement.java | 98 ++ .../web/util/patterns/SubSequence.java | 55 ++ .../util/patterns/WildcardPathElement.java | 69 ++ .../patterns/WildcardTheRestPathElement.java | 54 ++ .../patterns/PathPatternMatcherTests.java | 905 ++++++++++++++++++ .../util/patterns/PathPatternParserTests.java | 465 +++++++++ .../function/server/RequestPredicates.java | 4 +- .../handler/AbstractHandlerMapping.java | 4 +- .../resource/ResourceUrlProvider.java | 4 +- .../condition/PatternsRequestCondition.java | 4 +- .../handler/SimpleUrlHandlerMappingTests.java | 2 +- .../PatternsRequestConditionTests.java | 4 +- .../method/HandlerMethodMappingTests.java | 4 +- .../web/reactive/handler/map.xml | 21 +- .../web/servlet/ResourceServlet.java | 345 +++++++ .../WebMvcConfigurationSupport.java | 6 +- .../handler/AbstractHandlerMapping.java | 4 +- .../servlet/mvc/WebContentInterceptor.java | 4 +- .../condition/PatternsRequestCondition.java | 4 +- .../annotation/MvcUriComponentsBuilder.java | 4 +- .../servlet/resource/ResourceUrlProvider.java | 4 +- .../annotation/InterceptorRegistryTests.java | 3 +- .../WebMvcConfigurationSupportTests.java | 3 +- .../handler/HandlerMethodMappingTests.java | 3 +- .../PathMatchingUrlHandlerMappingTests.java | 7 +- .../mvc/UrlFilenameViewControllerTests.java | 4 +- .../mvc/WebContentInterceptorTests.java | 16 +- ...nnotationControllerHandlerMethodTests.java | 10 +- ...nnotationControllerHandlerMethodTests.java | 6 +- .../web/servlet/handler/map3.xml | 19 +- 44 files changed, 3880 insertions(+), 73 deletions(-) create mode 100644 spring-web/src/main/java/org/springframework/web/util/ParsingPathMatcher.java create mode 100644 spring-web/src/main/java/org/springframework/web/util/patterns/CaptureTheRestPathElement.java create mode 100644 spring-web/src/main/java/org/springframework/web/util/patterns/CaptureVariablePathElement.java create mode 100644 spring-web/src/main/java/org/springframework/web/util/patterns/LiteralPathElement.java create mode 100644 spring-web/src/main/java/org/springframework/web/util/patterns/PathElement.java create mode 100644 spring-web/src/main/java/org/springframework/web/util/patterns/PathPattern.java create mode 100644 spring-web/src/main/java/org/springframework/web/util/patterns/PathPatternComparator.java create mode 100644 spring-web/src/main/java/org/springframework/web/util/patterns/PathPatternParser.java create mode 100644 spring-web/src/main/java/org/springframework/web/util/patterns/PatternComparatorConsideringPath.java create mode 100644 spring-web/src/main/java/org/springframework/web/util/patterns/PatternMessage.java create mode 100644 spring-web/src/main/java/org/springframework/web/util/patterns/PatternParseException.java create mode 100644 spring-web/src/main/java/org/springframework/web/util/patterns/RegexPathElement.java create mode 100644 spring-web/src/main/java/org/springframework/web/util/patterns/SeparatorPathElement.java create mode 100644 spring-web/src/main/java/org/springframework/web/util/patterns/SingleCharWildcardedPathElement.java create mode 100644 spring-web/src/main/java/org/springframework/web/util/patterns/SubSequence.java create mode 100644 spring-web/src/main/java/org/springframework/web/util/patterns/WildcardPathElement.java create mode 100644 spring-web/src/main/java/org/springframework/web/util/patterns/WildcardTheRestPathElement.java create mode 100644 spring-web/src/test/java/org/springframework/web/util/patterns/PathPatternMatcherTests.java create mode 100644 spring-web/src/test/java/org/springframework/web/util/patterns/PathPatternParserTests.java create mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/ResourceServlet.java diff --git a/spring-web/src/main/java/org/springframework/web/cors/UrlBasedCorsConfigurationSource.java b/spring-web/src/main/java/org/springframework/web/cors/UrlBasedCorsConfigurationSource.java index 0d4eb11406a..a5843aaf8e0 100644 --- a/spring-web/src/main/java/org/springframework/web/cors/UrlBasedCorsConfigurationSource.java +++ b/spring-web/src/main/java/org/springframework/web/cors/UrlBasedCorsConfigurationSource.java @@ -21,9 +21,9 @@ import java.util.LinkedHashMap; import java.util.Map; import javax.servlet.http.HttpServletRequest; -import org.springframework.util.AntPathMatcher; import org.springframework.util.Assert; import org.springframework.util.PathMatcher; +import org.springframework.web.util.ParsingPathMatcher; import org.springframework.web.util.UrlPathHelper; /** @@ -40,7 +40,7 @@ public class UrlBasedCorsConfigurationSource implements CorsConfigurationSource private final Map corsConfigurations = new LinkedHashMap<>(); - private PathMatcher pathMatcher = new AntPathMatcher(); + private PathMatcher pathMatcher = new ParsingPathMatcher(); private UrlPathHelper urlPathHelper = new UrlPathHelper(); diff --git a/spring-web/src/main/java/org/springframework/web/util/ParsingPathMatcher.java b/spring-web/src/main/java/org/springframework/web/util/ParsingPathMatcher.java new file mode 100644 index 00000000000..fff0d2af733 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/util/ParsingPathMatcher.java @@ -0,0 +1,115 @@ +/* + * Copyright 2002-2016 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; + +import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; + +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; + + +/** + * + * @author Andy Clement + * @since 5.0 + */ +public class ParsingPathMatcher implements PathMatcher { + + Map cache = new HashMap<>(); + + PathPatternParser parser; + + public ParsingPathMatcher() { + parser = new PathPatternParser(); + } + + @Override + public boolean match(String pattern, String path) { + PathPattern p = getPathPattern(pattern); + return p.matches(path); + } + + @Override + public boolean matchStart(String pattern, String path) { + PathPattern p = getPathPattern(pattern); + return p.matchStart(path); + } + + @Override + public String extractPathWithinPattern(String pattern, String path) { + PathPattern p = getPathPattern(pattern); + return p.extractPathWithinPattern(path); + } + + @Override + public Map extractUriTemplateVariables(String pattern, String path) { + PathPattern p = getPathPattern(pattern); + return p.matchAndExtract(path); + } + + @Override + public String combine(String pattern1, String pattern2) { + PathPattern pathPattern = getPathPattern(pattern1); + return pathPattern.combine(pattern2); + } + + @Override + public Comparator getPatternComparator(String path) { + return new PathPatternStringComparatorConsideringPath(path); + } + + class PathPatternStringComparatorConsideringPath implements Comparator { + + 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 -1; + } + PathPattern p1 = getPathPattern(o1); + PathPattern p2 = getPathPattern(o2); + return 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 = cache.get(pattern); + if (pathPattern == null) { + pathPattern = parser.parse(pattern); + cache.put(pattern, pathPattern); + } + return pathPattern; + } + +} 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/patterns/CaptureTheRestPathElement.java new file mode 100644 index 00000000000..ff2076abb80 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/util/patterns/CaptureTheRestPathElement.java @@ -0,0 +1,77 @@ +/* + * Copyright 2016 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 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; + + /** + * @param pos + * @param captureDescriptor a character array containing contents like '{' '*' 'a' 'b' '}' + * @param separator the separator ahead of this construct + */ + CaptureTheRestPathElement(int pos, char[] captureDescriptor, char separator) { + super(pos); + variableName = new String(captureDescriptor, 2, captureDescriptor.length - 3); + this.separator = separator; + } + + @Override + public boolean matches(int candidateIndex, MatchingContext matchingContext) { + // No need to handle 'match start' checking as this captures everything + // anyway and cannot be followed by anything else + // assert next == null + while ((candidateIndex+1) matchingContext.candidateLength) { + return false; // not enough data, cannot be a match + } + if (caseSensitive) { + for (int i = 0; i < len; i++) { + if (matchingContext.candidate[candidateIndex++] != text[i]) { + return false; + } + } + } 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]) { + return false; + } + } + } + if (next == null) { + return candidateIndex == matchingContext.candidateLength; + } else { + if (matchingContext.isMatchStartMatching && candidateIndex == matchingContext.candidateLength) { + return true; // no more data but everything matched so far + } + return next.matches(candidateIndex, matchingContext); + } + } + + @Override + public int getNormalizedLength() { + return len; + } + + public String toString() { + return "Literal(" + new String(text) + ")"; + } + +} \ No newline at end of file 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/patterns/PathElement.java new file mode 100644 index 00000000000..29fb5f492aa --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/util/patterns/PathElement.java @@ -0,0 +1,88 @@ +/* + * Copyright 2016 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 org.springframework.web.util.patterns.PathPattern.MatchingContext; + +/** + * Common supertype for the Ast nodes created to represent a path pattern. + * + * @author Andy Clement + */ +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 + */ + protected PathElement prev; + + /** + * Create a new path element. + * @param pos the position where this path element starts in the pattern data + */ + PathElement(int pos) { + this.pos = pos; + } + + /** + * 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 + */ + public abstract boolean matches(int candidatePos, MatchingContext matchingContext); + + /** + * @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 + */ + public int getCaptureCount() { + return 0; + } + + /** + * @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. + */ + public int getScore() { + return 0; + } +} \ 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/patterns/PathPattern.java new file mode 100644 index 00000000000..65738470f9e --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/util/patterns/PathPattern.java @@ -0,0 +1,462 @@ +/* + * Copyright 2016 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.Collections; +import java.util.HashMap; +import java.util.Map; + +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. + * + * @author Andy Clement + */ +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; + + /** The text of the parsed pattern */ + private String patternString; + + /** The separator used when parsing the pattern */ + private char separator; + + /** Will this match candidates in a case sensitive way? (case sensitivity at parse time) */ + private boolean caseSensitive; + + /** How many variables are captured in this pattern */ + private int capturedVariableCount; + + /** + * The normalized length is trying to measure the 'active' part of the pattern. It is computed + * by assuming all captured variables have a normalized length of 1. Effectively this means changing + * 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; + + /** + * Does the pattern end with '<separator>*' + */ + boolean endsWithSeparatorWildcard = false; + + /** + * Score is used to quickly compare patterns. Different pattern components are given different + * weights. A 'lower score' is more specific. Current weights: + *
    + *
  • Captured variables are worth 1 + *
  • Wildcard is worth 100 + *
+ */ + private int score; + + /** Does the pattern end with {*...} */ + private boolean isCatchAll = false; + + public PathPattern(String patternText, PathElement head, char separator, boolean caseSensitive) { + this.head = head; + this.patternString = patternText; + this.separator = separator; + this.caseSensitive = caseSensitive; + // 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; + } + if (s instanceof SeparatorPathElement && s.next!=null && s.next instanceof WildcardPathElement && s.next.next == null) { + this.endsWithSeparatorWildcard=true; + } + s = s.next; + } + } + + /** + * @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) { + return (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 { + return 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 true; + } + MatchingContext matchingContext = new MatchingContext(path,false); + matchingContext.setMatchStartMatching(true); + return head.matches(0, matchingContext); + } + + /** + * @param path a path to match against this pattern + * @return a map of extracted variables - an empty map if no variables extracted. + */ + public Map matchAndExtract(String path) { + MatchingContext matchingContext = new MatchingContext(path,true); + if (head != null && head.matches(0, matchingContext)) { + return matchingContext.getExtractedVariables(); + } else { + if (path== null || path.length()==0) { + return NO_VARIABLES_MAP; + } else { + throw new IllegalStateException("Pattern \"" + this.toString() + "\" 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:

    + *
  • '{@code /docs/cvs/commit.html}' and '{@code /docs/cvs/commit.html} -> ''
  • + *
  • '{@code /docs/*}' and '{@code /docs/cvs/commit} -> '{@code cvs/commit}'
  • + *
  • '{@code /docs/cvs/*.html}' and '{@code /docs/cvs/commit.html} -> '{@code commit.html}'
  • + *
  • '{@code /docs/**}' and '{@code /docs/cvs/commit} -> '{@code cvs/commit}'
  • + *
+ *

Note: Assumes that {@link #matches} returns {@code true} for '{@code pattern}' and '{@code path}', but + * does not enforce this. As per the contract on {@link PathMatcher}, this + * method will trim leading/trailing separators. It will also remove duplicate separators in + * 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) { + separatorCount++; + } + if (s.getWildcardCount()!=0 || s.getCaptureCount()!=0) { + break; + } + s = s.next; + } + if (s == null) { + return ""; // There is no pattern mapped section + } + // Now separatorCount indicates how many sections of the path to skip + char[] pathChars = path.toCharArray(); + int len = pathChars.length; + int pos = 0; + while (separatorCount > 0 && pos < len) { + if (path.charAt(pos++) == separator) { + // Skip any adjacent separators + while (path.charAt(pos) == separator) { + pos++; + } + separatorCount--; + } + } + int end = len; + // Trim trailing separators + while (path.charAt(end-1) == separator) { + end--; + } + // Check if multiple separators embedded in the resulting path, if so trim them out. + // Example: aaa////bbb//ccc/d -> aaa/bbb/ccc/d + // The stringWithDuplicateSeparatorsRemoved is only computed if necessary + int c = pos; + StringBuilder stringWithDuplicateSeparatorsRemoved = null; + while (c extractedVariables; + + public boolean extractingVariables; + + public MatchingContext(String path, boolean extractVariables) { + candidate = path.toCharArray(); + candidateLength = candidate.length; + this.extractingVariables = extractVariables; + } + + public void setMatchStartMatching(boolean b) { + isMatchStartMatching = b; + } + + public void set(String key, String value) { + if (this.extractedVariables == null) { + extractedVariables = new HashMap<>(); + } + extractedVariables.put(key, value); + } + + public Map getExtractedVariables() { + if (this.extractedVariables == null) { + return NO_VARIABLES_MAP; + } else { + return this.extractedVariables; + } + } + + /** + * 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 + */ + public int scanAhead(int pos) { + while (pos < candidateLength) { + if (candidate[pos] == separator) { + return pos; + } + pos++; + } + return candidateLength; + } + } + + /** + * Combine this pattern with another. Currently does not produce a new PathPattern, just produces a new string. + */ + 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) { + return ""; + } else { + return pattern2string; + } + } 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)) { + return pattern2string; + } + + // /hotels/* + /booking => /hotels/booking + // /hotels/* + booking => /hotels/booking + if (endsWithSeparatorWildcard) { + 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); + } + + // /*.html + /hotel => /hotel.html + // /*.html + /hotel.* => /hotel.html + 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)); + 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); + } + + /** + * 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; + if (path1EndsWithSeparator && path2StartsWithSeparator) { + return path1 + path2.substring(1); + } + else if (path1EndsWithSeparator || path2StartsWithSeparator) { + return path1 + path2; + } + else { + return path1 + separator + path2; + } + } + +} \ No newline at end of file diff --git a/spring-web/src/main/java/org/springframework/web/util/patterns/PathPatternComparator.java b/spring-web/src/main/java/org/springframework/web/util/patterns/PathPatternComparator.java new file mode 100644 index 00000000000..167774dba1b --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/util/patterns/PathPatternComparator.java @@ -0,0 +1,38 @@ +/* + * Copyright 2016 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.Comparator; + +/** + * Basic PathPattern comparator. + * + * @author Andy Clement + */ +public class PathPatternComparator implements Comparator { + + @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; + } + return o1.compareTo(o2); + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/util/patterns/PathPatternParser.java b/spring-web/src/main/java/org/springframework/web/util/patterns/PathPatternParser.java new file mode 100644 index 00000000000..31317e267a4 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/util/patterns/PathPatternParser.java @@ -0,0 +1,390 @@ +/* + * Copyright 2016 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 + * path elements in a linked list. + * + * @author Andy Clement + */ +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 + 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 capturedVariableNames; + + // The head of the path element chain currently being built + PathElement headPE; + + // The most recently constructed path element in the chain + PathElement currentPE; + + /** + * Default constructor, will use the default path separator to identify + * the elements of the path pattern. + */ + 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. + */ + public PathPatternParser(char separator) { + this.separator = separator; + } + + public void setCaseSensitive(boolean caseSensitive) { + 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 }. + *

+ * Nested {...} pairs don't have to be escaped: /abc/{var:x{1,2}}/def + *

An escaped } will not be treated as the end of the regex: /abc/{var:x\\{y:}/def + *

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(); + } + if (capturedVariableNames.contains(variableName)) { + throw new PatternParseException(pos, this.pathPatternData, PatternMessage.ILLEGAL_DOUBLE_CAPTURE, variableName); + } + capturedVariableNames.add(variableName); + } +} \ No newline at end of file diff --git a/spring-web/src/main/java/org/springframework/web/util/patterns/PatternComparatorConsideringPath.java b/spring-web/src/main/java/org/springframework/web/util/patterns/PatternComparatorConsideringPath.java new file mode 100644 index 00000000000..06fb69251cf --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/util/patterns/PatternComparatorConsideringPath.java @@ -0,0 +1,50 @@ +/* + * Copyright 2016 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.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 + */ +public class PatternComparatorConsideringPath implements Comparator { + + private 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(path)) { + return (o2.getPatternString().equals(path))?0:-1; + } else if (o2.getPatternString().equals(path)) { + return +1; + } + return o1.compareTo(o2); + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/util/patterns/PatternMessage.java b/spring-web/src/main/java/org/springframework/web/util/patterns/PatternMessage.java new file mode 100644 index 00000000000..a3b88365339 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/util/patterns/PatternMessage.java @@ -0,0 +1,52 @@ +/* + * Copyright 2016 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.text.MessageFormat; + +/** + * The messages that can be included in a {@link PatternParseException} when there is a parse failure. + * + * @author Andy Clement + */ +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'{'"), + 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"), + ILLEGAL_CHARACTER_IN_CAPTURE_DESCRIPTOR("Character ''{0}'' is not allowed in a captured variable name"), + NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST("No more pattern data allowed after '{*...}' pattern element"), + BADLY_FORMED_CAPTURE_THE_REST("Expected form when capturing the rest of the path is simply '{*...}'"), + MISSING_REGEX_CONSTRAINT("Missing regex constraint on capture"), + ILLEGAL_DOUBLE_CAPTURE("Not allowed to capture ''{0}'' twice in the same pattern"), + JDK_PATTERN_SYNTAX_EXCEPTION("Exception occurred in pattern compilation"), + CAPTURE_ALL_IS_STANDALONE_CONSTRUCT("'{*...}' can only be preceeded by a path separator"); + // @formatter:on + + private final String message; + + private PatternMessage(String message) { + this.message = message; + } + + public String formatMessage(Object... inserts) { + return MessageFormat.format(this.message, inserts); + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/util/patterns/PatternParseException.java b/spring-web/src/main/java/org/springframework/web/util/patterns/PatternParseException.java new file mode 100644 index 00000000000..7e40476c043 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/util/patterns/PatternParseException.java @@ -0,0 +1,86 @@ +/* + * Copyright 2016 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; + +/** + * Exception that is thrown when there is a problem with the pattern being parsed. + * + * @author Andy Clement + */ +public class PatternParseException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + private int pos; + + private char[] patternText; + + private final PatternMessage message; + + private final Object[] inserts; + + public PatternParseException(int pos, char[] patternText, PatternMessage message, Object... inserts) { + super(message.formatMessage(inserts)); + this.pos = pos; + this.patternText = patternText; + this.message = message; + this.inserts = inserts; + } + + public PatternParseException(Throwable cause, int pos, char[] patternText, PatternMessage message, Object... inserts) { + super(message.formatMessage(inserts),cause); + this.pos = pos; + this.patternText = patternText; + this.message = message; + this.inserts = inserts; + } + + /** + * @return a formatted message with inserts applied + */ + @Override + public String getMessage() { + return this.message.formatMessage(this.inserts); + } + + /** + * @return a detailed message that includes the original pattern text with a pointer to the error position, + * as well as the error message. + */ + public String toDetailedString() { + StringBuilder buf = new StringBuilder(); + buf.append(patternText).append('\n'); + for (int i = 0; i < pos; i++) { + buf.append(' '); + } + buf.append("^\n"); + buf.append(getMessage()); + return buf.toString(); + } + + public Object[] getInserts() { + return this.inserts; + } + + public int getPosition() { + return pos; + } + + public PatternMessage getMessageType() { + return message; + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/util/patterns/RegexPathElement.java b/spring-web/src/main/java/org/springframework/web/util/patterns/RegexPathElement.java new file mode 100644 index 00000000000..a1647189052 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/util/patterns/RegexPathElement.java @@ -0,0 +1,174 @@ +/* + * Copyright 2016 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.LinkedList; +import java.util.List; +import java.util.regex.Matcher; + +import org.springframework.util.AntPathMatcher; +import org.springframework.web.util.patterns.PathPattern.MatchingContext; + +/** + * A regex path element. Used to represent any complicated element of the path. + * For example in '/foo/*_*/*_{foobar}' both *_* and *_{foobar} + * are {@link RegexPathElement} path elements. Derived from the general {@link AntPathMatcher} approach. + * + * @author Andy Clement + */ +class RegexPathElement extends PathElement { + + private final java.util.regex.Pattern GLOB_PATTERN = java.util.regex.Pattern + .compile("\\?|\\*|\\{((?:\\{[^/]+?\\}|[^/{}]|\\\\[{}])+?)\\}"); + + private final String DEFAULT_VARIABLE_PATTERN = "(.*)"; + + private final List variableNames = new LinkedList<>(); + + private char[] regex; + + private java.util.regex.Pattern pattern; + + private boolean caseSensitive; + + private int wildcardCount; + + RegexPathElement(int pos, char[] regex, boolean caseSensitive, char[] completePattern) { + super(pos); + this.regex = regex; + this.caseSensitive = caseSensitive; + buildPattern(regex, completePattern); + } + + public void buildPattern(char[] regex, char[] completePattern) { + StringBuilder patternBuilder = new StringBuilder(); + String text = new String(regex); + Matcher matcher = GLOB_PATTERN.matcher(text); + int end = 0; + while (matcher.find()) { + patternBuilder.append(quote(text, end, matcher.start())); + String match = matcher.group(); + if ("?".equals(match)) { + patternBuilder.append('.'); + } else if ("*".equals(match)) { + patternBuilder.append(".*"); + wildcardCount++; + } else if (match.startsWith("{") && match.endsWith("}")) { + int colonIdx = match.indexOf(':'); + if (colonIdx == -1) { + patternBuilder.append(DEFAULT_VARIABLE_PATTERN); + String variableName = matcher.group(1); + if (variableNames.contains(variableName)) { + throw new PatternParseException(pos, completePattern, PatternMessage.ILLEGAL_DOUBLE_CAPTURE, + variableName); + } + this.variableNames.add(variableName); + } else { + String variablePattern = match.substring(colonIdx + 1, match.length() - 1); + patternBuilder.append('('); + patternBuilder.append(variablePattern); + patternBuilder.append(')'); + String variableName = match.substring(1, colonIdx); + if (variableNames.contains(variableName)) { + throw new PatternParseException(pos, completePattern, PatternMessage.ILLEGAL_DOUBLE_CAPTURE, + variableName); + } + this.variableNames.add(variableName); + } + } + end = matcher.end(); + } + patternBuilder.append(quote(text, end, text.length())); + if (caseSensitive) { + pattern = java.util.regex.Pattern.compile(patternBuilder.toString()); + } else { + pattern = java.util.regex.Pattern.compile(patternBuilder.toString(), + java.util.regex.Pattern.CASE_INSENSITIVE); + } + } + + public List getVariableNames() { + return variableNames; + } + + private String quote(String s, int start, int end) { + if (start == end) { + return ""; + } + return java.util.regex.Pattern.quote(s.substring(start, end)); + } + + @Override + public boolean matches(int candidateIndex, MatchingContext matchingContext) { + int p = matchingContext.scanAhead(candidateIndex); + Matcher m = pattern.matcher(new SubSequence(matchingContext.candidate, candidateIndex, p)); + boolean matches = m.matches(); + if (matches) { + if (next == null) { + // No more pattern, is there more data? + matches = (p == matchingContext.candidateLength); + } else { + if (matchingContext.isMatchStartMatching && p == matchingContext.candidateLength) { + return true; // no more data but matches up to this point + } + matches = next.matches(p, matchingContext); + } + } + if (matches && matchingContext.extractingVariables) { + // Process captures + if (this.variableNames.size() != m.groupCount()) { // SPR-8455 + throw new IllegalArgumentException("The number of capturing groups in the pattern segment " + + this.pattern + " does not match the number of URI template variables it defines, " + + "which can occur if capturing groups are used in a URI template regex. " + + "Use non-capturing groups instead."); + } + for (int i = 1; i <= m.groupCount(); i++) { + String name = this.variableNames.get(i - 1); + String value = m.group(i); + matchingContext.set(name, value); + } + } + return matches; + } + + public String toString() { + return "Regex(" + new String(regex) + ")"; + } + + @Override + public int getNormalizedLength() { + int varsLength = 0; + for (String variableName : variableNames) { + varsLength += variableName.length(); + } + return regex.length - varsLength - variableNames.size(); + } + + public int getCaptureCount() { + return variableNames.size(); + } + + @Override + public int getWildcardCount() { + return wildcardCount; + } + + @Override + public int getScore() { + return getCaptureCount()*CAPTURE_VARIABLE_WEIGHT + getWildcardCount()*WILDCARD_WEIGHT; + } + +} \ No newline at end of file diff --git a/spring-web/src/main/java/org/springframework/web/util/patterns/SeparatorPathElement.java b/spring-web/src/main/java/org/springframework/web/util/patterns/SeparatorPathElement.java new file mode 100644 index 00000000000..d85a1cbb4e5 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/util/patterns/SeparatorPathElement.java @@ -0,0 +1,74 @@ +/* + * Copyright 2016 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 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 + */ +class SeparatorPathElement extends PathElement { + + private char separator; + + SeparatorPathElement(int pos, char separator) { + super(pos); + this.separator = separator; + } + + /** + * Matching a separator is easy, basically the character at candidateIndex + * must be the separator. + */ + @Override + public boolean matches(int candidateIndex, MatchingContext matchingContext) { + boolean matched = false; + if (candidateIndex < matchingContext.candidateLength) { + if (matchingContext.candidate[candidateIndex] == separator) { + // Skip further separators in the path (they are all 'matched' + // by a single SeparatorPathElement) + while ((candidateIndex+1) extracted = checkCapture("/abc","/abc"); + assertEquals(0,extracted.size()); + } + + @Test + public void extractUriTemplateVariablesRegex() { + PathPatternParser pp = new PathPatternParser(); + PathPattern p = null; + + p = pp.parse("{symbolicName:[\\w\\.]+}-{version:[\\w\\.]+}.jar"); + Map result = p.matchAndExtract("com.example-1.0.0.jar"); + assertEquals("com.example", result.get("symbolicName")); + assertEquals("1.0.0", result.get("version")); + + p = pp.parse("{symbolicName:[\\w\\.]+}-sources-{version:[\\w\\.]+}.jar"); + result = p.matchAndExtract("com.example-sources-1.0.0.jar"); + assertEquals("com.example", result.get("symbolicName")); + assertEquals("1.0.0", result.get("version")); + } + + @Test + public void extractUriTemplateVarsRegexQualifiers() { + PathPatternParser pp = new PathPatternParser(); + + PathPattern p = pp.parse("{symbolicName:[\\p{L}\\.]+}-sources-{version:[\\p{N}\\.]+}.jar"); + Map result = p.matchAndExtract("com.example-sources-1.0.0.jar"); + assertEquals("com.example", result.get("symbolicName")); + assertEquals("1.0.0", result.get("version")); + + p = pp.parse("{symbolicName:[\\w\\.]+}-sources-{version:[\\d\\.]+}-{year:\\d{4}}{month:\\d{2}}{day:\\d{2}}.jar"); + result = p.matchAndExtract("com.example-sources-1.0.0-20100220.jar"); + assertEquals("com.example", result.get("symbolicName")); + assertEquals("1.0.0", result.get("version")); + assertEquals("2010", result.get("year")); + assertEquals("02", result.get("month")); + assertEquals("20", result.get("day")); + + p = pp.parse("{symbolicName:[\\p{L}\\.]+}-sources-{version:[\\p{N}\\.\\{\\}]+}.jar"); + result = p.matchAndExtract("com.example-sources-1.0.0.{12}.jar"); + assertEquals("com.example", result.get("symbolicName")); + assertEquals("1.0.0.{12}", result.get("version")); + } + + @Test + public void extractUriTemplateVarsRegexCapturingGroups() { + PathPatternParser pp = new PathPatternParser(); + PathPattern pathMatcher = pp.parse("/web/{id:foo(bar)?}_{goo}"); + exception.expect(IllegalArgumentException.class); + exception.expectMessage(containsString("The number of capturing groups in the pattern")); + pathMatcher.matchAndExtract("/web/foobar_goo"); + } + + @Rule + public final ExpectedException exception = ExpectedException.none(); + + @Test + public void combine() { + TestPathCombiner pathMatcher = new TestPathCombiner(); + assertEquals("", pathMatcher.combine(null, null)); + assertEquals("/hotels", pathMatcher.combine("/hotels", null)); + assertEquals("/hotels", pathMatcher.combine(null, "/hotels")); + assertEquals("/hotels/booking", pathMatcher.combine("/hotels/*", "booking")); + assertEquals("/hotels/booking", pathMatcher.combine("/hotels/*", "/booking")); + assertEquals("/hotels/**/booking", pathMatcher.combine("/hotels/**", "booking")); + assertEquals("/hotels/**/booking", pathMatcher.combine("/hotels/**", "/booking")); + assertEquals("/hotels/booking", pathMatcher.combine("/hotels", "/booking")); + assertEquals("/hotels/booking", pathMatcher.combine("/hotels", "booking")); + assertEquals("/hotels/booking", pathMatcher.combine("/hotels/", "booking")); + assertEquals("/hotels/{hotel}", pathMatcher.combine("/hotels/*", "{hotel}")); + assertEquals("/hotels/**/{hotel}", pathMatcher.combine("/hotels/**", "{hotel}")); + assertEquals("/hotels/{hotel}", pathMatcher.combine("/hotels", "{hotel}")); + assertEquals("/hotels/{hotel}.*", pathMatcher.combine("/hotels", "{hotel}.*")); + assertEquals("/hotels/*/booking/{booking}", + pathMatcher.combine("/hotels/*/booking", "{booking}")); + assertEquals("/hotel.html", pathMatcher.combine("/*.html", "/hotel.html")); + assertEquals("/hotel.html", pathMatcher.combine("/*.html", "/hotel")); + assertEquals("/hotel.html", pathMatcher.combine("/*.html", "/hotel.*")); + // TODO this seems rather bogus, should we eagerly show an error? + assertEquals("/d/e/f/hotel.html", pathMatcher.combine("/a/b/c/*.html", "/d/e/f/hotel.*")); + assertEquals("/*.html", pathMatcher.combine("/**", "/*.html")); + assertEquals("/*.html", pathMatcher.combine("/*", "/*.html")); + assertEquals("/*.html", pathMatcher.combine("/*.*", "/*.html")); + assertEquals("/{foo}/bar", pathMatcher.combine("/{foo}", "/bar")); // SPR-8858 + assertEquals("/user/user", pathMatcher.combine("/user", "/user")); // SPR-7970 + assertEquals("/{foo:.*[^0-9].*}/edit/", + pathMatcher.combine("/{foo:.*[^0-9].*}", "/edit/")); // SPR-10062 + assertEquals("/1.0/foo/test", pathMatcher.combine("/1.0", "/foo/test")); + // SPR-10554 + assertEquals("/hotel", pathMatcher.combine("/", "/hotel")); // SPR-12975 + assertEquals("/hotel/booking", pathMatcher.combine("/hotel/", "/booking")); // SPR-12975 + assertEquals("",pathMatcher.combine(null, null)); + assertEquals("",pathMatcher.combine(null, "")); + assertEquals("",pathMatcher.combine("",null)); + assertEquals("",pathMatcher.combine(null, null)); + assertEquals("",pathMatcher.combine("", "")); + assertEquals("/hotel",pathMatcher.combine("", "/hotel")); + assertEquals("/hotel",pathMatcher.combine("/hotel", null)); + assertEquals("/hotel",pathMatcher.combine("/hotel", "")); + // TODO Do we need special handling when patterns contain multiple dots? + } + + @Test + public void combineWithTwoFileExtensionPatterns() { + TestPathCombiner pathMatcher = new TestPathCombiner(); + exception.expect(IllegalArgumentException.class); + pathMatcher.combine("/*.html", "/*.txt"); + } + + @Test + public void patternComparator() { + Comparator comparator = new PatternComparatorConsideringPath( + "/hotels/new"); + + assertEquals(0, comparator.compare(null, null)); + assertEquals(1, comparator.compare(null, parse("/hotels/new"))); + assertEquals(-1, comparator.compare(parse("/hotels/new"), null)); + + assertEquals(0, comparator.compare(parse("/hotels/new"), parse("/hotels/new"))); + + assertEquals(-1, comparator.compare(parse("/hotels/new"), parse("/hotels/*"))); + assertEquals(1, comparator.compare(parse("/hotels/*"), parse("/hotels/new"))); + assertEquals(0, comparator.compare(parse("/hotels/*"), parse("/hotels/*"))); + + assertEquals(-1, + comparator.compare(parse("/hotels/new"), parse("/hotels/{hotel}"))); + assertEquals(1, + comparator.compare(parse("/hotels/{hotel}"), parse("/hotels/new"))); + assertEquals(0, + comparator.compare(parse("/hotels/{hotel}"), parse("/hotels/{hotel}"))); + assertEquals(-1, comparator.compare(parse("/hotels/{hotel}/booking"), + parse("/hotels/{hotel}/bookings/{booking}"))); + assertEquals(1, comparator.compare(parse("/hotels/{hotel}/bookings/{booking}"), + parse("/hotels/{hotel}/booking"))); + + assertEquals(-1, + comparator.compare( + parse("/hotels/{hotel}/bookings/{booking}/cutomers/{customer}"), + parse("/**"))); + assertEquals(1, comparator.compare(parse("/**"), + parse("/hotels/{hotel}/bookings/{booking}/cutomers/{customer}"))); + assertEquals(0, comparator.compare(parse("/**"), parse("/**"))); + + assertEquals(-1, + comparator.compare(parse("/hotels/{hotel}"), parse("/hotels/*"))); + assertEquals(1, comparator.compare(parse("/hotels/*"), parse("/hotels/{hotel}"))); + + assertEquals(-1, comparator.compare(parse("/hotels/*"), parse("/hotels/*/**"))); + assertEquals(1, comparator.compare(parse("/hotels/*/**"), parse("/hotels/*"))); + + assertEquals(-1, + comparator.compare(parse("/hotels/new"), parse("/hotels/new.*"))); + + // SPR-6741 + assertEquals(-1, + comparator.compare( + parse("/hotels/{hotel}/bookings/{booking}/cutomers/{customer}"), + parse("/hotels/**"))); + assertEquals(1, comparator.compare(parse("/hotels/**"), + parse("/hotels/{hotel}/bookings/{booking}/cutomers/{customer}"))); + assertEquals(1, comparator.compare(parse("/hotels/foo/bar/**"), + parse("/hotels/{hotel}"))); + assertEquals(-1, comparator.compare(parse("/hotels/{hotel}"), + parse("/hotels/foo/bar/**"))); + + // SPR-8683 + assertEquals(1, comparator.compare(parse("/**"), parse("/hotels/{hotel}"))); + + // longer is better + assertEquals(1, comparator.compare(parse("/hotels"), parse("/hotels2"))); + + // SPR-13139 + assertEquals(-1, comparator.compare(parse("*"), parse("*/**"))); + assertEquals(1, comparator.compare(parse("*/**"), parse("*"))); + } + + @Test + public void pathPatternComparator() { + PathPatternComparator ppc = new PathPatternComparator(); + assertEquals(0,ppc.compare(null, null)); + assertEquals(1,ppc.compare(null, parse(""))); + assertEquals(-1,ppc.compare(parse(""), null)); + assertEquals(0,ppc.compare(parse(""), parse(""))); + } + + @Test + public void patternCompareTo() { + PathPatternParser p = new PathPatternParser(); + PathPattern pp = p.parse("/abc"); + assertEquals(-1,pp.compareTo(null)); + } + + @Test + public void patternComparatorSort() { + Comparator comparator = new PatternComparatorConsideringPath( + "/hotels/new"); + List paths = new ArrayList<>(3); + PathPatternParser pp = new PathPatternParser(); + paths.add(null); + paths.add(pp.parse("/hotels/new")); + Collections.sort(paths, comparator); + assertEquals("/hotels/new", paths.get(0).getPatternString()); + assertNull(paths.get(1)); + paths.clear(); + + paths.add(pp.parse("/hotels/new")); + paths.add(null); + Collections.sort(paths, comparator); + assertEquals("/hotels/new", paths.get(0).getPatternString()); + assertNull(paths.get(1)); + paths.clear(); + + paths.add(pp.parse("/hotels/*")); + paths.add(pp.parse("/hotels/new")); + Collections.sort(paths, comparator); + assertEquals("/hotels/new", paths.get(0).getPatternString()); + assertEquals("/hotels/*", paths.get(1).getPatternString()); + paths.clear(); + + paths.add(pp.parse("/hotels/new")); + paths.add(pp.parse("/hotels/*")); + Collections.sort(paths, comparator); + assertEquals("/hotels/new", paths.get(0).getPatternString()); + assertEquals("/hotels/*", paths.get(1).getPatternString()); + paths.clear(); + + paths.add(pp.parse("/hotels/**")); + paths.add(pp.parse("/hotels/*")); + Collections.sort(paths, comparator); + assertEquals("/hotels/*", paths.get(0).getPatternString()); + assertEquals("/hotels/**", paths.get(1).getPatternString()); + paths.clear(); + + paths.add(pp.parse("/hotels/*")); + paths.add(pp.parse("/hotels/**")); + Collections.sort(paths, comparator); + assertEquals("/hotels/*", paths.get(0).getPatternString()); + assertEquals("/hotels/**", paths.get(1).getPatternString()); + paths.clear(); + + paths.add(pp.parse("/hotels/{hotel}")); + paths.add(pp.parse("/hotels/new")); + Collections.sort(paths, comparator); + assertEquals("/hotels/new", paths.get(0).getPatternString()); + assertEquals("/hotels/{hotel}", paths.get(1).getPatternString()); + paths.clear(); + + paths.add(pp.parse("/hotels/new")); + paths.add(pp.parse("/hotels/{hotel}")); + Collections.sort(paths, comparator); + assertEquals("/hotels/new", paths.get(0).getPatternString()); + assertEquals("/hotels/{hotel}", paths.get(1).getPatternString()); + paths.clear(); + + paths.add(pp.parse("/hotels/*")); + paths.add(pp.parse("/hotels/{hotel}")); + paths.add(pp.parse("/hotels/new")); + Collections.sort(paths, comparator); + assertEquals("/hotels/new", paths.get(0).getPatternString()); + assertEquals("/hotels/{hotel}", paths.get(1).getPatternString()); + assertEquals("/hotels/*", paths.get(2).getPatternString()); + paths.clear(); + + paths.add(pp.parse("/hotels/ne*")); + paths.add(pp.parse("/hotels/n*")); + Collections.shuffle(paths); + Collections.sort(paths, comparator); + assertEquals("/hotels/ne*", paths.get(0).getPatternString()); + assertEquals("/hotels/n*", paths.get(1).getPatternString()); + paths.clear(); + + // comparator = new PatternComparatorConsideringPath("/hotels/new.html"); + // paths.add(pp.parse("/hotels/new.*")); + // paths.add(pp.parse("/hotels/{hotel}")); + // Collections.shuffle(paths); + // Collections.sort(paths, comparator); + // assertEquals("/hotels/new.*", paths.get(0).toPatternString()); + // assertEquals("/hotels/{hotel}", paths.get(1).toPatternString()); + // paths.clear(); + + comparator = new PatternComparatorConsideringPath("/web/endUser/action/login.html"); + paths.add(pp.parse("/*/login.*")); + paths.add(pp.parse("/*/endUser/action/login.*")); + Collections.sort(paths, comparator); + assertEquals("/*/endUser/action/login.*", paths.get(0).getPatternString()); + assertEquals("/*/login.*", paths.get(1).getPatternString()); + paths.clear(); + } + + @Test // SPR-13286 + public void caseInsensitive() { + PathPatternParser pp = new PathPatternParser(); + pp.setCaseSensitive(false); + PathPattern p = pp.parse("/group/{groupName}/members"); + assertTrue(p.matches("/group/sales/members")); + assertTrue(p.matches("/Group/Sales/Members")); + assertTrue(p.matches("/group/Sales/members")); + } + + @Test + public void patternmessage() { + PatternMessage[] values = PatternMessage.values(); + assertNotNull(values); + for (PatternMessage pm: values) { + String name = pm.toString(); + assertEquals(pm.ordinal(),PatternMessage.valueOf(name).ordinal()); + } + } + + private PathPattern parse(String path) { + PathPatternParser pp = new PathPatternParser(); + return pp.parse(path); + } + + private char separator = PathPatternParser.DEFAULT_SEPARATOR; + + private void checkMatches(String uriTemplate, String path) { + PathPatternParser parser = (separator == PathPatternParser.DEFAULT_SEPARATOR + ? new PathPatternParser() : new PathPatternParser(separator)); + PathPattern p = parser.parse(uriTemplate); + assertTrue(p.matches(path)); + } + + private void checkStartNoMatch(String uriTemplate, String path) { + PathPatternParser p = new PathPatternParser(); + PathPattern pattern = p.parse(uriTemplate); + assertFalse(pattern.matchStart(path)); + } + + private void checkStartMatches(String uriTemplate, String path) { + PathPatternParser p = new PathPatternParser(); + PathPattern pattern = p.parse(uriTemplate); + assertTrue(pattern.matchStart(path)); + } + + private void checkNoMatch(String uriTemplate, String path) { + PathPatternParser p = new PathPatternParser(); + PathPattern pattern = p.parse(uriTemplate); + assertFalse(pattern.matches(path)); + } + + private Map checkCapture(String uriTemplate, String path, String... keyValues) { + PathPatternParser parser = new PathPatternParser(); + PathPattern pattern = parser.parse(uriTemplate); + Map matchResults = pattern.matchAndExtract(path); + Map expectedKeyValues = new HashMap<>(); + if (keyValues != null) { + for (int i = 0; i < keyValues.length; i += 2) { + expectedKeyValues.put(keyValues[i], keyValues[i + 1]); + } + } + Map capturedVariables = matchResults; + for (Map.Entry me : expectedKeyValues.entrySet()) { + String value = capturedVariables.get(me.getKey()); + if (value == null) { + fail("Did not find key '" + me.getKey() + "' in captured variables: " + + capturedVariables); + } + if (!value.equals(me.getValue())) { + fail("Expected value '" + me.getValue() + "' for key '" + me.getKey() + + "' but was '" + value + "'"); + } + } + return capturedVariables; + } + + private void checkExtractPathWithinPattern(String pattern, String path, String expected) { + PathPatternParser ppp = new PathPatternParser(); + PathPattern pp = ppp.parse(pattern); + String s = pp.extractPathWithinPattern(path); + assertEquals(expected,s); + } + + static class TestPathCombiner { + + PathPatternParser pp = new PathPatternParser(); + + public String combine(String string1, String string2) { + PathPattern pattern1 = pp.parse(string1); + return pattern1.combine(string2); + } + + } + +} diff --git a/spring-web/src/test/java/org/springframework/web/util/patterns/PathPatternParserTests.java b/spring-web/src/test/java/org/springframework/web/util/patterns/PathPatternParserTests.java new file mode 100644 index 00000000000..22bc6b4ac7c --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/util/patterns/PathPatternParserTests.java @@ -0,0 +1,465 @@ +/* + * Copyright 2016 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.Collections; +import java.util.List; +import java.util.Map; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Exercise the {@link PathPatternParser}. + * + * @author Andy Clement + */ +public class PathPatternParserTests { + + private PathPattern p; + + @Test + public void basicPatterns() { + checkStructure("/"); + checkStructure("/foo"); + checkStructure("foo"); + checkStructure("foo/"); + checkStructure("/foo/"); + checkStructure("//"); + } + + @Test + public void singleCharWildcardPatterns() { + p = checkStructure("?"); + assertPathElements(p , SingleCharWildcardedPathElement.class); + checkStructure("/?/"); + checkStructure("//?abc?/"); + } + + @Test + public void multiwildcardPattern() { + p = checkStructure("/**"); + assertPathElements(p,WildcardTheRestPathElement.class); + p = checkStructure("/**acb"); // this is not double wildcard use, it is / then **acb (an odd, unnecessary use of double *) + assertPathElements(p,SeparatorPathElement.class, RegexPathElement.class); + } + + @Test + public void toStringTests() { + assertEquals("CaptureTheRest(/{*foobar})", checkStructure("/{*foobar}").toChainString()); + assertEquals("CaptureVariable({foobar})", checkStructure("{foobar}").toChainString()); + assertEquals("Literal(abc)", checkStructure("abc").toChainString()); + assertEquals("Regex({a}_*_{b})", checkStructure("{a}_*_{b}").toChainString()); + assertEquals("Separator(/)", checkStructure("/").toChainString()); + assertEquals("SingleCharWildcarding(?a?b?c)", checkStructure("?a?b?c").toChainString()); + assertEquals("Wildcard(*)", checkStructure("*").toChainString()); + assertEquals("WildcardTheRest(/**)", checkStructure("/**").toChainString()); + } + + @Test + public void captureTheRestPatterns() { + checkError("/{*foobar}x{abc}", 10, PatternMessage.NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST); + p = checkStructure("{*foobar}"); + assertPathElements(p, CaptureTheRestPathElement.class); + p = checkStructure("/{*foobar}"); + assertPathElements(p, CaptureTheRestPathElement.class); + checkError("/{*foobar}/", 10, PatternMessage.NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST); + checkError("/{*foobar}abc", 10, PatternMessage.NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST); + checkError("/{*f%obar}", 4, PatternMessage.ILLEGAL_CHARACTER_IN_CAPTURE_DESCRIPTOR); + checkError("/{*foobar}abc",10,PatternMessage.NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST); + checkError("/{f*oobar}",3,PatternMessage.ILLEGAL_CHARACTER_IN_CAPTURE_DESCRIPTOR); + checkError("/{*foobar}/abc",10,PatternMessage.NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST); + checkError("/{abc}{*foobar}",1,PatternMessage.CAPTURE_ALL_IS_STANDALONE_CONSTRUCT); + checkError("/{abc}{*foobar}{foo}",15,PatternMessage.NO_MORE_DATA_EXPECTED_AFTER_CAPTURE_THE_REST); + } + + @Test + public void equalsAndHashcode() { + PathPatternParser caseInsensitiveParser = new PathPatternParser(); + caseInsensitiveParser.setCaseSensitive(false); + PathPatternParser caseSensitiveParser = new PathPatternParser(); + PathPattern pp1 = caseInsensitiveParser.parse("/abc"); + PathPattern pp2 = caseInsensitiveParser.parse("/abc"); + PathPattern pp3 = caseInsensitiveParser.parse("/def"); + assertEquals(pp1,pp2); + assertEquals(pp1.hashCode(),pp2.hashCode()); + assertNotEquals(pp1, pp3); + assertFalse(pp1.equals("abc")); + + pp1 = caseInsensitiveParser.parse("/abc"); + pp2 = caseSensitiveParser.parse("/abc"); + assertFalse(pp1.equals(pp2)); + assertNotEquals(pp1.hashCode(),pp2.hashCode()); + + PathPatternParser alternateSeparatorParser = new PathPatternParser(':'); + pp1 = caseInsensitiveParser.parse("abc"); + pp2 = alternateSeparatorParser.parse("abc"); + assertFalse(pp1.equals(pp2)); + assertNotEquals(pp1.hashCode(),pp2.hashCode()); + } + + @Test + public void regexPathElementPatterns() { + checkError("/{var:[^/]*}", 8, PatternMessage.MISSING_CLOSE_CAPTURE); + checkError("/{var:abc", 8, PatternMessage.MISSING_CLOSE_CAPTURE); + checkError("/{var:a{{1,2}}}", 6, PatternMessage.JDK_PATTERN_SYNTAX_EXCEPTION); + + p = checkStructure("/{var:\\\\}"); + assertEquals(CaptureVariablePathElement.class.getName(),p.getHeadSection().next.getClass().getName()); + assertTrue(p.matches("/\\")); + + p = checkStructure("/{var:\\/}"); + assertEquals(CaptureVariablePathElement.class.getName(),p.getHeadSection().next.getClass().getName()); + assertFalse(p.matches("/aaa")); + + p = checkStructure("/{var:a{1,2}}",1); + assertEquals(CaptureVariablePathElement.class.getName(),p.getHeadSection().next.getClass().getName()); + + p = checkStructure("/{var:[^\\/]*}",1); + assertEquals(CaptureVariablePathElement.class.getName(),p.getHeadSection().next.getClass().getName()); + Map result = p.matchAndExtract("/foo"); + assertEquals("foo",result.get("var")); + + p = checkStructure("/{var:\\[*}",1); + assertEquals(CaptureVariablePathElement.class.getName(),p.getHeadSection().next.getClass().getName()); + result = p.matchAndExtract("/[[["); + assertEquals("[[[",result.get("var")); + + p = checkStructure("/{var:[\\{]*}",1); + assertEquals(CaptureVariablePathElement.class.getName(),p.getHeadSection().next.getClass().getName()); + result = p.matchAndExtract("/{{{"); + assertEquals("{{{",result.get("var")); + + p = checkStructure("/{var:[\\}]*}",1); + assertEquals(CaptureVariablePathElement.class.getName(),p.getHeadSection().next.getClass().getName()); + result = p.matchAndExtract("/}}}"); + assertEquals("}}}",result.get("var")); + + p = checkStructure("*"); + assertEquals(WildcardPathElement.class.getName(),p.getHeadSection().getClass().getName()); + checkStructure("/*"); + checkStructure("/*/"); + checkStructure("*/"); + checkStructure("/*/"); + p = checkStructure("/*a*/"); + assertEquals(RegexPathElement.class.getName(),p.getHeadSection().next.getClass().getName()); + p = checkStructure("*/"); + assertEquals(WildcardPathElement.class.getName(),p.getHeadSection().getClass().getName()); + checkError("{foo}_{foo}", 0, PatternMessage.ILLEGAL_DOUBLE_CAPTURE, "foo"); + checkError("/{bar}/{bar}", 7, PatternMessage.ILLEGAL_DOUBLE_CAPTURE, "bar"); + checkError("/{bar}/{bar}_{foo}", 7, PatternMessage.ILLEGAL_DOUBLE_CAPTURE, "bar"); + + p = checkStructure("{symbolicName:[\\p{L}\\.]+}-sources-{version:[\\p{N}\\.]+}.jar"); + assertEquals(RegexPathElement.class.getName(),p.getHeadSection().getClass().getName()); + + + } + + @Test + public void completeCapturingPatterns() { + p = checkStructure("{foo}"); + assertEquals(CaptureVariablePathElement.class.getName(),p.getHeadSection().getClass().getName()); + checkStructure("/{foo}"); + checkStructure("//{f}/"); + checkStructure("/{foo}/{bar}/{wibble}"); + } + + @Test + public void completeCaptureWithConstraints() { + p = checkStructure("{foo:...}"); + assertPathElements(p, CaptureVariablePathElement.class); + p = checkStructure("{foo:[0-9]*}"); + assertPathElements(p, CaptureVariablePathElement.class); + checkError("{foo:}",5,PatternMessage.MISSING_REGEX_CONSTRAINT); + } + + @Test + public void partialCapturingPatterns() { + p = checkStructure("{foo}abc"); + assertEquals(RegexPathElement.class.getName(),p.getHeadSection().getClass().getName()); + checkStructure("abc{foo}"); + checkStructure("/abc{foo}"); + checkStructure("{foo}def/"); + checkStructure("/abc{foo}def/"); + checkStructure("{foo}abc{bar}"); + checkStructure("{foo}abc{bar}/"); + checkStructure("/{foo}abc{bar}/"); + } + + @Test + public void illegalCapturePatterns() { + checkError("{abc/",4,PatternMessage.MISSING_CLOSE_CAPTURE); + checkError("{abc:}/",5,PatternMessage.MISSING_REGEX_CONSTRAINT); + checkError("{",1,PatternMessage.MISSING_CLOSE_CAPTURE); + checkError("{abc",4,PatternMessage.MISSING_CLOSE_CAPTURE); + checkError("{/}",1,PatternMessage.MISSING_CLOSE_CAPTURE); + checkError("//{",3,PatternMessage.MISSING_CLOSE_CAPTURE); + checkError("}",0,PatternMessage.MISSING_OPEN_CAPTURE); + checkError("/}",1,PatternMessage.MISSING_OPEN_CAPTURE); + checkError("def}",3,PatternMessage.MISSING_OPEN_CAPTURE); + checkError("//{/}",3,PatternMessage.MISSING_CLOSE_CAPTURE); + checkError("//{{/}",3,PatternMessage.ILLEGAL_NESTED_CAPTURE); + checkError("//{abc{/}",6,PatternMessage.ILLEGAL_NESTED_CAPTURE); + checkError("/{0abc}/abc",2,PatternMessage.ILLEGAL_CHARACTER_AT_START_OF_CAPTURE_DESCRIPTOR); + checkError("/{a?bc}/abc",3,PatternMessage.ILLEGAL_CHARACTER_IN_CAPTURE_DESCRIPTOR); + checkError("/{abc}_{abc}",1,PatternMessage.ILLEGAL_DOUBLE_CAPTURE); + checkError("/foobar/{abc}_{abc}",8,PatternMessage.ILLEGAL_DOUBLE_CAPTURE); + checkError("/foobar/{abc:..}_{abc:..}",8,PatternMessage.ILLEGAL_DOUBLE_CAPTURE); + PathPattern pp = parse("/{abc:foo(bar)}"); + try { + pp.matchAndExtract("/foo"); + fail("Should have raised exception"); + } catch (IllegalArgumentException iae) { + assertEquals("No capture groups allowed in the constraint regex: foo(bar)",iae.getMessage()); + } + try { + pp.matchAndExtract("/foobar"); + fail("Should have raised exception"); + } catch (IllegalArgumentException iae) { + assertEquals("No capture groups allowed in the constraint regex: foo(bar)",iae.getMessage()); + } + } + + @Test + public void badPatterns() { +// checkError("/{foo}{bar}/",6,PatternMessage.CANNOT_HAVE_ADJACENT_CAPTURES); + checkError("/{?}/",2,PatternMessage.ILLEGAL_CHARACTER_AT_START_OF_CAPTURE_DESCRIPTOR,"?"); + checkError("/{a?b}/",3,PatternMessage.ILLEGAL_CHARACTER_IN_CAPTURE_DESCRIPTOR,"?"); + checkError("/{%%$}",2,PatternMessage.ILLEGAL_CHARACTER_AT_START_OF_CAPTURE_DESCRIPTOR,"%"); + checkError("/{ }",2,PatternMessage.ILLEGAL_CHARACTER_AT_START_OF_CAPTURE_DESCRIPTOR," "); + checkError("/{%:[0-9]*}",2,PatternMessage.ILLEGAL_CHARACTER_AT_START_OF_CAPTURE_DESCRIPTOR,"%"); + } + + @Test + public void patternPropertyGetCaptureCountTests() { + // Test all basic section types + assertEquals(1,parse("{foo}").getCapturedVariableCount()); + assertEquals(0,parse("foo").getCapturedVariableCount()); + assertEquals(1,parse("{*foobar}").getCapturedVariableCount()); + assertEquals(1,parse("/{*foobar}").getCapturedVariableCount()); + assertEquals(0,parse("/**").getCapturedVariableCount()); + assertEquals(1,parse("{abc}asdf").getCapturedVariableCount()); + assertEquals(1,parse("{abc}_*").getCapturedVariableCount()); + assertEquals(2,parse("{abc}_{def}").getCapturedVariableCount()); + assertEquals(0,parse("/").getCapturedVariableCount()); + assertEquals(0,parse("a?b").getCapturedVariableCount()); + assertEquals(0,parse("*").getCapturedVariableCount()); + + // Test on full templates + assertEquals(0,parse("/foo/bar").getCapturedVariableCount()); + assertEquals(1,parse("/{foo}").getCapturedVariableCount()); + assertEquals(2,parse("/{foo}/{bar}").getCapturedVariableCount()); + assertEquals(4,parse("/{foo}/{bar}_{goo}_{wibble}/abc/bar").getCapturedVariableCount()); + } + + @Test + public void patternPropertyGetWildcardCountTests() { + // Test all basic section types + assertEquals(computeScore(1,0),parse("{foo}").getScore()); + assertEquals(computeScore(0,0),parse("foo").getScore()); + assertEquals(computeScore(0,0),parse("{*foobar}").getScore()); +// assertEquals(1,parse("/**").getScore()); + assertEquals(computeScore(1,0),parse("{abc}asdf").getScore()); + assertEquals(computeScore(1,1),parse("{abc}_*").getScore()); + assertEquals(computeScore(2,0),parse("{abc}_{def}").getScore()); + assertEquals(computeScore(0,0),parse("/").getScore()); + assertEquals(computeScore(0,0),parse("a?b").getScore()); // currently deliberate + assertEquals(computeScore(0,1),parse("*").getScore()); + + // Test on full templates + assertEquals(computeScore(0,0),parse("/foo/bar").getScore()); + assertEquals(computeScore(1,0),parse("/{foo}").getScore()); + assertEquals(computeScore(2,0),parse("/{foo}/{bar}").getScore()); + assertEquals(computeScore(4,0),parse("/{foo}/{bar}_{goo}_{wibble}/abc/bar").getScore()); + assertEquals(computeScore(4,3),parse("/{foo}/*/*_*/{bar}_{goo}_{wibble}/abc/bar").getScore()); + } + + @Test + public void multipleSeparatorPatterns() { + p = checkStructure("///aaa"); + assertEquals(4,p.getNormalizedLength()); + assertPathElements(p,SeparatorPathElement.class,LiteralPathElement.class); + p = checkStructure("///aaa////aaa/b"); + assertEquals(10,p.getNormalizedLength()); + assertPathElements(p,SeparatorPathElement.class, LiteralPathElement.class, + SeparatorPathElement.class, LiteralPathElement.class, SeparatorPathElement.class, LiteralPathElement.class); + p = checkStructure("/////**"); + assertEquals(1,p.getNormalizedLength()); + assertPathElements(p,WildcardTheRestPathElement.class); + } + + @Test + public void patternPropertyGetLengthTests() { + // Test all basic section types + assertEquals(1,parse("{foo}").getNormalizedLength()); + assertEquals(3,parse("foo").getNormalizedLength()); + assertEquals(1,parse("{*foobar}").getNormalizedLength()); + assertEquals(1,parse("/{*foobar}").getNormalizedLength()); + assertEquals(1,parse("/**").getNormalizedLength()); + assertEquals(5,parse("{abc}asdf").getNormalizedLength()); + assertEquals(3,parse("{abc}_*").getNormalizedLength()); + assertEquals(3,parse("{abc}_{def}").getNormalizedLength()); + assertEquals(1,parse("/").getNormalizedLength()); + assertEquals(3,parse("a?b").getNormalizedLength()); + assertEquals(1,parse("*").getNormalizedLength()); + + // Test on full templates + assertEquals(8,parse("/foo/bar").getNormalizedLength()); + assertEquals(2,parse("/{foo}").getNormalizedLength()); + assertEquals(4,parse("/{foo}/{bar}").getNormalizedLength()); + assertEquals(16,parse("/{foo}/{bar}_{goo}_{wibble}/abc/bar").getNormalizedLength()); + } + + @Test + public void compareTests() { + PathPattern p1,p2,p3; + + // Based purely on number of captures + p1 = parse("{a}"); + p2 = parse("{a}/{b}"); + p3 = parse("{a}/{b}/{c}"); + assertEquals(-1,p1.compareTo(p2)); // Based on number of captures + List patterns = new ArrayList<>(); + patterns.add(p2); + patterns.add(p3); + patterns.add(p1); + Collections.sort(patterns,new PathPatternComparator()); + assertEquals(p1,patterns.get(0)); + + // Based purely on length + p1 = parse("/a/b/c"); + p2 = parse("/a/boo/c/doo"); + p3 = parse("/asdjflaksjdfjasdf"); + assertEquals(1,p1.compareTo(p2)); + patterns = new ArrayList<>(); + patterns.add(p2); + patterns.add(p3); + patterns.add(p1); + Collections.sort(patterns,new PathPatternComparator()); + assertEquals(p3,patterns.get(0)); + + // Based purely on 'wildness' + p1 = parse("/*"); + p2 = parse("/*/*"); + p3 = parse("/*/*/*_*"); + assertEquals(-1,p1.compareTo(p2)); + patterns = new ArrayList<>(); + patterns.add(p2); + patterns.add(p3); + patterns.add(p1); + Collections.sort(patterns,new PathPatternComparator()); + assertEquals(p1,patterns.get(0)); + + // Based purely on catchAll + p1 = parse("{*foobar}"); + p2 = parse("{*goo}"); + assertEquals(0,p1.compareTo(p2)); + + p1 = parse("/{*foobar}"); + p2 = parse("/abc/{*ww}"); + assertEquals(+1,p1.compareTo(p2)); + assertEquals(-1,p2.compareTo(p1)); + + p3 = parse("/this/that/theother"); + assertTrue(p1.isCatchAll()); + assertTrue(p2.isCatchAll()); + assertFalse(p3.isCatchAll()); + patterns = new ArrayList<>(); + patterns.add(p2); + patterns.add(p3); + patterns.add(p1); + Collections.sort(patterns,new PathPatternComparator()); + assertEquals(p3,patterns.get(0)); + assertEquals(p2,patterns.get(1)); + + patterns = new ArrayList<>(); + patterns.add(parse("/abc")); + patterns.add(null); + patterns.add(parse("/def")); + Collections.sort(patterns,new PathPatternComparator()); + assertNull(patterns.get(2)); + } + + // --- + + private PathPattern parse(String pattern) { + PathPatternParser patternParser = new PathPatternParser(); + return patternParser.parse(pattern); + } + + /** + * Verify the parsed chain of sections matches the original pattern and the separator count + * that has been determined is correct. + */ + private PathPattern checkStructure(String pattern) { + int count = 0; + for (int i=0;i... sectionClasses) { + PathElement head = p.getHeadSection(); + for (int i=0;i handlerMap = new LinkedHashMap<>(); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/condition/PatternsRequestCondition.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/condition/PatternsRequestCondition.java index 82797a3182f..b26af55ce7d 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/condition/PatternsRequestCondition.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/condition/PatternsRequestCondition.java @@ -27,11 +27,11 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Set; -import org.springframework.util.AntPathMatcher; import org.springframework.util.PathMatcher; import org.springframework.util.StringUtils; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.support.HttpRequestPathHelper; +import org.springframework.web.util.ParsingPathMatcher; /** * A logical disjunction (' || ') request condition that matches a request @@ -90,7 +90,7 @@ public final class PatternsRequestCondition extends AbstractRequestCondition { - private PathMatcher pathMatcher = new AntPathMatcher(); + private PathMatcher pathMatcher = new ParsingPathMatcher(); @Override protected boolean isHandler(Class beanType) { diff --git a/spring-webflux/src/test/resources/org/springframework/web/reactive/handler/map.xml b/spring-webflux/src/test/resources/org/springframework/web/reactive/handler/map.xml index be0f6f067f2..f2fe52c6d90 100644 --- a/spring-webflux/src/test/resources/org/springframework/web/reactive/handler/map.xml +++ b/spring-webflux/src/test/resources/org/springframework/web/reactive/handler/map.xml @@ -1,23 +1,22 @@ - + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> + welcome.html=mainController - /**/pathmatchingTest.html=mainController - /**/pathmatching??.html=mainController - /**/path??matching.html=mainController - /**/??path??matching.html=mainController - /**/*.jsp=mainController - /administrator/**/pathmatching.html=mainController - /administrator/**/testlast*=mainController + /*pathmatchingTest.html=mainController + /pathmatching??.html=mainController + /administrator/pathmatching.html=mainController + /administrator/*/pathmatching.html=mainController + /administrator/*/testlast*=mainController + /administrator/testing/longer/*=mainController + /??path??matching.html=mainController + /path??matching.html=mainController /administrator/another/bla.xml=mainController - /administrator/testing/longer/**/**/**/**/**=mainController - /administrator/testing/longer2/**/**/bla/**=mainController /*test*.jpeg=mainController /*/test.jpeg=mainController /outofpattern*yeah=mainController diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/ResourceServlet.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/ResourceServlet.java new file mode 100644 index 00000000000..1d18ad6cc5c --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/ResourceServlet.java @@ -0,0 +1,345 @@ +/* + * Copyright 2002-2012 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.servlet; + +import java.io.IOException; +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.util.PathMatcher; +import org.springframework.util.StringUtils; +import org.springframework.web.context.support.ServletContextResource; +import org.springframework.web.util.ParsingPathMatcher; + +/** + * Simple servlet that can expose an internal resource, including a + * default URL if the specified resource is not found. An alternative, + * for example, to trying and catching exceptions when using JSP include. + * + *

A further usage of this servlet is the ability to apply last-modified + * timestamps to quasi-static resources (typically JSPs). This can happen + * as bridge to parameter-specified resources, or as proxy for a specific + * target resource (or a list of specific target resources to combine). + * + *

A typical usage would map a URL like "/ResourceServlet" onto an instance + * of this servlet, and use the "JSP include" action to include this URL, + * with the "resource" parameter indicating the actual target path in the WAR. + * + *

The {@code defaultUrl} property can be set to the internal + * resource path of a default URL, to be rendered when the target resource + * is not found or not specified in the first place. + * + *

The "resource" parameter and the {@code defaultUrl} property can + * also specify a list of target resources to combine. Those resources will be + * included one by one to build the response. If last-modified determination + * is active, the newest timestamp among those files will be used. + * + *

The {@code allowedResources} property can be set to a URL + * pattern of resources that should be available via this servlet. + * If not set, any target resource can be requested, including resources + * in the WEB-INF directory! + * + *

If using this servlet for direct access rather than via includes, + * the {@code contentType} property should be specified to apply a + * proper content type. Note that a content type header in the target JSP will + * be ignored when including the resource via a RequestDispatcher include. + * + *

To apply last-modified timestamps for the target resource, set the + * {@code applyLastModified} property to true. This servlet will then + * return the file timestamp of the target resource as last-modified value, + * falling back to the startup time of this servlet if not retrievable. + * + *

Note that applying the last-modified timestamp in the above fashion + * just makes sense if the target resource does not generate content that + * depends on the HttpSession or cookies; it is just allowed to evaluate + * request parameters. + * + *

A typical case for such last-modified usage is a JSP that just makes + * minimal usage of basic means like includes or message resolution to + * build quasi-static content. Regenerating such content on every request + * is unnecessary; it can be cached as long as the file hasn't changed. + * + *

Note that this servlet will apply the last-modified timestamp if you + * tell it to do so: It's your decision whether the content of the target + * resource can be cached in such a fashion. Typical use cases are helper + * resources that are not fronted by a controller, like JavaScript files + * that are generated by a JSP (without depending on the HttpSession). + * + * @author Juergen Hoeller + * @author Rod Johnson + * @see #setDefaultUrl + * @see #setAllowedResources + * @see #setApplyLastModified + */ +@SuppressWarnings("serial") +public class ResourceServlet extends HttpServletBean { + + /** + * Any number of these characters are considered delimiters + * between multiple resource paths in a single String value. + */ + public static final String RESOURCE_URL_DELIMITERS = ",; \t\n"; + + /** + * Name of the parameter that must contain the actual resource path. + */ + public static final String RESOURCE_PARAM_NAME = "resource"; + + + private String defaultUrl; + + private String allowedResources; + + private String contentType; + + private boolean applyLastModified = false; + + private PathMatcher pathMatcher; + + private long startupTime; + + + /** + * Set the URL within the current web application from which to + * include content if the requested path isn't found, or if none + * is specified in the first place. + *

If specifying multiple URLs, they will be included one by one + * to build the response. If last-modified determination is active, + * the newest timestamp among those files will be used. + * @see #setApplyLastModified + */ + public void setDefaultUrl(String defaultUrl) { + this.defaultUrl = defaultUrl; + } + + /** + * Set allowed resources as URL pattern, e.g. "/WEB-INF/res/*.jsp", + * The parameter can be any Ant-style pattern parsable by AntPathMatcher. + * @see org.springframework.util.AntPathMatcher + */ + public void setAllowedResources(String allowedResources) { + this.allowedResources = allowedResources; + } + + /** + * Set the content type of the target resource (typically a JSP). + * Default is none, which is appropriate when including resources. + *

For directly accessing resources, for example to leverage this + * servlet's last-modified support, specify a content type here. + * Note that a content type header in the target JSP will be ignored + * when including the resource via a RequestDispatcher include. + */ + public void setContentType(String contentType) { + this.contentType = contentType; + } + + /** + * Set whether to apply the file timestamp of the target resource + * as last-modified value. Default is "false". + *

This is mainly intended for JSP targets that don't generate + * session-specific or database-driven content: Such files can be + * cached by the browser as long as the last-modified timestamp + * of the JSP file doesn't change. + *

This will only work correctly with expanded WAR files that + * allow access to the file timestamps. Else, the startup time + * of this servlet is returned. + */ + public void setApplyLastModified(boolean applyLastModified) { + this.applyLastModified = applyLastModified; + } + + + /** + * Remember the startup time, using no last-modified time before it. + */ + @Override + protected void initServletBean() { + this.pathMatcher = getPathMatcher(); + this.startupTime = System.currentTimeMillis(); + } + + /** + * Return a PathMatcher to use for matching the "allowedResources" URL pattern. + * Default is AntPathMatcher. + * @see #setAllowedResources + * @see org.springframework.util.AntPathMatcher + */ + protected PathMatcher getPathMatcher() { + return new ParsingPathMatcher(); + } + + + /** + * Determine the URL of the target resource and include it. + * @see #determineResourceUrl + */ + @Override + protected final void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + // determine URL of resource to include + String resourceUrl = determineResourceUrl(request); + + if (resourceUrl != null) { + try { + doInclude(request, response, resourceUrl); + } + catch (ServletException ex) { + if (logger.isWarnEnabled()) { + logger.warn("Failed to include content of resource [" + resourceUrl + "]", ex); + } + // Try including default URL if appropriate. + if (!includeDefaultUrl(request, response)) { + throw ex; + } + } + catch (IOException ex) { + if (logger.isWarnEnabled()) { + logger.warn("Failed to include content of resource [" + resourceUrl + "]", ex); + } + // Try including default URL if appropriate. + if (!includeDefaultUrl(request, response)) { + throw ex; + } + } + } + + // no resource URL specified -> try to include default URL. + else if (!includeDefaultUrl(request, response)) { + throw new ServletException("No target resource URL found for request"); + } + } + + /** + * Determine the URL of the target resource of this request. + *

Default implementation returns the value of the "resource" parameter. + * Can be overridden in subclasses. + * @param request current HTTP request + * @return the URL of the target resource, or {@code null} if none found + * @see #RESOURCE_PARAM_NAME + */ + protected String determineResourceUrl(HttpServletRequest request) { + return request.getParameter(RESOURCE_PARAM_NAME); + } + + /** + * Include the specified default URL, if appropriate. + * @param request current HTTP request + * @param response current HTTP response + * @return whether a default URL was included + * @throws ServletException if thrown by the RequestDispatcher + * @throws IOException if thrown by the RequestDispatcher + */ + private boolean includeDefaultUrl(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + if (this.defaultUrl == null) { + return false; + } + doInclude(request, response, this.defaultUrl); + return true; + } + + /** + * Include the specified resource via the RequestDispatcher. + * @param request current HTTP request + * @param response current HTTP response + * @param resourceUrl the URL of the target resource + * @throws ServletException if thrown by the RequestDispatcher + * @throws IOException if thrown by the RequestDispatcher + */ + private void doInclude(HttpServletRequest request, HttpServletResponse response, String resourceUrl) + throws ServletException, IOException { + + if (this.contentType != null) { + response.setContentType(this.contentType); + } + String[] resourceUrls = + StringUtils.tokenizeToStringArray(resourceUrl, RESOURCE_URL_DELIMITERS); + for (int i = 0; i < resourceUrls.length; i++) { + // check whether URL matches allowed resources + if (this.allowedResources != null && !this.pathMatcher.match(this.allowedResources, resourceUrls[i])) { + throw new ServletException("Resource [" + resourceUrls[i] + + "] does not match allowed pattern [" + this.allowedResources + "]"); + } + if (logger.isDebugEnabled()) { + logger.debug("Including resource [" + resourceUrls[i] + "]"); + } + RequestDispatcher rd = request.getRequestDispatcher(resourceUrls[i]); + rd.include(request, response); + } + } + + /** + * Return the last-modified timestamp of the file that corresponds + * to the target resource URL (i.e. typically the request ".jsp" file). + * Will simply return -1 if "applyLastModified" is false (the default). + *

Returns no last-modified date before the startup time of this servlet, + * to allow for message resolution etc that influences JSP contents, + * assuming that those background resources might have changed on restart. + *

Returns the startup time of this servlet if the file that corresponds + * to the target resource URL couldn't be resolved (for example, because + * the WAR is not expanded). + * @see #determineResourceUrl + * @see #getFileTimestamp + */ + @Override + protected final long getLastModified(HttpServletRequest request) { + if (this.applyLastModified) { + String resourceUrl = determineResourceUrl(request); + if (resourceUrl == null) { + resourceUrl = this.defaultUrl; + } + if (resourceUrl != null) { + String[] resourceUrls = StringUtils.tokenizeToStringArray(resourceUrl, RESOURCE_URL_DELIMITERS); + long latestTimestamp = -1; + for (int i = 0; i < resourceUrls.length; i++) { + long timestamp = getFileTimestamp(resourceUrls[i]); + if (timestamp > latestTimestamp) { + latestTimestamp = timestamp; + } + } + return (latestTimestamp > this.startupTime ? latestTimestamp : this.startupTime); + } + } + return -1; + } + + /** + * Return the file timestamp for the given resource. + * @param resourceUrl the URL of the resource + * @return the file timestamp in milliseconds, or -1 if not determinable + */ + protected long getFileTimestamp(String resourceUrl) { + ServletContextResource resource = new ServletContextResource(getServletContext(), resourceUrl); + try { + long lastModifiedTime = resource.lastModified(); + if (logger.isDebugEnabled()) { + logger.debug("Last-modified timestamp of " + resource + " is " + lastModifiedTime); + } + return lastModifiedTime; + } + catch (IOException ex) { + logger.warn("Couldn't retrieve last-modified timestamp of [" + resource + + "] - using ResourceServlet startup time"); + return -1; + } + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java index 3c27aa5ee77..85d5e30aced 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java @@ -55,7 +55,6 @@ import org.springframework.http.converter.support.AllEncompassingFormHttpMessage import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter; import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter; import org.springframework.http.converter.xml.SourceHttpMessageConverter; -import org.springframework.util.AntPathMatcher; import org.springframework.util.ClassUtils; import org.springframework.util.PathMatcher; import org.springframework.validation.Errors; @@ -94,6 +93,7 @@ import org.springframework.web.servlet.resource.ResourceUrlProvider; import org.springframework.web.servlet.resource.ResourceUrlProviderExposingInterceptor; import org.springframework.web.servlet.view.InternalResourceViewResolver; import org.springframework.web.servlet.view.ViewResolverComposite; +import org.springframework.web.util.ParsingPathMatcher; import org.springframework.web.util.UrlPathHelper; /** @@ -140,7 +140,7 @@ import org.springframework.web.util.UrlPathHelper; * exception types * * - *

Registers an {@link AntPathMatcher} and a {@link UrlPathHelper} + *

Registers an {@link ParsingPathMatcher} and a {@link UrlPathHelper} * to be used by: *

    *
  • the {@link RequestMappingHandlerMapping}, @@ -345,7 +345,7 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv @Bean public PathMatcher mvcPathMatcher() { PathMatcher pathMatcher = getPathMatchConfigurer().getPathMatcher(); - return (pathMatcher != null ? pathMatcher : new AntPathMatcher()); + return (pathMatcher != null ? pathMatcher : new ParsingPathMatcher()); } /** diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java index a44ee50143e..1ef601cec54 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java @@ -27,7 +27,6 @@ import javax.servlet.http.HttpServletResponse; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.core.Ordered; -import org.springframework.util.AntPathMatcher; import org.springframework.util.Assert; import org.springframework.util.PathMatcher; import org.springframework.web.HttpRequestHandler; @@ -42,6 +41,7 @@ import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.util.ParsingPathMatcher; import org.springframework.web.util.UrlPathHelper; /** @@ -72,7 +72,7 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport private UrlPathHelper urlPathHelper = new UrlPathHelper(); - private PathMatcher pathMatcher = new AntPathMatcher(); + private PathMatcher pathMatcher = new ParsingPathMatcher(); private final List interceptors = new ArrayList<>(); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/WebContentInterceptor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/WebContentInterceptor.java index 2bd9d7f62ff..908e1f3efe6 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/WebContentInterceptor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/WebContentInterceptor.java @@ -25,12 +25,12 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.http.CacheControl; -import org.springframework.util.AntPathMatcher; import org.springframework.util.Assert; import org.springframework.util.PathMatcher; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.support.WebContentGenerator; +import org.springframework.web.util.ParsingPathMatcher; import org.springframework.web.util.UrlPathHelper; /** @@ -52,7 +52,7 @@ public class WebContentInterceptor extends WebContentGenerator implements Handle private UrlPathHelper urlPathHelper = new UrlPathHelper(); - private PathMatcher pathMatcher = new AntPathMatcher(); + private PathMatcher pathMatcher = new ParsingPathMatcher(); private Map cacheMappings = new HashMap<>(); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/PatternsRequestCondition.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/PatternsRequestCondition.java index 25f8046d067..e1c4d090ead 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/PatternsRequestCondition.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/PatternsRequestCondition.java @@ -27,9 +27,9 @@ import java.util.List; import java.util.Set; import javax.servlet.http.HttpServletRequest; -import org.springframework.util.AntPathMatcher; import org.springframework.util.PathMatcher; import org.springframework.util.StringUtils; +import org.springframework.web.util.ParsingPathMatcher; import org.springframework.web.util.UrlPathHelper; /** @@ -104,7 +104,7 @@ public final class PatternsRequestCondition extends AbstractRequestCondition handlerMap = new LinkedHashMap<>(); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/InterceptorRegistryTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/InterceptorRegistryTests.java index 4e31e6534b7..ce923e80681 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/InterceptorRegistryTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/InterceptorRegistryTests.java @@ -37,6 +37,7 @@ import org.springframework.web.servlet.handler.MappedInterceptor; import org.springframework.web.servlet.handler.WebRequestHandlerInterceptorAdapter; import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; import org.springframework.web.servlet.theme.ThemeChangeInterceptor; +import org.springframework.web.util.ParsingPathMatcher; import static org.junit.Assert.*; @@ -152,7 +153,7 @@ public class InterceptorRegistryTests { private List getInterceptorsForPath(String lookupPath) { - PathMatcher pathMatcher = new AntPathMatcher(); + PathMatcher pathMatcher = new ParsingPathMatcher(); List result = new ArrayList<>(); for (Object interceptor : this.registry.getInterceptors()) { if (interceptor instanceof MappedInterceptor) { diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java index b31d35ffd48..77f12428863 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java @@ -83,6 +83,7 @@ import org.springframework.web.servlet.resource.ResourceUrlProviderExposingInter import org.springframework.web.servlet.view.BeanNameViewResolver; import org.springframework.web.servlet.view.InternalResourceViewResolver; import org.springframework.web.servlet.view.ViewResolverComposite; +import org.springframework.web.util.ParsingPathMatcher; import org.springframework.web.util.UrlPathHelper; import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES; @@ -320,7 +321,7 @@ public class WebMvcConfigurationSupportTests { assertNotNull(urlPathHelper); assertNotNull(pathMatcher); - assertEquals(AntPathMatcher.class, pathMatcher.getClass()); + assertEquals(ParsingPathMatcher.class, pathMatcher.getClass()); } private ApplicationContext initContext(Class... configClasses) { diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMethodMappingTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMethodMappingTests.java index 5a181069fe1..810dc414274 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMethodMappingTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMethodMappingTests.java @@ -38,6 +38,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.context.support.StaticWebApplicationContext; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.method.HandlerMethod; +import org.springframework.web.util.ParsingPathMatcher; import org.springframework.web.util.UrlPathHelper; @@ -244,7 +245,7 @@ public class HandlerMethodMappingTests { private UrlPathHelper pathHelper = new UrlPathHelper(); - private PathMatcher pathMatcher = new AntPathMatcher(); + private PathMatcher pathMatcher = new ParsingPathMatcher(); public MyHandlerMethodMapping() { diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/PathMatchingUrlHandlerMappingTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/PathMatchingUrlHandlerMappingTests.java index c2ce3e358b2..14d3206a21b 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/PathMatchingUrlHandlerMappingTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/PathMatchingUrlHandlerMappingTests.java @@ -81,7 +81,7 @@ public class PathMatchingUrlHandlerMappingTests { HandlerExecutionChain hec = getHandler(req); assertTrue("Handler is null", hec != null); assertTrue("Handler is correct bean", hec.getHandler() == bean); - assertEquals("pathmatchingTest.html", req.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE)); + assertEquals("/pathmatchingTest.html", req.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE)); // no match, no forward slash included req = new MockHttpServletRequest("GET", "welcome.html"); @@ -121,11 +121,6 @@ public class PathMatchingUrlHandlerMappingTests { hec = getHandler(req); assertTrue("Handler is correct bean", hec != null && hec.getHandler() == bean); - // this as well, because there's a **/in there as well - req = new MockHttpServletRequest("GET", "/testing/bla.jsp"); - hec = getHandler(req); - assertTrue("Handler is correct bean", hec != null && hec.getHandler() == bean); - // should match because exact pattern is there req = new MockHttpServletRequest("GET", "/administrator/another/bla.xml"); hec = getHandler(req); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/UrlFilenameViewControllerTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/UrlFilenameViewControllerTests.java index 690ae54128e..1ee1afe6f83 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/UrlFilenameViewControllerTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/UrlFilenameViewControllerTests.java @@ -23,11 +23,11 @@ import static org.junit.Assert.*; import org.springframework.mock.web.test.MockHttpServletRequest; import org.springframework.mock.web.test.MockHttpServletResponse; import org.springframework.ui.ModelMap; -import org.springframework.util.AntPathMatcher; import org.springframework.util.PathMatcher; import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.util.ParsingPathMatcher; /** * @author Juergen Hoeller @@ -36,7 +36,7 @@ import org.springframework.web.servlet.ModelAndView; */ public class UrlFilenameViewControllerTests { - private PathMatcher pathMatcher = new AntPathMatcher(); + private PathMatcher pathMatcher = new ParsingPathMatcher(); @Test diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/WebContentInterceptorTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/WebContentInterceptorTests.java index 06a844e13a0..1ba056dcebf 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/WebContentInterceptorTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/WebContentInterceptorTests.java @@ -62,19 +62,21 @@ public class WebContentInterceptorTests { @Test public void mappedCacheConfigurationOverridesGlobal() throws Exception { Properties mappings = new Properties(); - mappings.setProperty("**/*handle.vm", "-1"); + mappings.setProperty("*/*handle.vm", "-1"); // was **/*handle.vm WebContentInterceptor interceptor = new WebContentInterceptor(); interceptor.setCacheSeconds(10); interceptor.setCacheMappings(mappings); - request.setRequestURI("http://localhost:7070/example/adminhandle.vm"); +// request.setRequestURI("http://localhost:7070/example/adminhandle.vm"); + request.setRequestURI("example/adminhandle.vm"); interceptor.preHandle(request, response, null); Iterable cacheControlHeaders = response.getHeaders("Cache-Control"); assertThat(cacheControlHeaders, Matchers.emptyIterable()); - request.setRequestURI("http://localhost:7070/example/bingo.html"); +// request.setRequestURI("http://localhost:7070/example/bingo.html"); + request.setRequestURI("example/bingo.html"); interceptor.preHandle(request, response, null); cacheControlHeaders = response.getHeaders("Cache-Control"); @@ -143,10 +145,11 @@ public class WebContentInterceptorTests { interceptor.setUseExpiresHeader(true); interceptor.setAlwaysMustRevalidate(true); Properties mappings = new Properties(); - mappings.setProperty("**/*.cache.html", "10"); + mappings.setProperty("*/*.cache.html", "10"); // was **/*.cache.html interceptor.setCacheMappings(mappings); - request.setRequestURI("http://example.org/foo/page.html"); +// request.setRequestURI("http://example.org/foo/page.html"); + request.setRequestURI("foo/page.html"); interceptor.preHandle(request, response, null); Iterable expiresHeaders = response.getHeaders("Expires"); @@ -157,7 +160,8 @@ public class WebContentInterceptorTests { assertThat(pragmaHeaders, Matchers.contains("no-cache")); response = new MockHttpServletResponse(); - request.setRequestURI("http://example.org/page.cache.html"); +// request.setRequestURI("http://example.org/page.cache.html"); + request.setRequestURI("foo/page.cache.html"); interceptor.preHandle(request, response, null); expiresHeaders = response.getHeaders("Expires"); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java index 3344d198091..cd072124dc8 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java @@ -2364,22 +2364,22 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl @Controller static class MyRelativeMethodPathDispatchingController { - @RequestMapping("**/myHandle") + @RequestMapping("*/myHandle") // was **/myHandle public void myHandle(HttpServletResponse response) throws IOException { response.getWriter().write("myView"); } - @RequestMapping("/**/*Other") + @RequestMapping("/*/*Other") // was /**/*Other public void myOtherHandle(HttpServletResponse response) throws IOException { response.getWriter().write("myOtherView"); } - @RequestMapping("**/myLang") + @RequestMapping("*/myLang") // was **/myLang public void myLangHandle(HttpServletResponse response) throws IOException { response.getWriter().write("myLangView"); } - @RequestMapping("/**/surprise") + @RequestMapping("/*/surprise") // was /**/surprise public void mySurpriseHandle(HttpServletResponse response) throws IOException { response.getWriter().write("mySurpriseView"); } @@ -2643,7 +2643,7 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl @Controller public static class PathOrderingController { - @RequestMapping(value = {"/dir/myPath1.do", "/**/*.do"}) + @RequestMapping(value = {"/dir/myPath1.do", "/*/*.do"}) public void method1(Writer writer) throws IOException { writer.write("method1"); } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/UriTemplateServletAnnotationControllerHandlerMethodTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/UriTemplateServletAnnotationControllerHandlerMethodTests.java index f85faf5dcad..d97046d13da 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/UriTemplateServletAnnotationControllerHandlerMethodTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/UriTemplateServletAnnotationControllerHandlerMethodTests.java @@ -571,14 +571,14 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab @RequestMapping("/category") public static class MultiPathController { - @RequestMapping(value = {"/{category}/page/{page}", "/**/{category}/page/{page}"}) + @RequestMapping(value = {"/{category}/page/{page}", "/*/{category}/page/{page}"}) public void category(@PathVariable String category, @PathVariable int page, Writer writer) throws IOException { writer.write("handle1-"); writer.write("category-" + category); writer.write("page-" + page); } - @RequestMapping(value = {"/{category}", "/**/{category}"}) + @RequestMapping(value = {"/{category}", "/*/{category}"}) public void category(@PathVariable String category, Writer writer) throws IOException { writer.write("handle2-"); writer.write("category-" + category); @@ -598,7 +598,7 @@ public class UriTemplateServletAnnotationControllerHandlerMethodTests extends Ab } @Controller - @RequestMapping("/*/menu/**") + @RequestMapping("/*/menu/") // was /*/menu/** public static class MenuTreeController { @RequestMapping("type/{var}") diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/handler/map3.xml b/spring-webmvc/src/test/resources/org/springframework/web/servlet/handler/map3.xml index bc7a9e49a19..3aa11bd36f4 100644 --- a/spring-webmvc/src/test/resources/org/springframework/web/servlet/handler/map3.xml +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/handler/map3.xml @@ -8,16 +8,17 @@ welcome.html=mainController - /**/pathmatchingTest.html=mainController - /**/pathmatching??.html=mainController - /**/path??matching.html=mainController - /**/??path??matching.html=mainController - /**/*.jsp=mainController - /administrator/**/pathmatching.html=mainController - /administrator/**/testlast*=mainController + /path??matching.html=mainController + /pathmatchingTest.html=mainController + ??path??matching.html=mainController + /administrator/pathmatching.html=mainController + /administrator/testlast*=mainController + /administrator/testing/longer/{*foobar}=mainController + /administrator/*/testlast*=mainController + /administrator/*/pathmatching.html=mainController + /pathmatching??.html=mainController + /*.jsp=mainController /administrator/another/bla.xml=mainController - /administrator/testing/longer/**/**/**/**/**=mainController - /administrator/testing/longer2/**/**/bla/**=mainController /*test*.jpeg=mainController /*/test.jpeg=mainController /outofpattern*yeah=mainController