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