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} &mdash; 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} &mdash; matches all {@code .png} files in the
059 * {@code resources} directory</li>
060 * <li><code>/resources/&#42;&#42;</code> &mdash; 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/{&#42;path}</code> &mdash; 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" &rarr; "/image.png", and {@code /resources/css/spring.css} will match
067 * with "path" &rarr; "/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 '&lt;separator&gt;'.
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} &rarr; ''</li>
279         * <li>'{@code /docs/*}' and '{@code /docs/cvs/commit}' &rarr; '{@code cvs/commit}'</li>
280         * <li>'{@code /docs/cvs/*.html}' and '{@code /docs/cvs/commit.html} &rarr; '{@code commit.html}'</li>
281         * <li>'{@code /docs/**}' and '{@code /docs/cvs/commit} &rarr; '{@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}