001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.camel.util;
018
019import java.util.ArrayList;
020import java.util.List;
021import java.util.StringTokenizer;
022
023/**
024 * PathMatcher implementation for Ant-style path patterns. Examples are provided below.
025 * <p>
026 * Part of this mapping code has been kindly borrowed from <a href="http://ant.apache.org">Apache Ant</a> and
027 * <a href="http://springframework.org">Spring Framework</a>.
028 * <p>
029 * The mapping matches URLs using the following rules:<br>
030 * <ul>
031 * <li>? matches one character</li>
032 * <li>* matches zero or more characters</li>
033 * <li>** matches zero or more 'directories' in a path</li>
034 * </ul>
035 * <p>
036 * Some examples:<br>
037 * <ul>
038 * <li><code>com/t?st.jsp</code> - matches <code>com/test.jsp</code> but also <code>com/tast.jsp</code> or
039 * <code>com/txst.jsp</code></li>
040 * <li><code>com/*.jsp</code> - matches all <code>.jsp</code> files in the <code>com</code> directory</li>
041 * <li><code>com/&#42;&#42;/test.jsp</code> - matches all <code>test.jsp</code> files underneath the <code>com</code>
042 * path</li>
043 * <li><code>org/springframework/&#42;&#42;/*.jsp</code> - matches all <code>.jsp</code> files underneath the
044 * <code>org/springframework</code> path</li>
045 * <li><code>org/&#42;&#42;/servlet/bla.jsp</code> - matches <code>org/springframework/servlet/bla.jsp</code> but also
046 * <code>org/springframework/testing/servlet/bla.jsp</code> and <code>org/servlet/bla.jsp</code></li>
047 * </ul>
048 */
049public class AntPathMatcher {
050    public static final AntPathMatcher INSTANCE = new AntPathMatcher();
051
052    /** Default path separator: "/" */
053    public static final String DEFAULT_PATH_SEPARATOR = "/";
054
055    private String pathSeparator = DEFAULT_PATH_SEPARATOR;
056
057    /**
058     * Set the path separator to use for pattern parsing. Default is "/", as in Ant.
059     */
060    public void setPathSeparator(String pathSeparator) {
061        this.pathSeparator = pathSeparator != null ? pathSeparator : DEFAULT_PATH_SEPARATOR;
062    }
063
064    public boolean isPattern(String path) {
065        return path.indexOf('*') != -1 || path.indexOf('?') != -1;
066    }
067
068    public boolean match(String pattern, String path) {
069        return match(pattern, path, true);
070    }
071
072    public boolean anyMatch(String[] patterns, String path) {
073        return anyMatch(patterns, path, true);
074    }
075
076    public boolean anyMatch(String[] patterns, String path, boolean isCaseSensitive) {
077        if (patterns == null) {
078            return false;
079        }
080        if (patterns.length == 0) {
081            return false;
082        }
083        for (String pattern : patterns) {
084            if (match(pattern, path, isCaseSensitive)) {
085                return true;
086            }
087        }
088        return false;
089    }
090
091    public boolean matchStart(String pattern, String path) {
092        return matchStart(pattern, path, true);
093    }
094
095    public boolean match(String pattern, String path, boolean isCaseSensitive) {
096        return doMatch(pattern, path, true, isCaseSensitive);
097    }
098
099    public boolean matchStart(String pattern, String path, boolean isCaseSensitive) {
100        return doMatch(pattern, path, false, isCaseSensitive);
101    }
102
103    /**
104     * Actually match the given <code>path</code> against the given <code>pattern</code>.
105     *
106     * @param  pattern         the pattern to match against
107     * @param  path            the path String to test
108     * @param  fullMatch       whether a full pattern match is required (else a pattern match as far as the given base
109     *                         path goes is sufficient)
110     * @param  isCaseSensitive Whether or not matching should be performed case sensitively.
111     * @return                 <code>true</code> if the supplied <code>path</code> matched, <code>false</code> if it
112     *                         didn't
113     */
114    protected boolean doMatch(String pattern, String path, boolean fullMatch, boolean isCaseSensitive) {
115        if (path.startsWith(this.pathSeparator) != pattern.startsWith(this.pathSeparator)) {
116            return false;
117        }
118
119        String[] pattDirs = tokenizeToStringArray(pattern, this.pathSeparator);
120        String[] pathDirs = tokenizeToStringArray(path, this.pathSeparator);
121
122        int pattIdxStart = 0;
123        int pattIdxEnd = pattDirs.length - 1;
124        int pathIdxStart = 0;
125        int pathIdxEnd = pathDirs.length - 1;
126
127        // Match all elements up to the first **
128        while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {
129            String patDir = pattDirs[pattIdxStart];
130            if ("**".equals(patDir)) {
131                break;
132            }
133            if (!matchStrings(patDir, pathDirs[pathIdxStart], isCaseSensitive)) {
134                return false;
135            }
136            pattIdxStart++;
137            pathIdxStart++;
138        }
139
140        if (pathIdxStart > pathIdxEnd) {
141            // Path is exhausted, only match if rest of pattern is * or **'s
142            if (pattIdxStart > pattIdxEnd) {
143                return pattern.endsWith(this.pathSeparator)
144                        ? path.endsWith(this.pathSeparator) : !path
145                                .endsWith(this.pathSeparator);
146            }
147            if (!fullMatch) {
148                return true;
149            }
150            if (pattIdxStart == pattIdxEnd && pattDirs[pattIdxStart].equals("*")
151                    && path.endsWith(this.pathSeparator)) {
152                return true;
153            }
154            for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
155                if (!pattDirs[i].equals("**")) {
156                    return false;
157                }
158            }
159            return true;
160        } else if (pattIdxStart > pattIdxEnd) {
161            // String not exhausted, but pattern is. Failure.
162            return false;
163        } else if (!fullMatch && "**".equals(pattDirs[pattIdxStart])) {
164            // Path start definitely matches due to "**" part in pattern.
165            return true;
166        }
167
168        // up to last '**'
169        while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {
170            String patDir = pattDirs[pattIdxEnd];
171            if (patDir.equals("**")) {
172                break;
173            }
174            if (!matchStrings(patDir, pathDirs[pathIdxEnd], isCaseSensitive)) {
175                return false;
176            }
177            pattIdxEnd--;
178            pathIdxEnd--;
179        }
180        if (pathIdxStart > pathIdxEnd) {
181            // String is exhausted
182            for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
183                if (!pattDirs[i].equals("**")) {
184                    return false;
185                }
186            }
187            return true;
188        }
189
190        while (pattIdxStart != pattIdxEnd && pathIdxStart <= pathIdxEnd) {
191            int patIdxTmp = -1;
192            for (int i = pattIdxStart + 1; i <= pattIdxEnd; i++) {
193                if (pattDirs[i].equals("**")) {
194                    patIdxTmp = i;
195                    break;
196                }
197            }
198            if (patIdxTmp == pattIdxStart + 1) {
199                // '**/**' situation, so skip one
200                pattIdxStart++;
201                continue;
202            }
203            // Find the pattern between padIdxStart & padIdxTmp in str between
204            // strIdxStart & strIdxEnd
205            int patLength = patIdxTmp - pattIdxStart - 1;
206            int strLength = pathIdxEnd - pathIdxStart + 1;
207            int foundIdx = -1;
208
209            strLoop: for (int i = 0; i <= strLength - patLength; i++) {
210                for (int j = 0; j < patLength; j++) {
211                    String subPat = pattDirs[pattIdxStart + j + 1];
212                    String subStr = pathDirs[pathIdxStart + i + j];
213                    if (!matchStrings(subPat, subStr, isCaseSensitive)) {
214                        continue strLoop;
215                    }
216                }
217                foundIdx = pathIdxStart + i;
218                break;
219            }
220
221            if (foundIdx == -1) {
222                return false;
223            }
224
225            pattIdxStart = patIdxTmp;
226            pathIdxStart = foundIdx + patLength;
227        }
228
229        for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
230            if (!pattDirs[i].equals("**")) {
231                return false;
232            }
233        }
234
235        return true;
236    }
237
238    /**
239     * Tests whether or not a string matches against a pattern. The pattern may contain two special characters:<br>
240     * '*' means zero or more characters<br>
241     * '?' means one and only one character
242     *
243     * @param  pattern       pattern to match against. Must not be <code>null</code>.
244     * @param  str           string which must be matched against the pattern. Must not be <code>null</code>.
245     * @param  caseSensitive Whether or not matching should be performed case sensitively.
246     * @return               <code>true</code> if the string matches against the pattern, or <code>false</code>
247     *                       otherwise.
248     */
249    private boolean matchStrings(String pattern, String str, boolean caseSensitive) {
250        char[] patArr = pattern.toCharArray();
251        char[] strArr = str.toCharArray();
252        int patIdxStart = 0;
253        int patIdxEnd = patArr.length - 1;
254        int strIdxStart = 0;
255        int strIdxEnd = strArr.length - 1;
256        char ch;
257
258        boolean containsStar = false;
259        for (char c : patArr) {
260            if (c == '*') {
261                containsStar = true;
262                break;
263            }
264        }
265
266        if (!containsStar) {
267            // No '*'s, so we make a shortcut
268            if (patIdxEnd != strIdxEnd) {
269                return false; // Pattern and string do not have the same size
270            }
271            for (int i = 0; i <= patIdxEnd; i++) {
272                ch = patArr[i];
273                if (ch != '?') {
274                    if (different(caseSensitive, ch, strArr[i])) {
275                        return false;
276                        // Character mismatch
277                    }
278                }
279            }
280            return true; // String matches against pattern
281        }
282
283        if (patIdxEnd == 0) {
284            return true; // Pattern contains only '*', which matches anything
285        }
286
287        // Process characters before first star
288        while ((ch = patArr[patIdxStart]) != '*' && strIdxStart <= strIdxEnd) {
289            if (ch != '?') {
290                if (different(caseSensitive, ch, strArr[strIdxStart])) {
291                    return false;
292                    // Character mismatch
293                }
294            }
295            patIdxStart++;
296            strIdxStart++;
297        }
298        if (strIdxStart > strIdxEnd) {
299            // All characters in the string are used. Check if only '*'s are
300            // left in the pattern. If so, we succeeded. Otherwise failure.
301            for (int i = patIdxStart; i <= patIdxEnd; i++) {
302                if (patArr[i] != '*') {
303                    return false;
304                }
305            }
306            return true;
307        }
308
309        // Process characters after last star
310        while ((ch = patArr[patIdxEnd]) != '*' && strIdxStart <= strIdxEnd) {
311            if (ch != '?') {
312                if (different(caseSensitive, ch, strArr[strIdxEnd])) {
313                    return false;
314                    // Character mismatch
315                }
316            }
317            patIdxEnd--;
318            strIdxEnd--;
319        }
320        if (strIdxStart > strIdxEnd) {
321            // All characters in the string are used. Check if only '*'s are
322            // left in the pattern. If so, we succeeded. Otherwise failure.
323            for (int i = patIdxStart; i <= patIdxEnd; i++) {
324                if (patArr[i] != '*') {
325                    return false;
326                }
327            }
328            return true;
329        }
330
331        // process pattern between stars. padIdxStart and patIdxEnd point
332        // always to a '*'.
333        while (patIdxStart != patIdxEnd && strIdxStart <= strIdxEnd) {
334            int patIdxTmp = -1;
335            for (int i = patIdxStart + 1; i <= patIdxEnd; i++) {
336                if (patArr[i] == '*') {
337                    patIdxTmp = i;
338                    break;
339                }
340            }
341            if (patIdxTmp == patIdxStart + 1) {
342                // Two stars next to each other, skip the first one.
343                patIdxStart++;
344                continue;
345            }
346            // Find the pattern between padIdxStart & padIdxTmp in str between
347            // strIdxStart & strIdxEnd
348            int patLength = patIdxTmp - patIdxStart - 1;
349            int strLength = strIdxEnd - strIdxStart + 1;
350            int foundIdx = -1;
351            strLoop: for (int i = 0; i <= strLength - patLength; i++) {
352                for (int j = 0; j < patLength; j++) {
353                    ch = patArr[patIdxStart + j + 1];
354                    if (ch != '?') {
355                        if (different(caseSensitive, ch, strArr[strIdxStart + i + j])) {
356                            continue strLoop;
357                        }
358                    }
359                }
360
361                foundIdx = strIdxStart + i;
362                break;
363            }
364
365            if (foundIdx == -1) {
366                return false;
367            }
368
369            patIdxStart = patIdxTmp;
370            strIdxStart = foundIdx + patLength;
371        }
372
373        // All characters in the string are used. Check if only '*'s are left
374        // in the pattern. If so, we succeeded. Otherwise failure.
375        for (int i = patIdxStart; i <= patIdxEnd; i++) {
376            if (patArr[i] != '*') {
377                return false;
378            }
379        }
380
381        return true;
382    }
383
384    /**
385     * Given a pattern and a full path, determine the pattern-mapped part.
386     * <p>
387     * For example:
388     * <ul>
389     * <li>'<code>/docs/cvs/commit.html</code>' and ' <code>/docs/cvs/commit.html</code> -> ''</li>
390     * <li>'<code>/docs/*</code>' and '<code>/docs/cvs/commit</code> -> ' <code>cvs/commit</code>'</li>
391     * <li>'<code>/docs/cvs/*.html</code>' and ' <code>/docs/cvs/commit.html</code> -> '<code>commit.html</code>'</li>
392     * <li>'<code>/docs/**</code>' and '<code>/docs/cvs/commit</code> -> ' <code>cvs/commit</code>'</li>
393     * <li>'<code>/docs/**\/*.html</code>' and ' <code>/docs/cvs/commit.html</code> ->
394     * '<code>cvs/commit.html</code>'</li>
395     * <li>'<code>/*.html</code>' and '<code>/docs/cvs/commit.html</code> -> ' <code>docs/cvs/commit.html</code>'</li>
396     * <li>'<code>*.html</code>' and '<code>/docs/cvs/commit.html</code> -> ' <code>/docs/cvs/commit.html</code>'</li>
397     * <li>'<code>*</code>' and '<code>/docs/cvs/commit.html</code> -> ' <code>/docs/cvs/commit.html</code>'</li>
398     * </ul>
399     * <p>
400     * Assumes that {@link #match} returns <code>true</code> for ' <code>pattern</code>' and '<code>path</code>', but
401     * does <strong>not</strong> enforce this.
402     */
403    public String extractPathWithinPattern(String pattern, String path) {
404        if (path == null) {
405            return null;
406        }
407        String[] patternParts = tokenizeToStringArray(pattern, this.pathSeparator);
408        String[] pathParts = tokenizeToStringArray(path, this.pathSeparator);
409
410        StringBuilder buffer = new StringBuilder();
411
412        // Add any path parts that have a wildcarded pattern part.
413        int puts = 0;
414        for (int i = 0; i < patternParts.length; i++) {
415            String patternPart = patternParts[i];
416            if ((patternPart.indexOf('*') > -1 || patternPart.indexOf('?') > -1) && pathParts.length >= i + 1) {
417                if (puts > 0 || i == 0 && !pattern.startsWith(this.pathSeparator)) {
418                    buffer.append(this.pathSeparator);
419                }
420                buffer.append(pathParts[i]);
421                puts++;
422            }
423        }
424
425        // Append any trailing path parts.
426        for (int i = patternParts.length; i < pathParts.length; i++) {
427            if (puts > 0 || i > 0) {
428                buffer.append(this.pathSeparator);
429            }
430            buffer.append(pathParts[i]);
431        }
432
433        return buffer.toString();
434    }
435
436    /**
437     * Tokenize the given String into a String array via a StringTokenizer. Trims tokens and omits empty tokens.
438     * <p>
439     * The given delimiters string is supposed to consist of any number of delimiter characters. Each of those
440     * characters can be used to separate tokens. A delimiter is always a single character; for multi-character
441     * delimiters, consider using <code>delimitedListToStringArray</code>
442     *
443     * @param  str        the String to tokenize
444     * @param  delimiters the delimiter characters, assembled as String (each of those characters is individually
445     *                    considered as delimiter).
446     * @return            an array of the tokens
447     * @see               java.util.StringTokenizer
448     * @see               java.lang.String#trim()
449     */
450    public static String[] tokenizeToStringArray(String str, String delimiters) {
451        if (str == null) {
452            return null;
453        }
454        StringTokenizer st = new StringTokenizer(str, delimiters);
455        List<String> tokens = new ArrayList<>();
456        while (st.hasMoreTokens()) {
457            String token = st.nextToken();
458            token = token.trim();
459            if (token.length() > 0) {
460                tokens.add(token);
461            }
462        }
463        return tokens.toArray(new String[tokens.size()]);
464    }
465
466    private static boolean different(boolean caseSensitive, char ch, char other) {
467        return caseSensitive ? ch != other : Character.toUpperCase(ch) != Character.toUpperCase(other);
468    }
469
470    /**
471     * Determine the root directory for the given location.
472     * <p>
473     * Used for determining the starting point for file matching, resolving the root directory location
474     * <p>
475     * Will return "/WEB-INF/" for the pattern "/WEB-INF/*.xml", for example.
476     *
477     * @param  location the location to check
478     * @return          the part of the location that denotes the root directory
479     */
480    public String determineRootDir(String location) {
481        int prefixEnd = location.indexOf(':') + 1;
482        int rootDirEnd = location.length();
483        while (rootDirEnd > prefixEnd && isPattern(location.substring(prefixEnd, rootDirEnd))) {
484            rootDirEnd = location.lastIndexOf('/', rootDirEnd - 2) + 1;
485        }
486        if (rootDirEnd == 0) {
487            rootDirEnd = prefixEnd;
488        }
489        return location.substring(0, rootDirEnd);
490    }
491
492}