@ -16,11 +16,18 @@
@@ -16,11 +16,18 @@
package org.springframework.web.util.pattern ;
import java.nio.charset.StandardCharsets ;
import java.util.Collections ;
import java.util.HashMap ;
import java.util.List ;
import java.util.Map ;
import org.springframework.http.server.reactive.PathContainer ;
import org.springframework.http.server.reactive.PathContainer.Element ;
import org.springframework.http.server.reactive.PathContainer.Segment ;
import org.springframework.http.server.reactive.PathContainer.Separator ;
import org.springframework.lang.Nullable ;
import org.springframework.util.MultiValueMap ;
import org.springframework.util.PathMatcher ;
import org.springframework.util.StringUtils ;
@ -63,9 +70,14 @@ import org.springframework.util.StringUtils;
@@ -63,9 +70,14 @@ import org.springframework.util.StringUtils;
* /
public class PathPattern implements Comparable < PathPattern > {
private final static PathContainer EMPTY_PATH = PathContainer . parse ( "" , StandardCharsets . UTF_8 ) ;
/** The parser used to construct this pattern */
private final PathPatternParser parser ;
/** First path element in the parsed chain of path elements for this pattern */
@Nullable
private PathElement head ;
private final PathElement head ;
/** The text of the parsed pattern */
private String patternString ;
@ -109,10 +121,10 @@ public class PathPattern implements Comparable<PathPattern> {
@@ -109,10 +121,10 @@ public class PathPattern implements Comparable<PathPattern> {
private boolean catchAll = false ;
PathPattern ( String patternText , PathElement head , char separator , boolean caseSensitive ,
PathPattern ( String patternText , PathPatternParser parser , Path Element head , char separator , boolean caseSensitive ,
boolean allowOptionalTrailingSlash ) {
this . patternString = patternText ;
this . parser = parser ;
this . head = head ;
this . separator = separator ;
this . caseSensitive = caseSensitive ;
@ -137,54 +149,48 @@ public class PathPattern implements Comparable<PathPattern> {
@@ -137,54 +149,48 @@ public class PathPattern implements Comparable<PathPattern> {
/ * *
* R eturn the original pattern string that was parsed to create this PathPattern .
* @r eturn the original pattern string that was parsed to create this PathPattern .
* /
public String getPatternString ( ) {
return this . patternString ;
}
@Nullable
PathElement getHeadSection ( ) {
return this . head ;
}
/ * *
* @param path the candidate path to attempt to match against this pattern
* @param pathContainer the candidate path container to attempt to match against this pattern
* @return true if the path matches this pattern
* /
public boolean matches ( String path ) {
public boolean matches ( PathContainer pathContainer ) {
if ( this . head = = null ) {
return ! StringUtils . hasLength ( path ) ;
return ! hasLength ( pathContainer ) ;
}
else if ( ! StringUtils . hasLength ( path ) ) {
else if ( ! hasLength ( pathContainer ) ) {
if ( this . head instanceof WildcardTheRestPathElement | | this . head instanceof CaptureTheRestPathElement ) {
path = "" ; // Will allow CaptureTheRest to bind the variable to empty
pathContainer = EMPTY_PATH ; // Will allow CaptureTheRest to bind the variable to empty
}
else {
return false ;
}
}
MatchingContext matchingContext = new MatchingContext ( path , false ) ;
MatchingContext matchingContext = new MatchingContext ( pathContainer , false ) ;
return this . head . matches ( 0 , matchingContext ) ;
}
/ * *
* For a given path return the remaining piece that is not covered by this PathPattern .
* @param path a path that may or may not match this path pattern
* @param pathContainer a path that may or may not match this path pattern
* @return a { @link PathRemainingMatchInfo } describing the match result ,
* or { @code null } if the path does not match this pattern
* /
@Nullable
public PathRemainingMatchInfo getPathRemaining ( String path ) {
public PathRemainingMatchInfo getPathRemaining ( @Nullable PathContainer pathContainer ) {
if ( this . head = = null ) {
return new PathRemainingMatchInfo ( path ) ;
return new PathRemainingMatchInfo ( pathContainer ) ;
}
else if ( ! StringUtils . hasLength ( path ) ) {
else if ( ! hasLength ( pathContainer ) ) {
return null ;
}
MatchingContext matchingContext = new MatchingContext ( path , true ) ;
MatchingContext matchingContext = new MatchingContext ( pathContainer , true ) ;
matchingContext . setMatchAllowExtraPath ( ) ;
boolean matches = this . head . matches ( 0 , matchingContext ) ;
if ( ! matches ) {
@ -192,11 +198,11 @@ public class PathPattern implements Comparable<PathPattern> {
@@ -192,11 +198,11 @@ public class PathPattern implements Comparable<PathPattern> {
}
else {
PathRemainingMatchInfo info ;
if ( matchingContext . remainingPathIndex = = path . length ( ) ) {
info = new PathRemainingMatchInfo ( "" , matchingContext . getExtractedVariables ( ) ) ;
if ( matchingContext . remainingPathIndex = = pathContainer . elements ( ) . size ( ) ) {
info = new PathRemainingMatchInfo ( EMPTY_PATH , matchingContext . getExtractedVariables ( ) ) ;
}
else {
info = new PathRemainingMatchInfo ( path . substring ( matchingContext . remainingPathIndex ) ,
info = new PathRemainingMatchInfo ( PathContainer . subPath ( pathContainer , matchingContext . remainingPathIndex ) ,
matchingContext . getExtractedVariables ( ) ) ;
}
return info ;
@ -204,36 +210,36 @@ public class PathPattern implements Comparable<PathPattern> {
@@ -204,36 +210,36 @@ public class PathPattern implements Comparable<PathPattern> {
}
/ * *
* @param path the path to check against the pattern
* @param pathContainer 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 ) {
public boolean matchStart ( PathContainer pathContainer ) {
if ( this . head = = null ) {
return ! StringUtils . hasLength ( path ) ;
return ! hasLength ( pathContainer ) ;
}
else if ( ! StringUtils . hasLength ( path ) ) {
else if ( ! hasLength ( pathContainer ) ) {
return true ;
}
MatchingContext matchingContext = new MatchingContext ( path , false ) ;
MatchingContext matchingContext = new MatchingContext ( pathContainer , false ) ;
matchingContext . setMatchStartMatching ( true ) ;
return this . head . matches ( 0 , matchingContext ) ;
}
/ * *
* @param path a path that matches this pattern from which to extract variables
* @param pathContainer a path that matches this pattern from which to extract variables
* @return a map of extracted variables - an empty map if no variables extracted .
* @throws IllegalStateException if the path does not match the pattern
* /
public Map < String , String > matchAndExtract ( String path ) {
MatchingContext matchingContext = new MatchingContext ( path , true ) ;
public Map < String , PathMatchResult > matchAndExtract ( PathContainer pathContainer ) {
MatchingContext matchingContext = new MatchingContext ( pathContainer , true ) ;
if ( this . head ! = null & & this . head . matches ( 0 , matchingContext ) ) {
return matchingContext . getExtractedVariables ( ) ;
}
else if ( ! StringUtils . hasLength ( path ) ) {
else if ( ! hasLength ( pathContainer ) ) {
return Collections . emptyMap ( ) ;
}
else {
throw new IllegalStateException ( "Pattern \"" + this + "\" is not a match for \"" + path + "\"" ) ;
throw new IllegalStateException ( "Pattern \"" + this + "\" is not a match for \"" + pathContainer . value ( ) + "\"" ) ;
}
}
@ -367,48 +373,21 @@ public class PathPattern implements Comparable<PathPattern> {
@@ -367,48 +373,21 @@ public class PathPattern implements Comparable<PathPattern> {
return ( lenDifference < 0 ) ? + 1 : ( lenDifference = = 0 ? 0 : - 1 ) ;
}
int getScore ( ) {
return this . score ;
}
boolean isCatchAll ( ) {
return this . catchAll ;
}
/ * *
* The normalized length is trying to measure the ' active ' part of the pattern . It is computed
* by assuming all capture 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 getNormalizedLength ( ) {
return this . normalizedLength ;
}
char getSeparator ( ) {
return this . separator ;
}
int getCapturedVariableCount ( ) {
return this . capturedVariableCount ;
}
/ * *
* Combine this pattern with another . Currently does not produce a new PathPattern , just produces a new string .
* /
public String combine ( String pattern2string ) {
public PathPattern combine ( PathPattern pattern2string ) {
// If one of them is empty the result is the other. If both empty the result is ""
if ( ! StringUtils . hasLength ( this . patternString ) ) {
if ( ! StringUtils . hasLength ( pattern2string ) ) {
return "" ;
if ( ! StringUtils . hasLength ( pattern2string . patternString ) ) {
return parser . parse ( "" ) ;
}
else {
return pattern2string ;
}
}
else if ( ! StringUtils . hasLength ( pattern2string ) ) {
return this . patternString ;
else if ( ! StringUtils . hasLength ( pattern2string . patternString ) ) {
return this ;
}
// /* + /hotel => /hotel
@ -416,61 +395,40 @@ public class PathPattern implements Comparable<PathPattern> {
@@ -416,61 +395,40 @@ public class PathPattern implements Comparable<PathPattern> {
// However:
// /usr + /user => /usr/user
// /{foo} + /bar => /{foo}/bar
if ( ! this . patternString . equals ( pattern2string ) & & this . capturedVariableCount = = 0 & & matches ( pattern2string ) ) {
if ( ! this . patternString . equals ( pattern2string . patternString ) & & this . capturedVariableCount = = 0 & &
matches ( PathContainer . parse ( pattern2string . patternString , StandardCharsets . UTF_8 ) ) ) {
return pattern2string ;
}
// /hotels/* + /booking => /hotels/booking
// /hotels/* + booking => /hotels/booking
if ( this . endsWithSeparatorWildcard ) {
return concat ( this . patternString . substring ( 0 , this . patternString . length ( ) - 2 ) , pattern2string ) ;
return parser . parse ( concat ( this . patternString . substring ( 0 , this . patternString . length ( ) - 2 ) , pattern2string . patternString ) ) ;
}
// /hotels + /booking => /hotels/booking
// /hotels + booking => /hotels/booking
int starDotPos1 = this . patternString . indexOf ( "*." ) ; // Are there any file prefix/suffix things to consider?
if ( this . capturedVariableCount ! = 0 | | starDotPos1 = = - 1 | | this . separator = = '.' ) {
return concat ( this . patternString , pattern2string ) ;
return parser . parse ( concat ( this . patternString , pattern2string . patternString ) ) ;
}
// /*.html + /hotel => /hotel.html
// /*.html + /hotel.* => /hotel.html
String firstExtension = this . patternString . substring ( starDotPos1 + 1 ) ; // looking for the first extension
int dotPos2 = pattern2string . indexOf ( '.' ) ;
String file2 = ( dotPos2 = = - 1 ? pattern2string : pattern2string . substring ( 0 , dotPos2 ) ) ;
String secondExtension = ( dotPos2 = = - 1 ? "" : pattern2string . substring ( dotPos2 ) ) ;
String p2string = pattern2string . patternString ;
int dotPos2 = p2string . indexOf ( '.' ) ;
String file2 = ( dotPos2 = = - 1 ? p2string : p2string . substring ( 0 , dotPos2 ) ) ;
String secondExtension = ( dotPos2 = = - 1 ? "" : p2string . substring ( dotPos2 ) ) ;
boolean firstExtensionWild = ( firstExtension . equals ( ".*" ) | | firstExtension . equals ( "" ) ) ;
boolean secondExtensionWild = ( secondExtension . equals ( ".*" ) | | secondExtension . equals ( "" ) ) ;
if ( ! firstExtensionWild & & ! secondExtensionWild ) {
throw new IllegalArgumentException (
"Cannot combine patterns: " + this . patternString + " and " + pattern2string ) ;
}
return file2 + ( firstExtensionWild ? secondExtension : firstExtension ) ;
return parser . parse ( 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 ) = = this . separator ) ;
boolean path2StartsWithSeparator = ( path2 . charAt ( 0 ) = = this . separator ) ;
if ( path1EndsWithSeparator & & path2StartsWithSeparator ) {
return path1 + path2 . substring ( 1 ) ;
}
else if ( path1EndsWithSeparator | | path2StartsWithSeparator ) {
return path1 + path2 ;
}
else {
return path1 + this . separator + path2 ;
}
}
public boolean equals ( Object other ) {
if ( ! ( other instanceof PathPattern ) ) {
return false ;
@ -489,16 +447,47 @@ public class PathPattern implements Comparable<PathPattern> {
@@ -489,16 +447,47 @@ public class PathPattern implements Comparable<PathPattern> {
return this . patternString ;
}
String toChainString ( ) {
StringBuilder buf = new StringBuilder ( ) ;
PathElement pe = this . head ;
while ( pe ! = null ) {
buf . append ( pe . toString ( ) ) . append ( " " ) ;
pe = pe . next ;
/ * *
* Represents the result of a successful variable match . This holds the key that matched , the
* value that was found for that key and , if any , the parameters attached to that path element .
* For example : "/{var}" against "/foo;a=b" will return a PathMathResult with ' key = var ' ,
* ' value = foo ' and parameters ' a = b ' .
* /
public static class PathMatchResult {
private final String key ;
private final String value ;
private final MultiValueMap < String , String > parameters ;
public PathMatchResult ( String key , String value , MultiValueMap < String , String > parameters ) {
this . key = key ;
this . value = value ;
this . parameters = parameters ;
}
return buf . toString ( ) . trim ( ) ;
}
/ * *
* @return match result key
* /
public String key ( ) {
return key ;
}
/ * *
* @return match result value
* /
public String value ( ) {
return this . value ;
}
/ * *
* @return match result parameters ( empty map if no parameters )
* /
public MultiValueMap < String , String > parameters ( ) {
return this . parameters ;
}
}
/ * *
* A holder for the result of a { @link PathPattern # getPathRemaining ( String ) } call . Holds
@ -507,15 +496,15 @@ public class PathPattern implements Comparable<PathPattern> {
@@ -507,15 +496,15 @@ public class PathPattern implements Comparable<PathPattern> {
* /
public static class PathRemainingMatchInfo {
private final String pathRemaining ;
private final PathContainer pathRemaining ;
private final Map < String , String > matchingVariables ;
private final Map < String , PathMatchResult > matchingVariables ;
PathRemainingMatchInfo ( String pathRemaining ) {
PathRemainingMatchInfo ( @Nullable PathContainer pathRemaining ) {
this ( pathRemaining , Collections . emptyMap ( ) ) ;
}
PathRemainingMatchInfo ( String pathRemaining , Map < String , String > matchingVariables ) {
PathRemainingMatchInfo ( @Nullable PathContainer pathRemaining , Map < String , PathMatchResult > matchingVariables ) {
this . pathRemaining = pathRemaining ;
this . matchingVariables = matchingVariables ;
}
@ -524,18 +513,71 @@ public class PathPattern implements Comparable<PathPattern> {
@@ -524,18 +513,71 @@ public class PathPattern implements Comparable<PathPattern> {
* Return the part of a path that was not matched by a pattern .
* /
public String getPathRemaining ( ) {
return this . pathRemaining ;
return this . pathRemaining = = null ? null : this . pathRemaining . value ( ) ;
}
/ * *
* Return variables that were bound in the part of the path that was successfully matched .
* Will be an empty map if no variables were bound
* /
public Map < String , String > getMatchingVariables ( ) {
public Map < String , PathMatchResult > getMatchingVariables ( ) {
return this . matchingVariables ;
}
}
int getScore ( ) {
return this . score ;
}
boolean isCatchAll ( ) {
return this . catchAll ;
}
/ * *
* The normalized length is trying to measure the ' active ' part of the pattern . It is computed
* by assuming all capture 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 getNormalizedLength ( ) {
return this . normalizedLength ;
}
char getSeparator ( ) {
return this . separator ;
}
int getCapturedVariableCount ( ) {
return this . capturedVariableCount ;
}
String toChainString ( ) {
StringBuilder buf = new StringBuilder ( ) ;
PathElement pe = this . head ;
while ( pe ! = null ) {
buf . append ( pe . toString ( ) ) . append ( " " ) ;
pe = pe . next ;
}
return buf . toString ( ) . trim ( ) ;
}
/ * *
* @return string form of the pattern built from walking the path element chain
* /
String computePatternString ( ) {
StringBuilder buf = new StringBuilder ( ) ;
PathElement pe = this . head ;
while ( pe ! = null ) {
buf . append ( pe . getChars ( ) ) ;
pe = pe . next ;
}
return buf . toString ( ) ;
}
@Nullable
PathElement getHeadSection ( ) {
return this . head ;
}
/ * *
* Encapsulates context when attempting a match . Includes some fixed state like the
@ -544,16 +586,16 @@ public class PathPattern implements Comparable<PathPattern> {
@@ -544,16 +586,16 @@ public class PathPattern implements Comparable<PathPattern> {
* /
class MatchingContext {
// The candidate path to attempt a match against
char [ ] candidate ;
final PathContainer candidate ;
// The length of the candidate path
int candidateLength ;
final List < Element > pathElements ;
final int pathLength ;
boolean isMatchStartMatching = false ;
@Nullable
private Map < String , String > extractedVariables ;
private Map < String , PathMatchResult > extractedVariables ;
boolean extractingVariables ;
@ -564,9 +606,10 @@ public class PathPattern implements Comparable<PathPattern> {
@@ -564,9 +606,10 @@ public class PathPattern implements Comparable<PathPattern> {
// points to the remaining path that wasn't consumed
int remainingPathIndex ;
public MatchingContext ( String path , boolean extractVariables ) {
candidate = path . toCharArray ( ) ;
candidateLength = candidate . length ;
public MatchingContext ( PathContainer pathContainer , boolean extractVariables ) {
candidate = pathContainer ;
pathElements = pathContainer . elements ( ) ;
pathLength = pathElements . size ( ) ;
this . extractingVariables = extractVariables ;
}
@ -582,14 +625,14 @@ public class PathPattern implements Comparable<PathPattern> {
@@ -582,14 +625,14 @@ public class PathPattern implements Comparable<PathPattern> {
isMatchStartMatching = b ;
}
public void set ( String key , String value ) {
public void set ( String key , String value , MultiValueMap < String , String > parameters ) {
if ( this . extractedVariables = = null ) {
extractedVariables = new HashMap < > ( ) ;
}
extractedVariables . put ( key , value ) ;
extractedVariables . put ( key , new PathMatchResult ( key , value , parameters ) ) ;
}
public Map < String , String > getExtractedVariables ( ) {
public Map < String , PathMatchResult > getExtractedVariables ( ) {
if ( this . extractedVariables = = null ) {
return Collections . emptyMap ( ) ;
}
@ -599,20 +642,54 @@ public class PathPattern implements Comparable<PathPattern> {
@@ -599,20 +642,54 @@ public class PathPattern implements Comparable<PathPattern> {
}
/ * *
* Scan ahead from the specified position for either the next separator
* character or the end of the candidate .
* @param pos the starting position for the scan
* @return the position of the next separator or the end of the candidate
* @param pathIndex possible index of a separator
* @return true if element at specified index is a separator
* /
public int scanAhead ( int pos ) {
while ( pos < candidateLength ) {
if ( candidate [ pos ] = = separator ) {
return pos ;
}
pos + + ;
boolean isSeparator ( int pathIndex ) {
return pathElements . get ( pathIndex ) instanceof Separator ;
}
/ * *
* @param pathIndex path element index
* @return decoded value of the specified element
* /
String pathElementValue ( int pathIndex ) {
Element element = ( pathIndex < pathLength ) ? pathElements . get ( pathIndex ) : null ;
if ( element instanceof Segment ) {
return ( ( Segment ) element ) . valueDecoded ( ) ;
}
return candidateLength ;
return "" ;
}
}
/ * *
* 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 ) = = this . separator ) ;
boolean path2StartsWithSeparator = ( path2 . charAt ( 0 ) = = this . separator ) ;
if ( path1EndsWithSeparator & & path2StartsWithSeparator ) {
return path1 + path2 . substring ( 1 ) ;
}
else if ( path1EndsWithSeparator | | path2StartsWithSeparator ) {
return path1 + path2 ;
}
else {
return path1 + this . separator + path2 ;
}
}
/ * *
* @param container a path container
* @return true if the container is not null and has more than zero elements
* /
private boolean hasLength ( PathContainer container ) {
return container ! = null & & container . elements ( ) . size ( ) > 0 ;
}
}