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