1 /* 2 * Hunt - A refined core library for D programming language. 3 * 4 * Copyright (C) 2018-2019 HuntLabs 5 * 6 * Website: https://www.huntlabs.net/ 7 * 8 * Licensed under the Apache-2.0 License. 9 * 10 */ 11 12 module hunt.text.PathMatcher; 13 14 import hunt.collection; 15 import hunt.Boolean; 16 import hunt.Exceptions; 17 import hunt.Nullable; 18 import hunt.text.Common; 19 import hunt.text.StringUtils; 20 import hunt.text.StringBuilder; 21 22 import std.array; 23 import std.string; 24 import std.regex; 25 import std.container.array; 26 27 /** 28 * Strategy interface for {@code string}-based path matching. 29 * 30 * <p>Used by {@link hunt.framework.core.io.support.PathMatchingResourcePatternResolver}, 31 * {@link hunt.framework.web.servlet.handler.AbstractUrlHandlerMapping}, 32 * and {@link hunt.framework.web.servlet.mvc.WebContentInterceptor}. 33 * 34 * <p>The default implementation is {@link AntPathMatcher}, supporting the 35 * Ant-style pattern syntax. 36 * 37 * @author Juergen Hoeller 38 * @since 1.2 39 * @see AntPathMatcher 40 */ 41 interface PathMatcher { 42 43 /** 44 * Does the given {@code path} represent a pattern that can be matched 45 * by an implementation of this interface? 46 * <p>If the return value is {@code false}, then the {@link #match} 47 * method does not have to be used because direct equality comparisons 48 * on the static path Strings will lead to the same result. 49 * @param path the path string to check 50 * @return {@code true} if the given {@code path} represents a pattern 51 */ 52 bool isPattern(string path); 53 54 /** 55 * Match the given {@code path} against the given {@code pattern}, 56 * according to this PathMatcher's matching strategy. 57 * @param pattern the pattern to match against 58 * @param path the path string to test 59 * @return {@code true} if the supplied {@code path} matched, 60 * {@code false} if it didn't 61 */ 62 bool match(string pattern, string path); 63 64 /** 65 * Match the given {@code path} against the corresponding part of the given 66 * {@code pattern}, according to this PathMatcher's matching strategy. 67 * <p>Determines whether the pattern at least matches as far as the given base 68 * path goes, assuming that a full path may then match as well. 69 * @param pattern the pattern to match against 70 * @param path the path string to test 71 * @return {@code true} if the supplied {@code path} matched, 72 * {@code false} if it didn't 73 */ 74 bool matchStart(string pattern, string path); 75 76 /** 77 * Given a pattern and a full path, determine the pattern-mapped part. 78 * <p>This method is supposed to find out which part of the path is matched 79 * dynamically through an actual pattern, that is, it strips off a statically 80 * defined leading path from the given full path, returning only the actually 81 * pattern-matched part of the path. 82 * <p>For example: For "myroot/*.html" as pattern and "myroot/myfile.html" 83 * as full path, this method should return "myfile.html". The detailed 84 * determination rules are specified to this PathMatcher's matching strategy. 85 * <p>A simple implementation may return the given full path as-is in case 86 * of an actual pattern, and the empty string in case of the pattern not 87 * containing any dynamic parts (i.e. the {@code pattern} parameter being 88 * a static path that wouldn't qualify as an actual {@link #isPattern pattern}). 89 * A sophisticated implementation will differentiate between the static parts 90 * and the dynamic parts of the given path pattern. 91 * @param pattern the path pattern 92 * @param path the full path to introspect 93 * @return the pattern-mapped part of the given {@code path} 94 * (never {@code null}) 95 */ 96 string extractPathWithinPattern(string pattern, string path); 97 98 /** 99 * Given a pattern and a full path, extract the URI template variables. URI template 100 * variables are expressed through curly brackets ('{' and '}'). 101 * <p>For example: For pattern "/hotels/{hotel}" and path "/hotels/1", this method will 102 * return a map containing "hotel"->"1". 103 * @param pattern the path pattern, possibly containing URI templates 104 * @param path the full path to extract template variables from 105 * @return a map, containing variable names as keys; variables values as values 106 */ 107 Map!(string, string) extractUriTemplateVariables(string pattern, string path); 108 109 /** 110 * Given a full path, returns a {@link Comparator} suitable for sorting patterns 111 * in order of explicitness for that path. 112 * <p>The full algorithm used depends on the underlying implementation, 113 * but generally, the returned {@code Comparator} will 114 * {@linkplain java.util.List#sort(java.util.Comparator) sort} 115 * a list so that more specific patterns come before generic patterns. 116 * @param path the full path to use for comparison 117 * @return a comparator capable of sorting patterns in order of explicitness 118 */ 119 // Comparator!(string) getPatternComparator(string path); 120 121 /** 122 * Combines two patterns into a new pattern that is returned. 123 * <p>The full algorithm used for combining the two pattern depends on the underlying implementation. 124 * @param pattern1 the first pattern 125 * @param pattern2 the second pattern 126 * @return the combination of the two patterns 127 * @throws IllegalArgumentException when the two patterns cannot be combined 128 */ 129 string combine(string pattern1, string pattern2); 130 131 } 132 133 134 135 private enum Regex!char VARIABLE_PATTERN = regex("\\{[^/]+?\\}"); 136 137 /** 138 * {@link PathMatcher} implementation for Ant-style path patterns. 139 * 140 * <p>Part of this mapping code has been kindly borrowed from <a href="http://ant.apache.org">Apache Ant</a>. 141 * 142 * <p>The mapping matches URLs using the following rules:<br> 143 * <ul> 144 * <li>{@code ?} matches one character</li> 145 * <li>{@code *} matches zero or more characters</li> 146 * <li>{@code **} matches zero or more <em>directories</em> in a path</li> 147 * <li>{@code {spring:[a-z]+}} matches the regexp {@code [a-z]+} as a path variable named "spring"</li> 148 * </ul> 149 * 150 * <h3>Examples</h3> 151 * <ul> 152 * <li>{@code com/t?st.jsp} — matches {@code com/test.jsp} but also 153 * {@code com/tast.jsp} or {@code com/txst.jsp}</li> 154 * <li>{@code com/*.jsp} — matches all {@code .jsp} files in the 155 * {@code com} directory</li> 156 * <li><code>com/**/test.jsp</code> — matches all {@code test.jsp} 157 * files underneath the {@code com} path</li> 158 * <li><code>org/springframework/**/*.jsp</code> — matches all 159 * {@code .jsp} files underneath the {@code org/springframework} path</li> 160 * <li><code>org/**/servlet/bla.jsp</code> — matches 161 * {@code org/springframework/servlet/bla.jsp} but also 162 * {@code org/springframework/testing/servlet/bla.jsp} and {@code org/servlet/bla.jsp}</li> 163 * <li>{@code com/{filename:\\w+}.jsp} will match {@code com/test.jsp} and assign the value {@code test} 164 * to the {@code filename} variable</li> 165 * </ul> 166 * 167 * <p><strong>Note:</strong> a pattern and a path must both be absolute or must 168 * both be relative in order for the two to match. Therefore it is recommended 169 * that users of this implementation to sanitize patterns in order to prefix 170 * them with "/" as it makes sense in the context in which they're used. 171 * 172 * @author Alef Arendsen 173 * @author Juergen Hoeller 174 * @author Rob Harrop 175 * @author Arjen Poutsma 176 * @author Rossen Stoyanchev 177 * @author Sam Brannen 178 * @since 16.07.2003 179 */ 180 class AntPathMatcher : PathMatcher { 181 182 /** Default path separator: "/" */ 183 enum string DEFAULT_PATH_SEPARATOR = "/"; 184 185 private enum int CACHE_TURNOFF_THRESHOLD = 65536; 186 187 private enum char[] WILDCARD_CHARS = ['*', '?', '{' ]; 188 189 private string pathSeparator; 190 191 private PathSeparatorPatternCache pathSeparatorPatternCache; 192 193 private bool caseSensitive = true; 194 195 private bool trimTokens = false; 196 197 private Boolean cachePatterns; 198 199 private Map!(string, string[]) tokenizedPatternCache; 200 201 Map!(string, AntPathStringMatcher) stringMatcherCache; 202 203 /** 204 * Create a new instance with the {@link #DEFAULT_PATH_SEPARATOR}. 205 */ 206 this() { 207 this.pathSeparator = DEFAULT_PATH_SEPARATOR; 208 this.pathSeparatorPatternCache = new PathSeparatorPatternCache(DEFAULT_PATH_SEPARATOR); 209 initialize(); 210 } 211 212 /** 213 * A convenient, alternative constructor to use with a custom path separator. 214 * @param pathSeparator the path separator to use, must not be {@code null}. 215 * @since 4.1 216 */ 217 this(string pathSeparator) { 218 assert(pathSeparator, "'pathSeparator' is required"); 219 this.pathSeparator = pathSeparator; 220 this.pathSeparatorPatternCache = new PathSeparatorPatternCache(pathSeparator); 221 initialize(); 222 } 223 224 private void initialize() { 225 tokenizedPatternCache = new HashMap!(string, string[])(256); //new ConcurrentHashMap<>(256); 226 stringMatcherCache = new HashMap!(string, AntPathStringMatcher)(256); 227 } 228 229 230 /** 231 * Set the path separator to use for pattern parsing. 232 * <p>Default is "/", as in Ant. 233 */ 234 void setPathSeparator( string pathSeparator) { 235 this.pathSeparator = (pathSeparator !is null ? pathSeparator : DEFAULT_PATH_SEPARATOR); 236 this.pathSeparatorPatternCache = new PathSeparatorPatternCache(this.pathSeparator); 237 } 238 239 /** 240 * Specify whether to perform pattern matching in a case-sensitive fashion. 241 * <p>Default is {@code true}. Switch this to {@code false} for case-insensitive matching. 242 * @since 4.2 243 */ 244 void setCaseSensitive(bool caseSensitive) { 245 this.caseSensitive = caseSensitive; 246 } 247 248 /** 249 * Specify whether to trim tokenized paths and patterns. 250 * <p>Default is {@code false}. 251 */ 252 void setTrimTokens(bool trimTokens) { 253 this.trimTokens = trimTokens; 254 } 255 256 /** 257 * Specify whether to cache parsed pattern metadata for patterns passed 258 * into this matcher's {@link #match} method. A value of {@code true} 259 * activates an unlimited pattern cache; a value of {@code false} turns 260 * the pattern cache off completely. 261 * <p>Default is for the cache to be on, but with the variant to automatically 262 * turn it off when encountering too many patterns to cache at runtime 263 * (the threshold is 65536), assuming that arbitrary permutations of patterns 264 * are coming in, with little chance for encountering a recurring pattern. 265 * @since 4.0.1 266 * @see #getStringMatcher(string) 267 */ 268 void setCachePatterns(bool cachePatterns) { 269 this.cachePatterns = cachePatterns; 270 } 271 272 private void deactivatePatternCache() { 273 this.cachePatterns = false; 274 this.tokenizedPatternCache.clear(); 275 this.stringMatcherCache.clear(); 276 } 277 278 279 override 280 bool isPattern(string path) { 281 return (path.indexOf('*') != -1 || path.indexOf('?') != -1); 282 } 283 284 override 285 bool match(string pattern, string path) { 286 return doMatch(pattern, path, true, null); 287 } 288 289 override 290 bool matchStart(string pattern, string path) { 291 return doMatch(pattern, path, false, null); 292 } 293 294 /** 295 * Actually match the given {@code path} against the given {@code pattern}. 296 * @param pattern the pattern to match against 297 * @param path the path string to test 298 * @param fullMatch whether a full pattern match is required (else a pattern match 299 * as far as the given base path goes is sufficient) 300 * @return {@code true} if the supplied {@code path} matched, {@code false} if it didn't 301 */ 302 protected bool doMatch(string pattern, string path, bool fullMatch, 303 Map!(string, string) uriTemplateVariables) { 304 305 // if (path.startsWith(this.pathSeparator) != pattern.startsWith(this.pathSeparator)) { 306 // return false; 307 // } 308 309 // string[] pattDirs = tokenizePattern(pattern); 310 // if (fullMatch && this.caseSensitive && !isPotentialMatch(path, pattDirs)) { 311 // return false; 312 // } 313 314 // string[] pathDirs = tokenizePath(path); 315 316 // int pattIdxStart = 0; 317 // int pattIdxEnd = cast(int)pattDirs.length - 1; 318 // int pathIdxStart = 0; 319 // int pathIdxEnd = cast(int)pathDirs.length - 1; 320 321 // // Match all elements up to the first ** 322 // while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) { 323 // string pattDir = pattDirs[pattIdxStart]; 324 // if ("**".equals(pattDir)) { 325 // break; 326 // } 327 // if (!matchStrings(pattDir, pathDirs[pathIdxStart], uriTemplateVariables)) { 328 // return false; 329 // } 330 // pattIdxStart++; 331 // pathIdxStart++; 332 // } 333 334 // if (pathIdxStart > pathIdxEnd) { 335 // // Path is exhausted, only match if rest of pattern is * or **'s 336 // if (pattIdxStart > pattIdxEnd) { 337 // return (pattern.endsWith(this.pathSeparator) == path.endsWith(this.pathSeparator)); 338 // } 339 // if (!fullMatch) { 340 // return true; 341 // } 342 // if (pattIdxStart == pattIdxEnd && pattDirs[pattIdxStart].equals("*") && path.endsWith(this.pathSeparator)) { 343 // return true; 344 // } 345 // for (int i = pattIdxStart; i <= pattIdxEnd; i++) { 346 // if (!pattDirs[i].equals("**")) { 347 // return false; 348 // } 349 // } 350 // return true; 351 // } 352 // else if (pattIdxStart > pattIdxEnd) { 353 // // string not exhausted, but pattern is. Failure. 354 // return false; 355 // } 356 // else if (!fullMatch && "**".equals(pattDirs[pattIdxStart])) { 357 // // Path start definitely matches due to "**" part in pattern. 358 // return true; 359 // } 360 361 // // up to last '**' 362 // while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) { 363 // string pattDir = pattDirs[pattIdxEnd]; 364 // if (pattDir.equals("**")) { 365 // break; 366 // } 367 // if (!matchStrings(pattDir, pathDirs[pathIdxEnd], uriTemplateVariables)) { 368 // return false; 369 // } 370 // pattIdxEnd--; 371 // pathIdxEnd--; 372 // } 373 // if (pathIdxStart > pathIdxEnd) { 374 // // string is exhausted 375 // for (int i = pattIdxStart; i <= pattIdxEnd; i++) { 376 // if (!pattDirs[i].equals("**")) { 377 // return false; 378 // } 379 // } 380 // return true; 381 // } 382 383 // while (pattIdxStart != pattIdxEnd && pathIdxStart <= pathIdxEnd) { 384 // int patIdxTmp = -1; 385 // for (int i = pattIdxStart + 1; i <= pattIdxEnd; i++) { 386 // if (pattDirs[i].equals("**")) { 387 // patIdxTmp = i; 388 // break; 389 // } 390 // } 391 // if (patIdxTmp == pattIdxStart + 1) { 392 // // '**/**' situation, so skip one 393 // pattIdxStart++; 394 // continue; 395 // } 396 // // Find the pattern between padIdxStart & padIdxTmp in str between 397 // // strIdxStart & strIdxEnd 398 // int patLength = (patIdxTmp - pattIdxStart - 1); 399 // int strLength = (pathIdxEnd - pathIdxStart + 1); 400 // int foundIdx = -1; 401 402 // strLoop: 403 // for (int i = 0; i <= strLength - patLength; i++) { 404 // for (int j = 0; j < patLength; j++) { 405 // string subPat = pattDirs[pattIdxStart + j + 1]; 406 // string subStr = pathDirs[pathIdxStart + i + j]; 407 // if (!matchStrings(subPat, subStr, uriTemplateVariables)) { 408 // continue strLoop; 409 // } 410 // } 411 // foundIdx = pathIdxStart + i; 412 // break; 413 // } 414 415 // if (foundIdx == -1) { 416 // return false; 417 // } 418 419 // pattIdxStart = patIdxTmp; 420 // pathIdxStart = foundIdx + patLength; 421 // } 422 423 // for (int i = pattIdxStart; i <= pattIdxEnd; i++) { 424 // if (!pattDirs[i].equals("**")) { 425 // return false; 426 // } 427 // } 428 429 return true; 430 } 431 432 private bool isPotentialMatch(string path, string[] pattDirs) { 433 if (!this.trimTokens) { 434 int pos = 0; 435 foreach (string pattDir ; pattDirs) { 436 int skipped = skipSeparator(path, pos, this.pathSeparator); 437 pos += skipped; 438 skipped = skipSegment(path, pos, pattDir); 439 if (skipped < pattDir.length) { 440 return (skipped > 0 || (pattDir.length > 0 && isWildcardChar(pattDir.charAt(0)))); 441 } 442 pos += skipped; 443 } 444 } 445 return true; 446 } 447 448 private int skipSegment(string path, int pos, string prefix) { 449 int skipped = 0; 450 for (int i = 0; i < prefix.length; i++) { 451 char c = prefix.charAt(i); 452 if (isWildcardChar(c)) { 453 return skipped; 454 } 455 int currPos = pos + skipped; 456 if (currPos >= path.length) { 457 return 0; 458 } 459 if (c == path.charAt(currPos)) { 460 skipped++; 461 } 462 } 463 return skipped; 464 } 465 466 private int skipSeparator(string path, int pos, string separator) { 467 int skipped = 0; 468 while (path.startsWith(separator, pos + skipped)) { 469 skipped += separator.length; 470 } 471 return skipped; 472 } 473 474 private bool isWildcardChar(char c) { 475 foreach (char candidate ; WILDCARD_CHARS) { 476 if (c == candidate) { 477 return true; 478 } 479 } 480 return false; 481 } 482 483 /** 484 * Tokenize the given path pattern into parts, based on this matcher's settings. 485 * <p>Performs caching based on {@link #setCachePatterns}, delegating to 486 * {@link #tokenizePath(string)} for the actual tokenization algorithm. 487 * @param pattern the pattern to tokenize 488 * @return the tokenized pattern parts 489 */ 490 // protected string[] tokenizePattern(string pattern) { 491 // string[] tokenized = null; 492 // Boolean cachePatterns = this.cachePatterns; 493 // if (cachePatterns is null || cachePatterns.booleanValue()) { 494 // tokenized = this.tokenizedPatternCache.get(pattern); 495 // } 496 // if (tokenized is null) { 497 // tokenized = tokenizePath(pattern); 498 // if (cachePatterns is null && this.tokenizedPatternCache.size() >= CACHE_TURNOFF_THRESHOLD) { 499 // // Try to adapt to the runtime situation that we're encountering: 500 // // There are obviously too many different patterns coming in here... 501 // // So let's turn off the cache since the patterns are unlikely to be reoccurring. 502 // deactivatePatternCache(); 503 // return tokenized; 504 // } 505 // if (cachePatterns is null || cachePatterns.booleanValue()) { 506 // this.tokenizedPatternCache.put(pattern, tokenized); 507 // } 508 // } 509 // return tokenized; 510 // } 511 512 /** 513 * Tokenize the given path string into parts, based on this matcher's settings. 514 * @param path the path to tokenize 515 * @return the tokenized path parts 516 */ 517 // protected string[] tokenizePath(string path) { 518 // return StringUtils.tokenizeToStringArray(path, this.pathSeparator, this.trimTokens, true); 519 // } 520 521 /** 522 * Test whether or not a string matches against a pattern. 523 * @param pattern the pattern to match against (never {@code null}) 524 * @param str the string which must be matched against the pattern (never {@code null}) 525 * @return {@code true} if the string matches against the pattern, or {@code false} otherwise 526 */ 527 // private bool matchStrings(string pattern, string str, 528 // Map!(string, string) uriTemplateVariables) { 529 530 // return getStringMatcher(pattern).matchStrings(str, uriTemplateVariables); 531 // } 532 533 /** 534 * Build or retrieve an {@link AntPathStringMatcher} for the given pattern. 535 * <p>The default implementation checks this AntPathMatcher's internal cache 536 * (see {@link #setCachePatterns}), creating a new AntPathStringMatcher instance 537 * if no cached copy is found. 538 * <p>When encountering too many patterns to cache at runtime (the threshold is 65536), 539 * it turns the default cache off, assuming that arbitrary permutations of patterns 540 * are coming in, with little chance for encountering a recurring pattern. 541 * <p>This method may be overridden to implement a custom cache strategy. 542 * @param pattern the pattern to match against (never {@code null}) 543 * @return a corresponding AntPathStringMatcher (never {@code null}) 544 * @see #setCachePatterns 545 */ 546 protected AntPathStringMatcher getStringMatcher(string pattern) { 547 AntPathStringMatcher matcher = null; 548 Boolean cachePatterns = this.cachePatterns; 549 if (cachePatterns is null || cachePatterns) { 550 matcher = this.stringMatcherCache.get(pattern); 551 } 552 if (matcher is null) { 553 matcher = new AntPathStringMatcher(pattern, this.caseSensitive); 554 if (cachePatterns is null && this.stringMatcherCache.size() >= CACHE_TURNOFF_THRESHOLD) { 555 // Try to adapt to the runtime situation that we're encountering: 556 // There are obviously too many different patterns coming in here... 557 // So let's turn off the cache since the patterns are unlikely to be reoccurring. 558 deactivatePatternCache(); 559 return matcher; 560 } 561 if (cachePatterns is null || cachePatterns) { 562 this.stringMatcherCache.put(pattern, matcher); 563 } 564 } 565 return matcher; 566 } 567 568 /** 569 * Given a pattern and a full path, determine the pattern-mapped part. <p>For example: <ul> 570 * <li>'{@code /docs/cvs/commit.html}' and '{@code /docs/cvs/commit.html} -> ''</li> 571 * <li>'{@code /docs/*}' and '{@code /docs/cvs/commit} -> '{@code cvs/commit}'</li> 572 * <li>'{@code /docs/cvs/*.html}' and '{@code /docs/cvs/commit.html} -> '{@code commit.html}'</li> 573 * <li>'{@code /docs/**}' and '{@code /docs/cvs/commit} -> '{@code cvs/commit}'</li> 574 * <li>'{@code /docs/**\/*.html}' and '{@code /docs/cvs/commit.html} -> '{@code cvs/commit.html}'</li> 575 * <li>'{@code /*.html}' and '{@code /docs/cvs/commit.html} -> '{@code docs/cvs/commit.html}'</li> 576 * <li>'{@code *.html}' and '{@code /docs/cvs/commit.html} -> '{@code /docs/cvs/commit.html}'</li> 577 * <li>'{@code *}' and '{@code /docs/cvs/commit.html} -> '{@code /docs/cvs/commit.html}'</li> </ul> 578 * <p>Assumes that {@link #match} returns {@code true} for '{@code pattern}' and '{@code path}', but 579 * does <strong>not</strong> enforce this. 580 */ 581 override 582 string extractPathWithinPattern(string pattern, string path) { 583 // string[] patternParts = StringUtils.tokenizeToStringArray(pattern, this.pathSeparator, this.trimTokens, true); 584 // string[] pathParts = StringUtils.tokenizeToStringArray(path, this.pathSeparator, this.trimTokens, true); 585 // StringBuilder builder = new StringBuilder(); 586 // bool pathStarted = false; 587 588 // for (int segment = 0; segment < patternParts.length; segment++) { 589 // string patternPart = patternParts[segment]; 590 // if (patternPart.indexOf('*') > -1 || patternPart.indexOf('?') > -1) { 591 // for (; segment < pathParts.length; segment++) { 592 // if (pathStarted || (segment == 0 && !pattern.startsWith(this.pathSeparator))) { 593 // builder.append(this.pathSeparator); 594 // } 595 // builder.append(pathParts[segment]); 596 // pathStarted = true; 597 // } 598 // } 599 // } 600 601 // return builder.toString(); 602 return path; 603 } 604 605 override 606 Map!(string, string) extractUriTemplateVariables(string pattern, string path) { 607 Map!(string, string) variables = new LinkedHashMap!(string, string)(); 608 bool result = doMatch(pattern, path, true, variables); 609 if (!result) { 610 throw new IllegalStateException("Pattern \"" ~ pattern ~ "\" is not a match for \"" ~ path ~ "\""); 611 } 612 return variables; 613 } 614 615 /** 616 * Combine two patterns into a new pattern. 617 * <p>This implementation simply concatenates the two patterns, unless 618 * the first pattern contains a file extension match (e.g., {@code *.html}). 619 * In that case, the second pattern will be merged into the first. Otherwise, 620 * an {@code IllegalArgumentException} will be thrown. 621 * <h3>Examples</h3> 622 * <table border="1"> 623 * <tr><th>Pattern 1</th><th>Pattern 2</th><th>Result</th></tr> 624 * <tr><td>{@code null}</td><td>{@code null}</td><td> </td></tr> 625 * <tr><td>/hotels</td><td>{@code null}</td><td>/hotels</td></tr> 626 * <tr><td>{@code null}</td><td>/hotels</td><td>/hotels</td></tr> 627 * <tr><td>/hotels</td><td>/bookings</td><td>/hotels/bookings</td></tr> 628 * <tr><td>/hotels</td><td>bookings</td><td>/hotels/bookings</td></tr> 629 * <tr><td>/hotels/*</td><td>/bookings</td><td>/hotels/bookings</td></tr> 630 * <tr><td>/hotels/**</td><td>/bookings</td><td>/hotels/**/bookings</td></tr> 631 * <tr><td>/hotels</td><td>{hotel}</td><td>/hotels/{hotel}</td></tr> 632 * <tr><td>/hotels/*</td><td>{hotel}</td><td>/hotels/{hotel}</td></tr> 633 * <tr><td>/hotels/**</td><td>{hotel}</td><td>/hotels/**/{hotel}</td></tr> 634 * <tr><td>/*.html</td><td>/hotels.html</td><td>/hotels.html</td></tr> 635 * <tr><td>/*.html</td><td>/hotels</td><td>/hotels.html</td></tr> 636 * <tr><td>/*.html</td><td>/*.txt</td><td>{@code IllegalArgumentException}</td></tr> 637 * </table> 638 * @param pattern1 the first pattern 639 * @param pattern2 the second pattern 640 * @return the combination of the two patterns 641 * @throws IllegalArgumentException if the two patterns cannot be combined 642 */ 643 override 644 string combine(string pattern1, string pattern2) { 645 if (pattern1.empty() && pattern2.empty()) { 646 return ""; 647 } 648 if (pattern1.empty()) { 649 return pattern2; 650 } 651 if (pattern2.empty()) { 652 return pattern1; 653 } 654 655 bool pattern1ContainsUriVar = (pattern1.indexOf('{') != -1); 656 if (!pattern1.equals(pattern2) && !pattern1ContainsUriVar && match(pattern1, pattern2)) { 657 // /* + /hotel -> /hotel ; "/*.*" ~ "/*.html" -> /*.html 658 // However /user + /user -> /usr/user ; /{foo} + /bar -> /{foo}/bar 659 return pattern2; 660 } 661 662 // /hotels/* + /booking -> /hotels/booking 663 // /hotels/* + booking -> /hotels/booking 664 if (pattern1.endsWith(this.pathSeparatorPatternCache.getEndsOnWildCard())) { 665 return concat(pattern1.substring(0, pattern1.length - 2), pattern2); 666 } 667 668 // /hotels/** + /booking -> /hotels/**/booking 669 // /hotels/** + booking -> /hotels/**/booking 670 if (pattern1.endsWith(this.pathSeparatorPatternCache.getEndsOnDoubleWildCard())) { 671 return concat(pattern1, pattern2); 672 } 673 674 int starDotPos1 = cast(int)pattern1.indexOf("*."); 675 if (pattern1ContainsUriVar || starDotPos1 == -1 || this.pathSeparator.equals(".")) { 676 // simply concatenate the two patterns 677 return concat(pattern1, pattern2); 678 } 679 680 string ext1 = pattern1.substring(starDotPos1 + 1); 681 int dotPos2 = cast(int)pattern2.indexOf('.'); 682 string file2 = (dotPos2 == -1 ? pattern2 : pattern2.substring(0, dotPos2)); 683 string ext2 = (dotPos2 == -1 ? "" : pattern2.substring(dotPos2)); 684 bool ext1All = (ext1.equals(".*") || ext1.equals("")); 685 bool ext2All = (ext2.equals(".*") || ext2.equals("")); 686 if (!ext1All && !ext2All) { 687 throw new IllegalArgumentException("Cannot combine patterns: " ~ pattern1 ~ " vs " ~ pattern2); 688 } 689 string ext = (ext1All ? ext2 : ext1); 690 return file2 ~ ext; 691 } 692 693 private string concat(string path1, string path2) { 694 bool path1EndsWithSeparator = path1.endsWith(this.pathSeparator); 695 bool path2StartsWithSeparator = path2.startsWith(this.pathSeparator); 696 697 if (path1EndsWithSeparator && path2StartsWithSeparator) { 698 return path1 ~ path2[1 .. $]; 699 } 700 else if (path1EndsWithSeparator || path2StartsWithSeparator) { 701 return path1 ~ path2; 702 } 703 else { 704 return path1 ~ this.pathSeparator ~ path2; 705 } 706 } 707 708 /** 709 * Given a full path, returns a {@link Comparator} suitable for sorting patterns in order of 710 * explicitness. 711 * <p>This{@code Comparator} will {@linkplain java.util.List#sort(Comparator) sort} 712 * a list so that more specific patterns (without uri templates or wild cards) come before 713 * generic patterns. So given a list with the following patterns: 714 * <ol> 715 * <li>{@code /hotels/new}</li> 716 * <li>{@code /hotels/{hotel}}</li> <li>{@code /hotels/*}</li> 717 * </ol> 718 * the returned comparator will sort this list so that the order will be as indicated. 719 * <p>The full path given as parameter is used to test for exact matches. So when the given path 720 * is {@code /hotels/2}, the pattern {@code /hotels/2} will be sorted before {@code /hotels/1}. 721 * @param path the full path to use for comparison 722 * @return a comparator capable of sorting patterns in order of explicitness 723 */ 724 // override 725 // Comparator!(string) getPatternComparator(string path) { 726 // return new AntPatternComparator(path); 727 // } 728 729 } 730 731 732 733 /** 734 * Tests whether or not a string matches against a pattern via a {@link Pattern}. 735 * <p>The pattern may contain special characters: '*' means zero or more characters; '?' means one and 736 * only one character; '{' and '}' indicate a URI template pattern. For example <tt>/users/{user}</tt>. 737 */ 738 protected static class AntPathStringMatcher { 739 740 private enum Regex!char GLOB_PATTERN = regex("\\?|\\*|\\{((?:\\{[^/]+?\\}|[^/{}]|\\\\[{}])+?)\\}"); 741 742 private enum string DEFAULT_VARIABLE_PATTERN = "(.*)"; 743 744 private Regex!char pattern; 745 746 private Array!(string) variableNames; 747 748 this(string pattern) { 749 this(pattern, true); 750 } 751 752 this(string pattern, bool caseSensitive) { 753 StringBuilder patternBuilder = new StringBuilder(); 754 RegexMatch!string matcher = matchAll(pattern, GLOB_PATTERN); 755 while(!matcher.empty) { 756 Captures!string item = matcher.front; 757 string match = item.front; 758 patternBuilder.append(quote(match)); 759 if ("?" == match) { 760 patternBuilder.append('.'); 761 } 762 else if ("*" == match) { 763 patternBuilder.append(".*"); 764 } 765 else if (match.startsWith("{") && match.endsWith("}")) { 766 int colonIdx = cast(int)match.indexOf(':'); 767 if (colonIdx == -1) { 768 patternBuilder.append(DEFAULT_VARIABLE_PATTERN); 769 this.variableNames.insertBack(matcher.post()); 770 } 771 else { 772 string variablePattern = match[colonIdx + 1 .. $ - 1]; 773 patternBuilder.append('('); 774 patternBuilder.append(variablePattern); 775 patternBuilder.append(')'); 776 string variableName = match[1 .. colonIdx]; 777 this.variableNames.insertBack(variableName); 778 } 779 } 780 781 matcher.popFront(); 782 } 783 // patternBuilder.append(quote(pattern, end, pattern.length)); 784 // this.pattern = (caseSensitive ? Pattern.compile(patternBuilder.toString()) : 785 // Pattern.compile(patternBuilder.toString(), Pattern.CASE_INSENSITIVE)); 786 this.pattern = regex(patternBuilder.toString()); 787 } 788 789 /** 790 * Returns a literal pattern <code>string</code> for the specified 791 * <code>string</code>. 792 * 793 * <p>This method produces a <code>string</code> that can be used to 794 * create a <code>Pattern</code> that would match the string 795 * <code>s</code> as if it were a literal pattern.</p> Metacharacters 796 * or escape sequences in the input sequence will be given no special 797 * meaning. 798 * 799 * @param s The string to be literalized 800 * @return A literal string replacement 801 * @since 1.5 802 */ 803 static string quote(string s) { 804 int slashEIndex = cast(int)s.indexOf("\\E"); 805 if (slashEIndex == -1) 806 return "\\Q" ~ s ~ "\\E"; 807 808 StringBuilder sb = new StringBuilder(s.length * 2); 809 sb.append("\\Q"); 810 slashEIndex = 0; 811 int current = 0; 812 while ((slashEIndex = cast(int)s.indexOf("\\E", current)) != -1) { 813 sb.append(s.substring(current, slashEIndex)); 814 current = slashEIndex + 2; 815 sb.append("\\E\\\\E\\Q"); 816 } 817 sb.append(s.substring(current, s.length)); 818 sb.append("\\E"); 819 return sb.toString(); 820 } 821 822 /** 823 * Main entry point. 824 * @return {@code true} if the string matches against the pattern, or {@code false} otherwise. 825 */ 826 bool matchStrings(string str, Map!(string, string) uriTemplateVariables) { 827 // RegexMatch!string matcher = matchAll(str, this.pattern); 828 // if (matcher.matches()) { 829 // if (uriTemplateVariables !is null) { 830 // // SPR-8455 831 // if (this.variableNames.length != matcher.groupCount()) { 832 // throw new IllegalArgumentException("The number of capturing groups in the pattern segment " ~ 833 // to!string(this.pattern) ~ " does not match the number of URI template variables it defines, " ~ 834 // "which can occur if capturing groups are used in a URI template regex. " ~ 835 // "Use non-capturing groups instead."); 836 // } 837 // for (int i = 1; i <= matcher.groupCount(); i++) { 838 // string name = this.variableNames[i - 1]; 839 // string value = matcher.group(i); 840 // uriTemplateVariables.put(name, value); 841 // } 842 // } 843 // return true; 844 // } 845 // else { 846 // return false; 847 // } 848 return true; 849 } 850 } 851 852 853 854 /** 855 * The default {@link Comparator} implementation returned by 856 * {@link #getPatternComparator(string)}. 857 * <p>In order, the most "generic" pattern is determined by the following: 858 * <ul> 859 * <li>if it's null or a capture all pattern (i.e. it is equal to "/**")</li> 860 * <li>if the other pattern is an actual match</li> 861 * <li>if it's a catch-all pattern (i.e. it ends with "**"</li> 862 * <li>if it's got more "*" than the other pattern</li> 863 * <li>if it's got more "{foo}" than the other pattern</li> 864 * <li>if it's shorter than the other pattern</li> 865 * </ul> 866 */ 867 // protected class AntPatternComparator : Comparator!(string) { 868 869 // private string path; 870 871 // this(string path) { 872 // this.path = path; 873 // } 874 875 // /** 876 // * Compare two patterns to determine which should match first, i.e. which 877 // * is the most specific regarding the current path. 878 // * @return a negative integer, zero, or a positive integer as pattern1 is 879 // * more specific, equally specific, or less specific than pattern2. 880 // */ 881 // override 882 // int compare(string pattern1, string pattern2) { 883 // PatternInfo info1 = new PatternInfo(pattern1); 884 // PatternInfo info2 = new PatternInfo(pattern2); 885 886 // if (info1.isLeastSpecific() && info2.isLeastSpecific()) { 887 // return 0; 888 // } 889 // else if (info1.isLeastSpecific()) { 890 // return 1; 891 // } 892 // else if (info2.isLeastSpecific()) { 893 // return -1; 894 // } 895 896 // bool pattern1EqualsPath = pattern1.equals(path); 897 // bool pattern2EqualsPath = pattern2.equals(path); 898 // if (pattern1EqualsPath && pattern2EqualsPath) { 899 // return 0; 900 // } 901 // else if (pattern1EqualsPath) { 902 // return -1; 903 // } 904 // else if (pattern2EqualsPath) { 905 // return 1; 906 // } 907 908 // if (info1.isPrefixPattern() && info2.getDoubleWildcards() == 0) { 909 // return 1; 910 // } 911 // else if (info2.isPrefixPattern() && info1.getDoubleWildcards() == 0) { 912 // return -1; 913 // } 914 915 // if (info1.getTotalCount() != info2.getTotalCount()) { 916 // return info1.getTotalCount() - info2.getTotalCount(); 917 // } 918 919 // if (info1.getLength() != info2.getLength()) { 920 // return info2.getLength() - info1.getLength(); 921 // } 922 923 // if (info1.getSingleWildcards() < info2.getSingleWildcards()) { 924 // return -1; 925 // } 926 // else if (info2.getSingleWildcards() < info1.getSingleWildcards()) { 927 // return 1; 928 // } 929 930 // if (info1.getUriVars() < info2.getUriVars()) { 931 // return -1; 932 // } 933 // else if (info2.getUriVars() < info1.getUriVars()) { 934 // return 1; 935 // } 936 937 // return 0; 938 // } 939 // } 940 941 942 /** 943 * Value class that holds information about the pattern, e.g. number of 944 * occurrences of "*", "**", and "{" pattern elements. 945 */ 946 private class PatternInfo { 947 948 private string pattern; 949 950 private int uriVars; 951 952 private int singleWildcards; 953 954 private int doubleWildcards; 955 956 private bool catchAllPattern; 957 958 private bool prefixPattern; 959 960 private int length; 961 962 this( string pattern) { 963 this.pattern = pattern; 964 if (this.pattern !is null) { 965 initCounters(); 966 this.catchAllPattern = this.pattern.equals("/**"); 967 this.prefixPattern = !this.catchAllPattern && this.pattern.endsWith("/**"); 968 } 969 if (this.uriVars == 0) { 970 this.length = cast(int) (this.pattern !is null ? this.pattern.length : 0); 971 } 972 } 973 974 protected void initCounters() { 975 int pos = 0; 976 if (this.pattern !is null) { 977 while (pos < this.pattern.length) { 978 if (this.pattern.charAt(pos) == '{') { 979 this.uriVars++; 980 pos++; 981 } 982 else if (this.pattern.charAt(pos) == '*') { 983 if (pos + 1 < this.pattern.length && this.pattern.charAt(pos + 1) == '*') { 984 this.doubleWildcards++; 985 pos += 2; 986 } 987 else if (pos > 0 && !this.pattern.substring(pos - 1).equals(".*")) { 988 this.singleWildcards++; 989 pos++; 990 } 991 else { 992 pos++; 993 } 994 } 995 else { 996 pos++; 997 } 998 } 999 } 1000 } 1001 1002 int getUriVars() { 1003 return this.uriVars; 1004 } 1005 1006 int getSingleWildcards() { 1007 return this.singleWildcards; 1008 } 1009 1010 int getDoubleWildcards() { 1011 return this.doubleWildcards; 1012 } 1013 1014 bool isLeastSpecific() { 1015 return (this.pattern is null || this.catchAllPattern); 1016 } 1017 1018 bool isPrefixPattern() { 1019 return this.prefixPattern; 1020 } 1021 1022 int getTotalCount() { 1023 return this.uriVars + this.singleWildcards + (2 * this.doubleWildcards); 1024 } 1025 1026 /** 1027 * Returns the length of the given pattern, where template variables are considered to be 1 long. 1028 */ 1029 int getLength() { 1030 // if (this.length == 0 && !this.pattern.empty) { 1031 // Captures!string m = matchFirst(this.pattern, VARIABLE_PATTERN); 1032 // string r = 1033 // this.length = 1034 // VARIABLE_PATTERN.matcher(this.pattern).replaceAll("#").length ; 1035 // } 1036 return this.length; 1037 } 1038 } 1039 1040 /** 1041 * A simple cache for patterns that depend on the configured path separator. 1042 */ 1043 private class PathSeparatorPatternCache { 1044 1045 private string endsOnWildCard; 1046 1047 private string endsOnDoubleWildCard; 1048 1049 this(string pathSeparator) { 1050 this.endsOnWildCard = pathSeparator ~ "*"; 1051 this.endsOnDoubleWildCard = pathSeparator ~ "**"; 1052 } 1053 1054 string getEndsOnWildCard() { 1055 return this.endsOnWildCard; 1056 } 1057 1058 string getEndsOnDoubleWildCard() { 1059 return this.endsOnDoubleWildCard; 1060 } 1061 }