001/* 002 * Copyright 2002-2020 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.web.util.pattern; 018 019import java.util.Collections; 020import java.util.Comparator; 021import java.util.HashMap; 022import java.util.List; 023import java.util.Map; 024import java.util.StringJoiner; 025 026import org.springframework.http.server.PathContainer; 027import org.springframework.http.server.PathContainer.Element; 028import org.springframework.http.server.PathContainer.Separator; 029import org.springframework.lang.Nullable; 030import org.springframework.util.CollectionUtils; 031import org.springframework.util.MultiValueMap; 032import org.springframework.util.StringUtils; 033 034/** 035 * Representation of a parsed path pattern. Includes a chain of path elements 036 * for fast matching and accumulates computed state for quick comparison of 037 * patterns. 038 * 039 * <p>{@code PathPattern} matches URL paths using the following rules:<br> 040 * <ul> 041 * <li>{@code ?} matches one character</li> 042 * <li>{@code *} matches zero or more characters within a path segment</li> 043 * <li>{@code **} matches zero or more <em>path segments</em> until the end of the path</li> 044 * <li><code>{spring}</code> matches a <em>path segment</em> and captures it as a variable named "spring"</li> 045 * <li><code>{spring:[a-z]+}</code> matches the regexp {@code [a-z]+} as a path variable named "spring"</li> 046 * <li><code>{*spring}</code> matches zero or more <em>path segments</em> until the end of the path 047 * and captures it as a variable named "spring"</li> 048 * </ul> 049 * 050 * Notable behavior difference with {@code AntPathMatcher}:<br> 051 * {@code **} and its capturing variant <code>{*spring}</code> cannot be used in the middle of a pattern 052 * string, only at the end: {@code /pages/{**}} is valid, but {@code /pages/{**}/details} is not. 053 * 054 * <h3>Examples</h3> 055 * <ul> 056 * <li>{@code /pages/t?st.html} — matches {@code /pages/test.html} as well as 057 * {@code /pages/tXst.html} but not {@code /pages/toast.html}</li> 058 * <li>{@code /resources/*.png} — matches all {@code .png} files in the 059 * {@code resources} directory</li> 060 * <li><code>/resources/**</code> — matches all files 061 * underneath the {@code /resources/} path, including {@code /resources/image.png} 062 * and {@code /resources/css/spring.css}</li> 063 * <li><code>/resources/{*path}</code> — matches all files 064 * underneath the {@code /resources/} path and captures their relative path in 065 * a variable named "path"; {@code /resources/image.png} will match with 066 * "path" → "/image.png", and {@code /resources/css/spring.css} will match 067 * with "path" → "/css/spring.css"</li> 068 * <li><code>/resources/{filename:\\w+}.dat</code> will match {@code /resources/spring.dat} 069 * and assign the value {@code "spring"} to the {@code filename} variable</li> 070 * </ul> 071 * 072 * @author Andy Clement 073 * @author Rossen Stoyanchev 074 * @since 5.0 075 * @see PathContainer 076 */ 077public class PathPattern implements Comparable<PathPattern> { 078 079 private static final PathContainer EMPTY_PATH = PathContainer.parsePath(""); 080 081 /** 082 * Comparator that sorts patterns by specificity as follows: 083 * <ol> 084 * <li>Null instances are last. 085 * <li>Catch-all patterns are last. 086 * <li>If both patterns are catch-all, consider the length (longer wins). 087 * <li>Compare wildcard and captured variable count (lower wins). 088 * <li>Consider length (longer wins) 089 * </ol> 090 */ 091 public static final Comparator<PathPattern> SPECIFICITY_COMPARATOR = 092 Comparator.nullsLast( 093 Comparator.<PathPattern> 094 comparingInt(p -> p.isCatchAll() ? 1 : 0) 095 .thenComparingInt(p -> p.isCatchAll() ? scoreByNormalizedLength(p) : 0) 096 .thenComparingInt(PathPattern::getScore) 097 .thenComparingInt(PathPattern::scoreByNormalizedLength) 098 ); 099 100 101 /** The text of the parsed pattern. */ 102 private final String patternString; 103 104 /** The parser used to construct this pattern. */ 105 private final PathPatternParser parser; 106 107 /** The options to use to parse a pattern. */ 108 private final PathContainer.Options pathOptions; 109 110 /** If this pattern has no trailing slash, allow candidates to include one and still match successfully. */ 111 private final boolean matchOptionalTrailingSeparator; 112 113 /** Will this match candidates in a case sensitive way? (case sensitivity at parse time). */ 114 private final boolean caseSensitive; 115 116 /** First path element in the parsed chain of path elements for this pattern. */ 117 @Nullable 118 private final PathElement head; 119 120 /** How many variables are captured in this pattern. */ 121 private int capturedVariableCount; 122 123 /** 124 * The normalized length is trying to measure the 'active' part of the pattern. It is computed 125 * by assuming all captured variables have a normalized length of 1. Effectively this means changing 126 * your variable name lengths isn't going to change the length of the active part of the pattern. 127 * Useful when comparing two patterns. 128 */ 129 private int normalizedLength; 130 131 /** 132 * Does the pattern end with '<separator>'. 133 */ 134 private boolean endsWithSeparatorWildcard = false; 135 136 /** 137 * Score is used to quickly compare patterns. Different pattern components are given different 138 * weights. A 'lower score' is more specific. Current weights: 139 * <ul> 140 * <li>Captured variables are worth 1 141 * <li>Wildcard is worth 100 142 * </ul> 143 */ 144 private int score; 145 146 /** Does the pattern end with {*...}. */ 147 private boolean catchAll = false; 148 149 150 PathPattern(String patternText, PathPatternParser parser, @Nullable PathElement head) { 151 this.patternString = patternText; 152 this.parser = parser; 153 this.pathOptions = parser.getPathOptions(); 154 this.matchOptionalTrailingSeparator = parser.isMatchOptionalTrailingSeparator(); 155 this.caseSensitive = parser.isCaseSensitive(); 156 this.head = head; 157 158 // Compute fields for fast comparison 159 PathElement elem = head; 160 while (elem != null) { 161 this.capturedVariableCount += elem.getCaptureCount(); 162 this.normalizedLength += elem.getNormalizedLength(); 163 this.score += elem.getScore(); 164 if (elem instanceof CaptureTheRestPathElement || elem instanceof WildcardTheRestPathElement) { 165 this.catchAll = true; 166 } 167 if (elem instanceof SeparatorPathElement && elem.next != null && 168 elem.next instanceof WildcardPathElement && elem.next.next == null) { 169 this.endsWithSeparatorWildcard = true; 170 } 171 elem = elem.next; 172 } 173 } 174 175 176 /** 177 * Return the original String that was parsed to create this PathPattern. 178 */ 179 public String getPatternString() { 180 return this.patternString; 181 } 182 183 /** 184 * Whether the pattern string contains pattern syntax that would require 185 * use of {@link #matches(PathContainer)}, or if it is a regular String that 186 * could be compared directly to others. 187 * @since 5.2 188 */ 189 public boolean hasPatternSyntax() { 190 return (this.score > 0 || this.patternString.indexOf('?') != -1); 191 } 192 193 /** 194 * Whether this pattern matches the given path. 195 * @param pathContainer the candidate path to attempt to match against 196 * @return {@code true} if the path matches this pattern 197 */ 198 public boolean matches(PathContainer pathContainer) { 199 if (this.head == null) { 200 return !hasLength(pathContainer) || 201 (this.matchOptionalTrailingSeparator && pathContainerIsJustSeparator(pathContainer)); 202 } 203 else if (!hasLength(pathContainer)) { 204 if (this.head instanceof WildcardTheRestPathElement || this.head instanceof CaptureTheRestPathElement) { 205 pathContainer = EMPTY_PATH; // Will allow CaptureTheRest to bind the variable to empty 206 } 207 else { 208 return false; 209 } 210 } 211 MatchingContext matchingContext = new MatchingContext(pathContainer, false); 212 return this.head.matches(0, matchingContext); 213 } 214 215 /** 216 * Match this pattern to the given URI path and return extracted URI template 217 * variables as well as path parameters (matrix variables). 218 * @param pathContainer the candidate path to attempt to match against 219 * @return info object with the extracted variables, or {@code null} for no match 220 */ 221 @Nullable 222 public PathMatchInfo matchAndExtract(PathContainer pathContainer) { 223 if (this.head == null) { 224 return (hasLength(pathContainer) && 225 !(this.matchOptionalTrailingSeparator && pathContainerIsJustSeparator(pathContainer)) ? 226 null : PathMatchInfo.EMPTY); 227 } 228 else if (!hasLength(pathContainer)) { 229 if (this.head instanceof WildcardTheRestPathElement || this.head instanceof CaptureTheRestPathElement) { 230 pathContainer = EMPTY_PATH; // Will allow CaptureTheRest to bind the variable to empty 231 } 232 else { 233 return null; 234 } 235 } 236 MatchingContext matchingContext = new MatchingContext(pathContainer, true); 237 return this.head.matches(0, matchingContext) ? matchingContext.getPathMatchResult() : null; 238 } 239 240 /** 241 * Match the beginning of the given path and return the remaining portion 242 * not covered by this pattern. This is useful for matching nested routes 243 * where the path is matched incrementally at each level. 244 * @param pathContainer the candidate path to attempt to match against 245 * @return info object with the match result or {@code null} for no match 246 */ 247 @Nullable 248 public PathRemainingMatchInfo matchStartOfPath(PathContainer pathContainer) { 249 if (this.head == null) { 250 return new PathRemainingMatchInfo(pathContainer); 251 } 252 else if (!hasLength(pathContainer)) { 253 return null; 254 } 255 256 MatchingContext matchingContext = new MatchingContext(pathContainer, true); 257 matchingContext.setMatchAllowExtraPath(); 258 boolean matches = this.head.matches(0, matchingContext); 259 if (!matches) { 260 return null; 261 } 262 else { 263 PathRemainingMatchInfo info; 264 if (matchingContext.remainingPathIndex == pathContainer.elements().size()) { 265 info = new PathRemainingMatchInfo(EMPTY_PATH, matchingContext.getPathMatchResult()); 266 } 267 else { 268 info = new PathRemainingMatchInfo(pathContainer.subPath(matchingContext.remainingPathIndex), 269 matchingContext.getPathMatchResult()); 270 } 271 return info; 272 } 273 } 274 275 /** 276 * Determine the pattern-mapped part for the given path. 277 * <p>For example: <ul> 278 * <li>'{@code /docs/cvs/commit.html}' and '{@code /docs/cvs/commit.html} → ''</li> 279 * <li>'{@code /docs/*}' and '{@code /docs/cvs/commit}' → '{@code cvs/commit}'</li> 280 * <li>'{@code /docs/cvs/*.html}' and '{@code /docs/cvs/commit.html} → '{@code commit.html}'</li> 281 * <li>'{@code /docs/**}' and '{@code /docs/cvs/commit} → '{@code cvs/commit}'</li> 282 * </ul> 283 * <p><b>Notes:</b> 284 * <ul> 285 * <li>Assumes that {@link #matches} returns {@code true} for 286 * the same path but does <strong>not</strong> enforce this. 287 * <li>Duplicate occurrences of separators within the returned result are removed 288 * <li>Leading and trailing separators are removed from the returned result 289 * </ul> 290 * @param path a path that matches this pattern 291 * @return the subset of the path that is matched by pattern or "" if none 292 * of it is matched by pattern elements 293 */ 294 public PathContainer extractPathWithinPattern(PathContainer path) { 295 List<Element> pathElements = path.elements(); 296 int pathElementsCount = pathElements.size(); 297 298 int startIndex = 0; 299 // Find first path element that is not a separator or a literal (i.e. the first pattern based element) 300 PathElement elem = this.head; 301 while (elem != null) { 302 if (elem.getWildcardCount() != 0 || elem.getCaptureCount() != 0) { 303 break; 304 } 305 elem = elem.next; 306 startIndex++; 307 } 308 if (elem == null) { 309 // There is no pattern piece 310 return PathContainer.parsePath(""); 311 } 312 313 // Skip leading separators that would be in the result 314 while (startIndex < pathElementsCount && (pathElements.get(startIndex) instanceof Separator)) { 315 startIndex++; 316 } 317 318 int endIndex = pathElements.size(); 319 // Skip trailing separators that would be in the result 320 while (endIndex > 0 && (pathElements.get(endIndex - 1) instanceof Separator)) { 321 endIndex--; 322 } 323 324 boolean multipleAdjacentSeparators = false; 325 for (int i = startIndex; i < (endIndex - 1); i++) { 326 if ((pathElements.get(i) instanceof Separator) && (pathElements.get(i+1) instanceof Separator)) { 327 multipleAdjacentSeparators=true; 328 break; 329 } 330 } 331 332 PathContainer resultPath = null; 333 if (multipleAdjacentSeparators) { 334 // Need to rebuild the path without the duplicate adjacent separators 335 StringBuilder buf = new StringBuilder(); 336 int i = startIndex; 337 while (i < endIndex) { 338 Element e = pathElements.get(i++); 339 buf.append(e.value()); 340 if (e instanceof Separator) { 341 while (i < endIndex && (pathElements.get(i) instanceof Separator)) { 342 i++; 343 } 344 } 345 } 346 resultPath = PathContainer.parsePath(buf.toString(), this.pathOptions); 347 } 348 else if (startIndex >= endIndex) { 349 resultPath = PathContainer.parsePath(""); 350 } 351 else { 352 resultPath = path.subPath(startIndex, endIndex); 353 } 354 return resultPath; 355 } 356 357 /** 358 * Compare this pattern with a supplied pattern: return -1,0,+1 if this pattern 359 * is more specific, the same or less specific than the supplied pattern. 360 * The aim is to sort more specific patterns first. 361 */ 362 @Override 363 public int compareTo(@Nullable PathPattern otherPattern) { 364 int result = SPECIFICITY_COMPARATOR.compare(this, otherPattern); 365 return (result == 0 && otherPattern != null ? 366 this.patternString.compareTo(otherPattern.patternString) : result); 367 } 368 369 /** 370 * Combine this pattern with another. 371 */ 372 public PathPattern combine(PathPattern pattern2string) { 373 // If one of them is empty the result is the other. If both empty the result is "" 374 if (!StringUtils.hasLength(this.patternString)) { 375 if (!StringUtils.hasLength(pattern2string.patternString)) { 376 return this.parser.parse(""); 377 } 378 else { 379 return pattern2string; 380 } 381 } 382 else if (!StringUtils.hasLength(pattern2string.patternString)) { 383 return this; 384 } 385 386 // /* + /hotel => /hotel 387 // /*.* + /*.html => /*.html 388 // However: 389 // /usr + /user => /usr/user 390 // /{foo} + /bar => /{foo}/bar 391 if (!this.patternString.equals(pattern2string.patternString) && this.capturedVariableCount == 0 && 392 matches(PathContainer.parsePath(pattern2string.patternString))) { 393 return pattern2string; 394 } 395 396 // /hotels/* + /booking => /hotels/booking 397 // /hotels/* + booking => /hotels/booking 398 if (this.endsWithSeparatorWildcard) { 399 return this.parser.parse(concat( 400 this.patternString.substring(0, this.patternString.length() - 2), 401 pattern2string.patternString)); 402 } 403 404 // /hotels + /booking => /hotels/booking 405 // /hotels + booking => /hotels/booking 406 int starDotPos1 = this.patternString.indexOf("*."); // Are there any file prefix/suffix things to consider? 407 if (this.capturedVariableCount != 0 || starDotPos1 == -1 || getSeparator() == '.') { 408 return this.parser.parse(concat(this.patternString, pattern2string.patternString)); 409 } 410 411 // /*.html + /hotel => /hotel.html 412 // /*.html + /hotel.* => /hotel.html 413 String firstExtension = this.patternString.substring(starDotPos1 + 1); // looking for the first extension 414 String p2string = pattern2string.patternString; 415 int dotPos2 = p2string.indexOf('.'); 416 String file2 = (dotPos2 == -1 ? p2string : p2string.substring(0, dotPos2)); 417 String secondExtension = (dotPos2 == -1 ? "" : p2string.substring(dotPos2)); 418 boolean firstExtensionWild = (firstExtension.equals(".*") || firstExtension.isEmpty()); 419 boolean secondExtensionWild = (secondExtension.equals(".*") || secondExtension.isEmpty()); 420 if (!firstExtensionWild && !secondExtensionWild) { 421 throw new IllegalArgumentException( 422 "Cannot combine patterns: " + this.patternString + " and " + pattern2string); 423 } 424 return this.parser.parse(file2 + (firstExtensionWild ? secondExtension : firstExtension)); 425 } 426 427 @Override 428 public boolean equals(@Nullable Object other) { 429 if (!(other instanceof PathPattern)) { 430 return false; 431 } 432 PathPattern otherPattern = (PathPattern) other; 433 return (this.patternString.equals(otherPattern.getPatternString()) && 434 getSeparator() == otherPattern.getSeparator() && 435 this.caseSensitive == otherPattern.caseSensitive); 436 } 437 438 @Override 439 public int hashCode() { 440 return (this.patternString.hashCode() + getSeparator()) * 17 + (this.caseSensitive ? 1 : 0); 441 } 442 443 @Override 444 public String toString() { 445 return this.patternString; 446 } 447 448 449 int getScore() { 450 return this.score; 451 } 452 453 boolean isCatchAll() { 454 return this.catchAll; 455 } 456 457 /** 458 * The normalized length is trying to measure the 'active' part of the pattern. It is computed 459 * by assuming all capture variables have a normalized length of 1. Effectively this means changing 460 * your variable name lengths isn't going to change the length of the active part of the pattern. 461 * Useful when comparing two patterns. 462 */ 463 int getNormalizedLength() { 464 return this.normalizedLength; 465 } 466 467 char getSeparator() { 468 return this.pathOptions.separator(); 469 } 470 471 int getCapturedVariableCount() { 472 return this.capturedVariableCount; 473 } 474 475 String toChainString() { 476 StringJoiner stringJoiner = new StringJoiner(" "); 477 PathElement pe = this.head; 478 while (pe != null) { 479 stringJoiner.add(pe.toString()); 480 pe = pe.next; 481 } 482 return stringJoiner.toString(); 483 } 484 485 /** 486 * Return the string form of the pattern built from walking the path element chain. 487 * @return the string form of the pattern 488 */ 489 String computePatternString() { 490 StringBuilder buf = new StringBuilder(); 491 PathElement pe = this.head; 492 while (pe != null) { 493 buf.append(pe.getChars()); 494 pe = pe.next; 495 } 496 return buf.toString(); 497 } 498 499 @Nullable 500 PathElement getHeadSection() { 501 return this.head; 502 } 503 504 /** 505 * Join two paths together including a separator if necessary. 506 * Extraneous separators are removed (if the first path 507 * ends with one and the second path starts with one). 508 * @param path1 first path 509 * @param path2 second path 510 * @return joined path that may include separator if necessary 511 */ 512 private String concat(String path1, String path2) { 513 boolean path1EndsWithSeparator = (path1.charAt(path1.length() - 1) == getSeparator()); 514 boolean path2StartsWithSeparator = (path2.charAt(0) == getSeparator()); 515 if (path1EndsWithSeparator && path2StartsWithSeparator) { 516 return path1 + path2.substring(1); 517 } 518 else if (path1EndsWithSeparator || path2StartsWithSeparator) { 519 return path1 + path2; 520 } 521 else { 522 return path1 + getSeparator() + path2; 523 } 524 } 525 526 /** 527 * Return if the container is not null and has more than zero elements. 528 * @param container a path container 529 * @return {@code true} has more than zero elements 530 */ 531 private boolean hasLength(@Nullable PathContainer container) { 532 return container != null && container.elements().size() > 0; 533 } 534 535 private static int scoreByNormalizedLength(PathPattern pattern) { 536 return -pattern.getNormalizedLength(); 537 } 538 539 private boolean pathContainerIsJustSeparator(PathContainer pathContainer) { 540 return pathContainer.value().length() == 1 && 541 pathContainer.value().charAt(0) == getSeparator(); 542 } 543 544 545 /** 546 * Holder for URI variables and path parameters (matrix variables) extracted 547 * based on the pattern for a given matched path. 548 */ 549 public static class PathMatchInfo { 550 551 private static final PathMatchInfo EMPTY = new PathMatchInfo(Collections.emptyMap(), Collections.emptyMap()); 552 553 private final Map<String, String> uriVariables; 554 555 private final Map<String, MultiValueMap<String, String>> matrixVariables; 556 557 PathMatchInfo(Map<String, String> uriVars, @Nullable Map<String, MultiValueMap<String, String>> matrixVars) { 558 this.uriVariables = Collections.unmodifiableMap(uriVars); 559 this.matrixVariables = (matrixVars != null ? 560 Collections.unmodifiableMap(matrixVars) : Collections.emptyMap()); 561 } 562 563 /** 564 * Return the extracted URI variables. 565 */ 566 public Map<String, String> getUriVariables() { 567 return this.uriVariables; 568 } 569 570 /** 571 * Return maps of matrix variables per path segment, keyed off by URI 572 * variable name. 573 */ 574 public Map<String, MultiValueMap<String, String>> getMatrixVariables() { 575 return this.matrixVariables; 576 } 577 578 @Override 579 public String toString() { 580 return "PathMatchInfo[uriVariables=" + this.uriVariables + ", " + 581 "matrixVariables=" + this.matrixVariables + "]"; 582 } 583 } 584 585 586 /** 587 * Holder for the result of a match on the start of a pattern. 588 * Provides access to the remaining path not matched to the pattern as well 589 * as any variables bound in that first part that was matched. 590 */ 591 public static class PathRemainingMatchInfo { 592 593 private final PathContainer pathRemaining; 594 595 private final PathMatchInfo pathMatchInfo; 596 597 598 PathRemainingMatchInfo(PathContainer pathRemaining) { 599 this(pathRemaining, PathMatchInfo.EMPTY); 600 } 601 602 PathRemainingMatchInfo(PathContainer pathRemaining, PathMatchInfo pathMatchInfo) { 603 this.pathRemaining = pathRemaining; 604 this.pathMatchInfo = pathMatchInfo; 605 } 606 607 /** 608 * Return the part of a path that was not matched by a pattern. 609 */ 610 public PathContainer getPathRemaining() { 611 return this.pathRemaining; 612 } 613 614 /** 615 * Return variables that were bound in the part of the path that was 616 * successfully matched or an empty map. 617 */ 618 public Map<String, String> getUriVariables() { 619 return this.pathMatchInfo.getUriVariables(); 620 } 621 622 /** 623 * Return the path parameters for each bound variable. 624 */ 625 public Map<String, MultiValueMap<String, String>> getMatrixVariables() { 626 return this.pathMatchInfo.getMatrixVariables(); 627 } 628 } 629 630 631 /** 632 * Encapsulates context when attempting a match. Includes some fixed state like the 633 * candidate currently being considered for a match but also some accumulators for 634 * extracted variables. 635 */ 636 class MatchingContext { 637 638 final PathContainer candidate; 639 640 final List<Element> pathElements; 641 642 final int pathLength; 643 644 @Nullable 645 private Map<String, String> extractedUriVariables; 646 647 @Nullable 648 private Map<String, MultiValueMap<String, String>> extractedMatrixVariables; 649 650 boolean extractingVariables; 651 652 boolean determineRemainingPath = false; 653 654 // if determineRemaining is true, this is set to the position in 655 // the candidate where the pattern finished matching - i.e. it 656 // points to the remaining path that wasn't consumed 657 int remainingPathIndex; 658 659 public MatchingContext(PathContainer pathContainer, boolean extractVariables) { 660 this.candidate = pathContainer; 661 this.pathElements = pathContainer.elements(); 662 this.pathLength = this.pathElements.size(); 663 this.extractingVariables = extractVariables; 664 } 665 666 public void setMatchAllowExtraPath() { 667 this.determineRemainingPath = true; 668 } 669 670 public boolean isMatchOptionalTrailingSeparator() { 671 return matchOptionalTrailingSeparator; 672 } 673 674 public void set(String key, String value, MultiValueMap<String,String> parameters) { 675 if (this.extractedUriVariables == null) { 676 this.extractedUriVariables = new HashMap<>(); 677 } 678 this.extractedUriVariables.put(key, value); 679 680 if (!parameters.isEmpty()) { 681 if (this.extractedMatrixVariables == null) { 682 this.extractedMatrixVariables = new HashMap<>(); 683 } 684 this.extractedMatrixVariables.put(key, CollectionUtils.unmodifiableMultiValueMap(parameters)); 685 } 686 } 687 688 public PathMatchInfo getPathMatchResult() { 689 if (this.extractedUriVariables == null) { 690 return PathMatchInfo.EMPTY; 691 } 692 else { 693 return new PathMatchInfo(this.extractedUriVariables, this.extractedMatrixVariables); 694 } 695 } 696 697 /** 698 * Return if element at specified index is a separator. 699 * @param pathIndex possible index of a separator 700 * @return {@code true} if element is a separator 701 */ 702 boolean isSeparator(int pathIndex) { 703 return this.pathElements.get(pathIndex) instanceof Separator; 704 } 705 706 /** 707 * Return the decoded value of the specified element. 708 * @param pathIndex path element index 709 * @return the decoded value 710 */ 711 String pathElementValue(int pathIndex) { 712 Element element = (pathIndex < this.pathLength) ? this.pathElements.get(pathIndex) : null; 713 if (element instanceof PathContainer.PathSegment) { 714 return ((PathContainer.PathSegment)element).valueToMatch(); 715 } 716 return ""; 717 } 718 } 719 720}