001package io.jboot.utils; 002 003import java.util.*; 004import java.util.concurrent.ConcurrentHashMap; 005import java.util.regex.Matcher; 006import java.util.regex.Pattern; 007 008 009public class AntPathMatcher { 010 011 /** 012 * Default path separator: "/". 013 */ 014 public static final String DEFAULT_PATH_SEPARATOR = "/"; 015 016 private static final int CACHE_TURNOFF_THRESHOLD = 65536; 017 018 private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\{[^/]+?}"); 019 020 private static final char[] WILDCARD_CHARS = {'*', '?', '{'}; 021 022 023 private String pathSeparator; 024 025 private PathSeparatorPatternCache pathSeparatorPatternCache; 026 027 private boolean caseSensitive = true; 028 029 private boolean trimTokens = false; 030 031 032 private volatile Boolean cachePatterns; 033 034 private final Map<String, String[]> tokenizedPatternCache = new ConcurrentHashMap<>(256); 035 036 final Map<String, AntPathStringMatcher> stringMatcherCache = new ConcurrentHashMap<>(256); 037 038 039 /** 040 * Create a new instance with the {@link #DEFAULT_PATH_SEPARATOR}. 041 */ 042 public AntPathMatcher() { 043 this.pathSeparator = DEFAULT_PATH_SEPARATOR; 044 this.pathSeparatorPatternCache = new PathSeparatorPatternCache(DEFAULT_PATH_SEPARATOR); 045 } 046 047 /** 048 * A convenient, alternative constructor to use with a custom path separator. 049 * 050 * @param pathSeparator the path separator to use, must not be {@code null}. 051 */ 052 public AntPathMatcher(String pathSeparator) { 053 this.pathSeparator = pathSeparator; 054 this.pathSeparatorPatternCache = new PathSeparatorPatternCache(pathSeparator); 055 } 056 057 058 /** 059 * Set the path separator to use for pattern parsing. 060 * <p>Default is "/", as in Ant. 061 */ 062 public void setPathSeparator(String pathSeparator) { 063 this.pathSeparator = (pathSeparator != null ? pathSeparator : DEFAULT_PATH_SEPARATOR); 064 this.pathSeparatorPatternCache = new PathSeparatorPatternCache(this.pathSeparator); 065 } 066 067 /** 068 * Specify whether to perform pattern matching in a case-sensitive fashion. 069 * <p>Default is {@code true}. Switch this to {@code false} for case-insensitive matching. * 070 */ 071 public void setCaseSensitive(boolean caseSensitive) { 072 this.caseSensitive = caseSensitive; 073 } 074 075 /** 076 * Specify whether to trim tokenized paths and patterns. 077 * <p>Default is {@code false}. 078 */ 079 public void setTrimTokens(boolean trimTokens) { 080 this.trimTokens = trimTokens; 081 } 082 083 /** 084 * Specify whether to cache parsed pattern metadata for patterns passed 085 * into this matcher's {@link #match} method. A value of {@code true} 086 * activates an unlimited pattern cache; a value of {@code false} turns 087 * the pattern cache off completely. 088 * <p>Default is for the cache to be on, but with the variant to automatically 089 * turn it off when encountering too many patterns to cache at runtime 090 * (the threshold is 65536), assuming that arbitrary permutations of patterns 091 * are coming in, with little chance for encountering a recurring pattern. 092 * 093 * @see #getStringMatcher(String) 094 */ 095 public void setCachePatterns(boolean cachePatterns) { 096 this.cachePatterns = cachePatterns; 097 } 098 099 private void deactivatePatternCache() { 100 this.cachePatterns = false; 101 this.tokenizedPatternCache.clear(); 102 this.stringMatcherCache.clear(); 103 } 104 105 106 public boolean isPattern(String path) { 107 if (path == null) { 108 return false; 109 } 110 boolean uriVar = false; 111 for (int i = 0; i < path.length(); i++) { 112 char c = path.charAt(i); 113 if (c == '*' || c == '?') { 114 return true; 115 } 116 if (c == '{') { 117 uriVar = true; 118 continue; 119 } 120 if (c == '}' && uriVar) { 121 return true; 122 } 123 } 124 return false; 125 } 126 127 128 public boolean match(String pattern, String path) { 129 return doMatch(pattern, path, true, null); 130 } 131 132 133 public boolean matchStart(String pattern, String path) { 134 return doMatch(pattern, path, false, null); 135 } 136 137 /** 138 * Actually match the given {@code path} against the given {@code pattern}. 139 * 140 * @param pattern the pattern to match against 141 * @param path the path to test 142 * @param fullMatch whether a full pattern match is required (else a pattern match 143 * as far as the given base path goes is sufficient) 144 * @return {@code true} if the supplied {@code path} matched, {@code false} if it didn't 145 */ 146 protected boolean doMatch(String pattern, String path, boolean fullMatch, 147 Map<String, String> uriTemplateVariables) { 148 149 if (path == null || path.startsWith(this.pathSeparator) != pattern.startsWith(this.pathSeparator)) { 150 return false; 151 } 152 153 String[] pattDirs = tokenizePattern(pattern); 154 if (fullMatch && this.caseSensitive && !isPotentialMatch(path, pattDirs)) { 155 return false; 156 } 157 158 String[] pathDirs = tokenizePath(path); 159 int pattIdxStart = 0; 160 int pattIdxEnd = pattDirs.length - 1; 161 int pathIdxStart = 0; 162 int pathIdxEnd = pathDirs.length - 1; 163 164 // Match all elements up to the first ** 165 while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) { 166 String pattDir = pattDirs[pattIdxStart]; 167 if ("**".equals(pattDir)) { 168 break; 169 } 170 if (!matchStrings(pattDir, pathDirs[pathIdxStart], uriTemplateVariables)) { 171 return false; 172 } 173 pattIdxStart++; 174 pathIdxStart++; 175 } 176 177 if (pathIdxStart > pathIdxEnd) { 178 // Path is exhausted, only match if rest of pattern is * or **'s 179 if (pattIdxStart > pattIdxEnd) { 180 return (pattern.endsWith(this.pathSeparator) == path.endsWith(this.pathSeparator)); 181 } 182 if (!fullMatch) { 183 return true; 184 } 185 if (pattIdxStart == pattIdxEnd && pattDirs[pattIdxStart].equals("*") && path.endsWith(this.pathSeparator)) { 186 return true; 187 } 188 for (int i = pattIdxStart; i <= pattIdxEnd; i++) { 189 if (!pattDirs[i].equals("**")) { 190 return false; 191 } 192 } 193 return true; 194 } else if (pattIdxStart > pattIdxEnd) { 195 // String not exhausted, but pattern is. Failure. 196 return false; 197 } else if (!fullMatch && "**".equals(pattDirs[pattIdxStart])) { 198 // Path start definitely matches due to "**" part in pattern. 199 return true; 200 } 201 202 // up to last '**' 203 while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) { 204 String pattDir = pattDirs[pattIdxEnd]; 205 if (pattDir.equals("**")) { 206 break; 207 } 208 if (!matchStrings(pattDir, pathDirs[pathIdxEnd], uriTemplateVariables)) { 209 return false; 210 } 211 pattIdxEnd--; 212 pathIdxEnd--; 213 } 214 if (pathIdxStart > pathIdxEnd) { 215 // String is exhausted 216 for (int i = pattIdxStart; i <= pattIdxEnd; i++) { 217 if (!pattDirs[i].equals("**")) { 218 return false; 219 } 220 } 221 return true; 222 } 223 224 while (pattIdxStart != pattIdxEnd && pathIdxStart <= pathIdxEnd) { 225 int patIdxTmp = -1; 226 for (int i = pattIdxStart + 1; i <= pattIdxEnd; i++) { 227 if (pattDirs[i].equals("**")) { 228 patIdxTmp = i; 229 break; 230 } 231 } 232 if (patIdxTmp == pattIdxStart + 1) { 233 // '**/**' situation, so skip one 234 pattIdxStart++; 235 continue; 236 } 237 // Find the pattern between padIdxStart & padIdxTmp in str between 238 // strIdxStart & strIdxEnd 239 int patLength = (patIdxTmp - pattIdxStart - 1); 240 int strLength = (pathIdxEnd - pathIdxStart + 1); 241 int foundIdx = -1; 242 243 strLoop: 244 for (int i = 0; i <= strLength - patLength; i++) { 245 for (int j = 0; j < patLength; j++) { 246 String subPat = pattDirs[pattIdxStart + j + 1]; 247 String subStr = pathDirs[pathIdxStart + i + j]; 248 if (!matchStrings(subPat, subStr, uriTemplateVariables)) { 249 continue strLoop; 250 } 251 } 252 foundIdx = pathIdxStart + i; 253 break; 254 } 255 256 if (foundIdx == -1) { 257 return false; 258 } 259 260 pattIdxStart = patIdxTmp; 261 pathIdxStart = foundIdx + patLength; 262 } 263 264 for (int i = pattIdxStart; i <= pattIdxEnd; i++) { 265 if (!pattDirs[i].equals("**")) { 266 return false; 267 } 268 } 269 270 return true; 271 } 272 273 private boolean isPotentialMatch(String path, String[] pattDirs) { 274 if (!this.trimTokens) { 275 int pos = 0; 276 for (String pattDir : pattDirs) { 277 int skipped = skipSeparator(path, pos, this.pathSeparator); 278 pos += skipped; 279 skipped = skipSegment(path, pos, pattDir); 280 if (skipped < pattDir.length()) { 281 return (skipped > 0 || (pattDir.length() > 0 && isWildcardChar(pattDir.charAt(0)))); 282 } 283 pos += skipped; 284 } 285 } 286 return true; 287 } 288 289 private int skipSegment(String path, int pos, String prefix) { 290 int skipped = 0; 291 for (int i = 0; i < prefix.length(); i++) { 292 char c = prefix.charAt(i); 293 if (isWildcardChar(c)) { 294 return skipped; 295 } 296 int currPos = pos + skipped; 297 if (currPos >= path.length()) { 298 return 0; 299 } 300 if (c == path.charAt(currPos)) { 301 skipped++; 302 } 303 } 304 return skipped; 305 } 306 307 private int skipSeparator(String path, int pos, String separator) { 308 int skipped = 0; 309 while (path.startsWith(separator, pos + skipped)) { 310 skipped += separator.length(); 311 } 312 return skipped; 313 } 314 315 private boolean isWildcardChar(char c) { 316 for (char candidate : WILDCARD_CHARS) { 317 if (c == candidate) { 318 return true; 319 } 320 } 321 return false; 322 } 323 324 /** 325 * Tokenize the given path pattern into parts, based on this matcher's settings. 326 * <p>Performs caching based on {@link #setCachePatterns}, delegating to 327 * {@link #tokenizePath(String)} for the actual tokenization algorithm. 328 * 329 * @param pattern the pattern to tokenize 330 * @return the tokenized pattern parts 331 */ 332 protected String[] tokenizePattern(String pattern) { 333 String[] tokenized = null; 334 Boolean cachePatterns = this.cachePatterns; 335 if (cachePatterns == null || cachePatterns.booleanValue()) { 336 tokenized = this.tokenizedPatternCache.get(pattern); 337 } 338 if (tokenized == null) { 339 tokenized = tokenizePath(pattern); 340 if (cachePatterns == null && this.tokenizedPatternCache.size() >= CACHE_TURNOFF_THRESHOLD) { 341 // Try to adapt to the runtime situation that we're encountering: 342 // There are obviously too many different patterns coming in here... 343 // So let's turn off the cache since the patterns are unlikely to be reoccurring. 344 deactivatePatternCache(); 345 return tokenized; 346 } 347 if (cachePatterns == null || cachePatterns.booleanValue()) { 348 this.tokenizedPatternCache.put(pattern, tokenized); 349 } 350 } 351 return tokenized; 352 } 353 354 /** 355 * Tokenize the given path into parts, based on this matcher's settings. 356 * 357 * @param path the path to tokenize 358 * @return the tokenized path parts 359 */ 360 protected String[] tokenizePath(String path) { 361 return StringUtils.tokenizeToStringArray(path, this.pathSeparator, this.trimTokens, true); 362 } 363 364 /** 365 * Test whether or not a string matches against a pattern. 366 * 367 * @param pattern the pattern to match against (never {@code null}) 368 * @param str the String which must be matched against the pattern (never {@code null}) 369 * @return {@code true} if the string matches against the pattern, or {@code false} otherwise 370 */ 371 private boolean matchStrings(String pattern, String str, 372 Map<String, String> uriTemplateVariables) { 373 374 return getStringMatcher(pattern).matchStrings(str, uriTemplateVariables); 375 } 376 377 /** 378 * Build or retrieve an {@link AntPathStringMatcher} for the given pattern. 379 * <p>The default implementation checks this AntPathMatcher's internal cache 380 * (see {@link #setCachePatterns}), creating a new AntPathStringMatcher instance 381 * if no cached copy is found. 382 * <p>When encountering too many patterns to cache at runtime (the threshold is 65536), 383 * it turns the default cache off, assuming that arbitrary permutations of patterns 384 * are coming in, with little chance for encountering a recurring pattern. 385 * <p>This method may be overridden to implement a custom cache strategy. 386 * 387 * @param pattern the pattern to match against (never {@code null}) 388 * @return a corresponding AntPathStringMatcher (never {@code null}) 389 * @see #setCachePatterns 390 */ 391 protected AntPathStringMatcher getStringMatcher(String pattern) { 392 AntPathStringMatcher matcher = null; 393 Boolean cachePatterns = this.cachePatterns; 394 if (cachePatterns == null || cachePatterns.booleanValue()) { 395 matcher = this.stringMatcherCache.get(pattern); 396 } 397 if (matcher == null) { 398 matcher = new AntPathStringMatcher(pattern, this.caseSensitive); 399 if (cachePatterns == null && this.stringMatcherCache.size() >= CACHE_TURNOFF_THRESHOLD) { 400 // Try to adapt to the runtime situation that we're encountering: 401 // There are obviously too many different patterns coming in here... 402 // So let's turn off the cache since the patterns are unlikely to be reoccurring. 403 deactivatePatternCache(); 404 return matcher; 405 } 406 if (cachePatterns == null || cachePatterns.booleanValue()) { 407 this.stringMatcherCache.put(pattern, matcher); 408 } 409 } 410 return matcher; 411 } 412 413 /** 414 * Given a pattern and a full path, determine the pattern-mapped part. <p>For example: <ul> 415 * <li>'{@code /docs/cvs/commit.html}' and '{@code /docs/cvs/commit.html} -> ''</li> 416 * <li>'{@code /docs/*}' and '{@code /docs/cvs/commit} -> '{@code cvs/commit}'</li> 417 * <li>'{@code /docs/cvs/*.html}' and '{@code /docs/cvs/commit.html} -> '{@code commit.html}'</li> 418 * <li>'{@code /docs/**}' and '{@code /docs/cvs/commit} -> '{@code cvs/commit}'</li> 419 * <li>'{@code /docs/**\/*.html}' and '{@code /docs/cvs/commit.html} -> '{@code cvs/commit.html}'</li> 420 * <li>'{@code /*.html}' and '{@code /docs/cvs/commit.html} -> '{@code docs/cvs/commit.html}'</li> 421 * <li>'{@code *.html}' and '{@code /docs/cvs/commit.html} -> '{@code /docs/cvs/commit.html}'</li> 422 * <li>'{@code *}' and '{@code /docs/cvs/commit.html} -> '{@code /docs/cvs/commit.html}'</li> </ul> 423 * <p>Assumes that {@link #match} returns {@code true} for '{@code pattern}' and '{@code path}', but 424 * does <strong>not</strong> enforce this. 425 */ 426 427 public String extractPathWithinPattern(String pattern, String path) { 428 if (pattern == null){ 429 return ""; 430 } 431 String[] patternParts = StringUtils.tokenizeToStringArray(pattern, this.pathSeparator, this.trimTokens, true); 432 String[] pathParts = StringUtils.tokenizeToStringArray(path, this.pathSeparator, this.trimTokens, true); 433 StringBuilder builder = new StringBuilder(); 434 boolean pathStarted = false; 435 436 for (int segment = 0; segment < patternParts.length; segment++) { 437 String patternPart = patternParts[segment]; 438 if (patternPart.indexOf('*') > -1 || patternPart.indexOf('?') > -1) { 439 for (; segment < pathParts.length; segment++) { 440 if (pathStarted || (segment == 0 && !pattern.startsWith(this.pathSeparator))) { 441 builder.append(this.pathSeparator); 442 } 443 builder.append(pathParts[segment]); 444 pathStarted = true; 445 } 446 } 447 } 448 449 return builder.toString(); 450 } 451 452 453 public Map<String, String> extractUriTemplateVariables(String pattern, String path) { 454 Map<String, String> variables = new LinkedHashMap<>(); 455 boolean result = doMatch(pattern, path, true, variables); 456 if (!result) { 457 throw new IllegalStateException("Pattern \"" + pattern + "\" is not a match for \"" + path + "\""); 458 } 459 return variables; 460 } 461 462 /** 463 * Given a full path, returns a {@link Comparator} suitable for sorting patterns in order of 464 * explicitness. 465 * <p>This {@code Comparator} will {@linkplain List#sort(Comparator) sort} 466 * a list so that more specific patterns (without URI templates or wild cards) come before 467 * generic patterns. So given a list with the following patterns, the returned comparator 468 * will sort this list so that the order will be as indicated. 469 * <ol> 470 * <li>{@code /hotels/new}</li> 471 * <li>{@code /hotels/{hotel}}</li> 472 * <li>{@code /hotels/*}</li> 473 * </ol> 474 * <p>The full path given as parameter is used to test for exact matches. So when the given path 475 * is {@code /hotels/2}, the pattern {@code /hotels/2} will be sorted before {@code /hotels/1}. 476 * 477 * @param path the full path to use for comparison 478 * @return a comparator capable of sorting patterns in order of explicitness 479 */ 480 481 public Comparator<String> getPatternComparator(String path) { 482 return new AntPatternComparator(path); 483 } 484 485 486 /** 487 * Tests whether or not a string matches against a pattern via a {@link Pattern}. 488 * <p>The pattern may contain special characters: '*' means zero or more characters; '?' means one and 489 * only one character; '{' and '}' indicate a URI template pattern. For example <tt>/users/{user}</tt>. 490 */ 491 protected static class AntPathStringMatcher { 492 493 private static final Pattern GLOB_PATTERN = Pattern.compile("\\?|\\*|\\{((?:\\{[^/]+?}|[^/{}]|\\\\[{}])+?)}"); 494 495 private static final String DEFAULT_VARIABLE_PATTERN = "((?s).*)"; 496 497 private final String rawPattern; 498 499 private final boolean caseSensitive; 500 501 private final boolean exactMatch; 502 503 504 private final Pattern pattern; 505 506 private final List<String> variableNames = new ArrayList<>(); 507 508 public AntPathStringMatcher(String pattern) { 509 this(pattern, true); 510 } 511 512 public AntPathStringMatcher(String pattern, boolean caseSensitive) { 513 this.rawPattern = pattern; 514 this.caseSensitive = caseSensitive; 515 StringBuilder patternBuilder = new StringBuilder(); 516 Matcher matcher = GLOB_PATTERN.matcher(pattern); 517 int end = 0; 518 while (matcher.find()) { 519 patternBuilder.append(quote(pattern, end, matcher.start())); 520 String match = matcher.group(); 521 if ("?".equals(match)) { 522 patternBuilder.append('.'); 523 } else if ("*".equals(match)) { 524 patternBuilder.append(".*"); 525 } else if (match.startsWith("{") && match.endsWith("}")) { 526 int colonIdx = match.indexOf(':'); 527 if (colonIdx == -1) { 528 patternBuilder.append(DEFAULT_VARIABLE_PATTERN); 529 this.variableNames.add(matcher.group(1)); 530 } else { 531 String variablePattern = match.substring(colonIdx + 1, match.length() - 1); 532 patternBuilder.append('('); 533 patternBuilder.append(variablePattern); 534 patternBuilder.append(')'); 535 String variableName = match.substring(1, colonIdx); 536 this.variableNames.add(variableName); 537 } 538 } 539 end = matcher.end(); 540 } 541 // No glob pattern was found, this is an exact String match 542 if (end == 0) { 543 this.exactMatch = true; 544 this.pattern = null; 545 } else { 546 this.exactMatch = false; 547 patternBuilder.append(quote(pattern, end, pattern.length())); 548 this.pattern = (this.caseSensitive ? Pattern.compile(patternBuilder.toString()) : 549 Pattern.compile(patternBuilder.toString(), Pattern.CASE_INSENSITIVE)); 550 } 551 } 552 553 private String quote(String s, int start, int end) { 554 if (start == end) { 555 return ""; 556 } 557 return Pattern.quote(s.substring(start, end)); 558 } 559 560 /** 561 * Main entry point. 562 * 563 * @return {@code true} if the string matches against the pattern, or {@code false} otherwise. 564 */ 565 public boolean matchStrings(String str, Map<String, String> uriTemplateVariables) { 566 if (this.exactMatch) { 567 return this.caseSensitive ? this.rawPattern.equals(str) : this.rawPattern.equalsIgnoreCase(str); 568 } else if (this.pattern != null) { 569 Matcher matcher = this.pattern.matcher(str); 570 if (matcher.matches()) { 571 if (uriTemplateVariables != null) { 572 if (this.variableNames.size() != matcher.groupCount()) { 573 throw new IllegalArgumentException("The number of capturing groups in the pattern segment " + 574 this.pattern + " does not match the number of URI template variables it defines, " + 575 "which can occur if capturing groups are used in a URI template regex. " + 576 "Use non-capturing groups instead."); 577 } 578 for (int i = 1; i <= matcher.groupCount(); i++) { 579 String name = this.variableNames.get(i - 1); 580 String value = matcher.group(i); 581 uriTemplateVariables.put(name, value); 582 } 583 } 584 return true; 585 } 586 } 587 return false; 588 } 589 590 } 591 592 593 /** 594 * The default {@link Comparator} implementation returned by 595 * {@link #getPatternComparator(String)}. 596 * <p>In order, the most "generic" pattern is determined by the following: 597 * <ul> 598 * <li>if it's null or a capture all pattern (i.e. it is equal to "/**")</li> 599 * <li>if the other pattern is an actual match</li> 600 * <li>if it's a catch-all pattern (i.e. it ends with "**"</li> 601 * <li>if it's got more "*" than the other pattern</li> 602 * <li>if it's got more "{foo}" than the other pattern</li> 603 * <li>if it's shorter than the other pattern</li> 604 * </ul> 605 */ 606 protected static class AntPatternComparator implements Comparator<String> { 607 608 private final String path; 609 610 public AntPatternComparator(String path) { 611 this.path = path; 612 } 613 614 /** 615 * Compare two patterns to determine which should match first, i.e. which 616 * is the most specific regarding the current path. 617 * 618 * @return a negative integer, zero, or a positive integer as pattern1 is 619 * more specific, equally specific, or less specific than pattern2. 620 */ 621 622 public int compare(String pattern1, String pattern2) { 623 PatternInfo info1 = new PatternInfo(pattern1); 624 PatternInfo info2 = new PatternInfo(pattern2); 625 626 if (info1.isLeastSpecific() && info2.isLeastSpecific()) { 627 return 0; 628 } else if (info1.isLeastSpecific()) { 629 return 1; 630 } else if (info2.isLeastSpecific()) { 631 return -1; 632 } 633 634 boolean pattern1EqualsPath = pattern1.equals(this.path); 635 boolean pattern2EqualsPath = pattern2.equals(this.path); 636 if (pattern1EqualsPath && pattern2EqualsPath) { 637 return 0; 638 } else if (pattern1EqualsPath) { 639 return -1; 640 } else if (pattern2EqualsPath) { 641 return 1; 642 } 643 644 if (info1.isPrefixPattern() && info2.isPrefixPattern()) { 645 return info2.getLength() - info1.getLength(); 646 } else if (info1.isPrefixPattern() && info2.getDoubleWildcards() == 0) { 647 return 1; 648 } else if (info2.isPrefixPattern() && info1.getDoubleWildcards() == 0) { 649 return -1; 650 } 651 652 if (info1.getTotalCount() != info2.getTotalCount()) { 653 return info1.getTotalCount() - info2.getTotalCount(); 654 } 655 656 if (info1.getLength() != info2.getLength()) { 657 return info2.getLength() - info1.getLength(); 658 } 659 660 if (info1.getSingleWildcards() < info2.getSingleWildcards()) { 661 return -1; 662 } else if (info2.getSingleWildcards() < info1.getSingleWildcards()) { 663 return 1; 664 } 665 666 if (info1.getUriVars() < info2.getUriVars()) { 667 return -1; 668 } else if (info2.getUriVars() < info1.getUriVars()) { 669 return 1; 670 } 671 672 return 0; 673 } 674 675 676 /** 677 * Value class that holds information about the pattern, e.g. number of 678 * occurrences of "*", "**", and "{" pattern elements. 679 */ 680 private static class PatternInfo { 681 682 683 private final String pattern; 684 685 private int uriVars; 686 687 private int singleWildcards; 688 689 private int doubleWildcards; 690 691 private boolean catchAllPattern; 692 693 private boolean prefixPattern; 694 695 696 private Integer length; 697 698 public PatternInfo(String pattern) { 699 this.pattern = pattern; 700 if (this.pattern != null) { 701 initCounters(); 702 this.catchAllPattern = this.pattern.equals("/**"); 703 this.prefixPattern = !this.catchAllPattern && this.pattern.endsWith("/**"); 704 } 705 if (this.uriVars == 0) { 706 this.length = (this.pattern != null ? this.pattern.length() : 0); 707 } 708 } 709 710 protected void initCounters() { 711 int pos = 0; 712 if (this.pattern != null) { 713 while (pos < this.pattern.length()) { 714 if (this.pattern.charAt(pos) == '{') { 715 this.uriVars++; 716 pos++; 717 } else if (this.pattern.charAt(pos) == '*') { 718 if (pos + 1 < this.pattern.length() && this.pattern.charAt(pos + 1) == '*') { 719 this.doubleWildcards++; 720 pos += 2; 721 } else if (pos > 0 && !this.pattern.substring(pos - 1).equals(".*")) { 722 this.singleWildcards++; 723 pos++; 724 } else { 725 pos++; 726 } 727 } else { 728 pos++; 729 } 730 } 731 } 732 } 733 734 public int getUriVars() { 735 return this.uriVars; 736 } 737 738 public int getSingleWildcards() { 739 return this.singleWildcards; 740 } 741 742 public int getDoubleWildcards() { 743 return this.doubleWildcards; 744 } 745 746 public boolean isLeastSpecific() { 747 return (this.pattern == null || this.catchAllPattern); 748 } 749 750 public boolean isPrefixPattern() { 751 return this.prefixPattern; 752 } 753 754 public int getTotalCount() { 755 return this.uriVars + this.singleWildcards + (2 * this.doubleWildcards); 756 } 757 758 /** 759 * Returns the length of the given pattern, where template variables are considered to be 1 long. 760 */ 761 public int getLength() { 762 if (this.length == null) { 763 this.length = (this.pattern != null ? 764 VARIABLE_PATTERN.matcher(this.pattern).replaceAll("#").length() : 0); 765 } 766 return this.length; 767 } 768 } 769 } 770 771 772 /** 773 * A simple cache for patterns that depend on the configured path separator. 774 */ 775 private static class PathSeparatorPatternCache { 776 777 private final String endsOnWildCard; 778 779 private final String endsOnDoubleWildCard; 780 781 public PathSeparatorPatternCache(String pathSeparator) { 782 this.endsOnWildCard = pathSeparator + "*"; 783 this.endsOnDoubleWildCard = pathSeparator + "**"; 784 } 785 786 public String getEndsOnWildCard() { 787 return this.endsOnWildCard; 788 } 789 790 public String getEndsOnDoubleWildCard() { 791 return this.endsOnDoubleWildCard; 792 } 793 } 794 795} 796 797abstract class StringUtils { 798 799 private static final String[] EMPTY_STRING_ARRAY = {}; 800 801 private static final String FOLDER_SEPARATOR = "/"; 802 803 private static final String WINDOWS_FOLDER_SEPARATOR = "\\"; 804 805 private static final String TOP_PATH = ".."; 806 807 private static final String CURRENT_PATH = "."; 808 809 private static final char EXTENSION_SEPARATOR = '.'; 810 811 812 public static String[] tokenizeToStringArray(String str, String delimiters) { 813 return tokenizeToStringArray(str, delimiters, true, true); 814 } 815 816 public static String[] tokenizeToStringArray( 817 String str, String delimiters, boolean trimTokens, boolean ignoreEmptyTokens) { 818 819 if (str == null) { 820 return EMPTY_STRING_ARRAY; 821 } 822 823 StringTokenizer st = new StringTokenizer(str, delimiters); 824 List<String> tokens = new ArrayList<>(); 825 while (st.hasMoreTokens()) { 826 String token = st.nextToken(); 827 if (trimTokens) { 828 token = token.trim(); 829 } 830 if (!ignoreEmptyTokens || token.length() > 0) { 831 tokens.add(token); 832 } 833 } 834 return toStringArray(tokens); 835 } 836 837 /** 838 * Copy the given {@link Collection} into a {@code String} array. 839 * <p>The {@code Collection} must contain {@code String} elements only. 840 * 841 * @param collection the {@code Collection} to copy 842 * (potentially {@code null} or empty) 843 * @return the resulting {@code String} array 844 */ 845 public static String[] toStringArray(Collection<String> collection) { 846 return ((collection != null && collection.size() != 0) ? collection.toArray(EMPTY_STRING_ARRAY) : EMPTY_STRING_ARRAY); 847 } 848 849}