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 matchStart(String pattern, String path) {
073        return matchStart(pattern, path, true);
074    }
075
076    public boolean match(String pattern, String path, boolean isCaseSensitive) {
077        return doMatch(pattern, path, true, isCaseSensitive);
078    }
079
080    public boolean matchStart(String pattern, String path, boolean isCaseSensitive) {
081        return doMatch(pattern, path, false, isCaseSensitive);
082    }
083
084    /**
085     * Actually match the given <code>path</code> against the given <code>pattern</code>.
086     * 
087     * @param  pattern         the pattern to match against
088     * @param  path            the path String to test
089     * @param  fullMatch       whether a full pattern match is required (else a pattern match as far as the given base
090     *                         path goes is sufficient)
091     * @param  isCaseSensitive Whether or not matching should be performed case sensitively.
092     * @return                 <code>true</code> if the supplied <code>path</code> matched, <code>false</code> if it
093     *                         didn't
094     */
095    protected boolean doMatch(String pattern, String path, boolean fullMatch, boolean isCaseSensitive) {
096        if (path.startsWith(this.pathSeparator) != pattern.startsWith(this.pathSeparator)) {
097            return false;
098        }
099
100        String[] pattDirs = tokenizeToStringArray(pattern, this.pathSeparator);
101        String[] pathDirs = tokenizeToStringArray(path, this.pathSeparator);
102
103        int pattIdxStart = 0;
104        int pattIdxEnd = pattDirs.length - 1;
105        int pathIdxStart = 0;
106        int pathIdxEnd = pathDirs.length - 1;
107
108        // Match all elements up to the first **
109        while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {
110            String patDir = pattDirs[pattIdxStart];
111            if ("**".equals(patDir)) {
112                break;
113            }
114            if (!matchStrings(patDir, pathDirs[pathIdxStart], isCaseSensitive)) {
115                return false;
116            }
117            pattIdxStart++;
118            pathIdxStart++;
119        }
120
121        if (pathIdxStart > pathIdxEnd) {
122            // Path is exhausted, only match if rest of pattern is * or **'s
123            if (pattIdxStart > pattIdxEnd) {
124                return pattern.endsWith(this.pathSeparator)
125                        ? path.endsWith(this.pathSeparator) : !path
126                                .endsWith(this.pathSeparator);
127            }
128            if (!fullMatch) {
129                return true;
130            }
131            if (pattIdxStart == pattIdxEnd && pattDirs[pattIdxStart].equals("*")
132                    && path.endsWith(this.pathSeparator)) {
133                return true;
134            }
135            for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
136                if (!pattDirs[i].equals("**")) {
137                    return false;
138                }
139            }
140            return true;
141        } else if (pattIdxStart > pattIdxEnd) {
142            // String not exhausted, but pattern is. Failure.
143            return false;
144        } else if (!fullMatch && "**".equals(pattDirs[pattIdxStart])) {
145            // Path start definitely matches due to "**" part in pattern.
146            return true;
147        }
148
149        // up to last '**'
150        while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {
151            String patDir = pattDirs[pattIdxEnd];
152            if (patDir.equals("**")) {
153                break;
154            }
155            if (!matchStrings(patDir, pathDirs[pathIdxEnd], isCaseSensitive)) {
156                return false;
157            }
158            pattIdxEnd--;
159            pathIdxEnd--;
160        }
161        if (pathIdxStart > pathIdxEnd) {
162            // String is exhausted
163            for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
164                if (!pattDirs[i].equals("**")) {
165                    return false;
166                }
167            }
168            return true;
169        }
170
171        while (pattIdxStart != pattIdxEnd && pathIdxStart <= pathIdxEnd) {
172            int patIdxTmp = -1;
173            for (int i = pattIdxStart + 1; i <= pattIdxEnd; i++) {
174                if (pattDirs[i].equals("**")) {
175                    patIdxTmp = i;
176                    break;
177                }
178            }
179            if (patIdxTmp == pattIdxStart + 1) {
180                // '**/**' situation, so skip one
181                pattIdxStart++;
182                continue;
183            }
184            // Find the pattern between padIdxStart & padIdxTmp in str between
185            // strIdxStart & strIdxEnd
186            int patLength = patIdxTmp - pattIdxStart - 1;
187            int strLength = pathIdxEnd - pathIdxStart + 1;
188            int foundIdx = -1;
189
190            strLoop: for (int i = 0; i <= strLength - patLength; i++) {
191                for (int j = 0; j < patLength; j++) {
192                    String subPat = pattDirs[pattIdxStart + j + 1];
193                    String subStr = pathDirs[pathIdxStart + i + j];
194                    if (!matchStrings(subPat, subStr, isCaseSensitive)) {
195                        continue strLoop;
196                    }
197                }
198                foundIdx = pathIdxStart + i;
199                break;
200            }
201
202            if (foundIdx == -1) {
203                return false;
204            }
205
206            pattIdxStart = patIdxTmp;
207            pathIdxStart = foundIdx + patLength;
208        }
209
210        for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
211            if (!pattDirs[i].equals("**")) {
212                return false;
213            }
214        }
215
216        return true;
217    }
218
219    /**
220     * Tests whether or not a string matches against a pattern. The pattern may contain two special characters:<br>
221     * '*' means zero or more characters<br>
222     * '?' means one and only one character
223     * 
224     * @param  pattern       pattern to match against. Must not be <code>null</code>.
225     * @param  str           string which must be matched against the pattern. Must not be <code>null</code>.
226     * @param  caseSensitive Whether or not matching should be performed case sensitively.
227     * @return               <code>true</code> if the string matches against the pattern, or <code>false</code>
228     *                       otherwise.
229     */
230    private boolean matchStrings(String pattern, String str, boolean caseSensitive) {
231        char[] patArr = pattern.toCharArray();
232        char[] strArr = str.toCharArray();
233        int patIdxStart = 0;
234        int patIdxEnd = patArr.length - 1;
235        int strIdxStart = 0;
236        int strIdxEnd = strArr.length - 1;
237        char ch;
238
239        boolean containsStar = false;
240        for (char c : patArr) {
241            if (c == '*') {
242                containsStar = true;
243                break;
244            }
245        }
246
247        if (!containsStar) {
248            // No '*'s, so we make a shortcut
249            if (patIdxEnd != strIdxEnd) {
250                return false; // Pattern and string do not have the same size
251            }
252            for (int i = 0; i <= patIdxEnd; i++) {
253                ch = patArr[i];
254                if (ch != '?') {
255                    if (different(caseSensitive, ch, strArr[i])) {
256                        return false;
257                        // Character mismatch
258                    }
259                }
260            }
261            return true; // String matches against pattern
262        }
263
264        if (patIdxEnd == 0) {
265            return true; // Pattern contains only '*', which matches anything
266        }
267
268        // Process characters before first star
269        while ((ch = patArr[patIdxStart]) != '*' && strIdxStart <= strIdxEnd) {
270            if (ch != '?') {
271                if (different(caseSensitive, ch, strArr[strIdxStart])) {
272                    return false;
273                    // Character mismatch
274                }
275            }
276            patIdxStart++;
277            strIdxStart++;
278        }
279        if (strIdxStart > strIdxEnd) {
280            // All characters in the string are used. Check if only '*'s are
281            // left in the pattern. If so, we succeeded. Otherwise failure.
282            for (int i = patIdxStart; i <= patIdxEnd; i++) {
283                if (patArr[i] != '*') {
284                    return false;
285                }
286            }
287            return true;
288        }
289
290        // Process characters after last star
291        while ((ch = patArr[patIdxEnd]) != '*' && strIdxStart <= strIdxEnd) {
292            if (ch != '?') {
293                if (different(caseSensitive, ch, strArr[strIdxEnd])) {
294                    return false;
295                    // Character mismatch
296                }
297            }
298            patIdxEnd--;
299            strIdxEnd--;
300        }
301        if (strIdxStart > strIdxEnd) {
302            // All characters in the string are used. Check if only '*'s are
303            // left in the pattern. If so, we succeeded. Otherwise failure.
304            for (int i = patIdxStart; i <= patIdxEnd; i++) {
305                if (patArr[i] != '*') {
306                    return false;
307                }
308            }
309            return true;
310        }
311
312        // process pattern between stars. padIdxStart and patIdxEnd point
313        // always to a '*'.
314        while (patIdxStart != patIdxEnd && strIdxStart <= strIdxEnd) {
315            int patIdxTmp = -1;
316            for (int i = patIdxStart + 1; i <= patIdxEnd; i++) {
317                if (patArr[i] == '*') {
318                    patIdxTmp = i;
319                    break;
320                }
321            }
322            if (patIdxTmp == patIdxStart + 1) {
323                // Two stars next to each other, skip the first one.
324                patIdxStart++;
325                continue;
326            }
327            // Find the pattern between padIdxStart & padIdxTmp in str between
328            // strIdxStart & strIdxEnd
329            int patLength = patIdxTmp - patIdxStart - 1;
330            int strLength = strIdxEnd - strIdxStart + 1;
331            int foundIdx = -1;
332            strLoop: for (int i = 0; i <= strLength - patLength; i++) {
333                for (int j = 0; j < patLength; j++) {
334                    ch = patArr[patIdxStart + j + 1];
335                    if (ch != '?') {
336                        if (different(caseSensitive, ch, strArr[strIdxStart + i + j])) {
337                            continue strLoop;
338                        }
339                    }
340                }
341
342                foundIdx = strIdxStart + i;
343                break;
344            }
345
346            if (foundIdx == -1) {
347                return false;
348            }
349
350            patIdxStart = patIdxTmp;
351            strIdxStart = foundIdx + patLength;
352        }
353
354        // All characters in the string are used. Check if only '*'s are left
355        // in the pattern. If so, we succeeded. Otherwise failure.
356        for (int i = patIdxStart; i <= patIdxEnd; i++) {
357            if (patArr[i] != '*') {
358                return false;
359            }
360        }
361
362        return true;
363    }
364
365    /**
366     * Given a pattern and a full path, determine the pattern-mapped part.
367     * <p>
368     * For example:
369     * <ul>
370     * <li>'<code>/docs/cvs/commit.html</code>' and ' <code>/docs/cvs/commit.html</code> -> ''</li>
371     * <li>'<code>/docs/*</code>' and '<code>/docs/cvs/commit</code> -> ' <code>cvs/commit</code>'</li>
372     * <li>'<code>/docs/cvs/*.html</code>' and ' <code>/docs/cvs/commit.html</code> -> '<code>commit.html</code>'</li>
373     * <li>'<code>/docs/**</code>' and '<code>/docs/cvs/commit</code> -> ' <code>cvs/commit</code>'</li>
374     * <li>'<code>/docs/**\/*.html</code>' and ' <code>/docs/cvs/commit.html</code> ->
375     * '<code>cvs/commit.html</code>'</li>
376     * <li>'<code>/*.html</code>' and '<code>/docs/cvs/commit.html</code> -> ' <code>docs/cvs/commit.html</code>'</li>
377     * <li>'<code>*.html</code>' and '<code>/docs/cvs/commit.html</code> -> ' <code>/docs/cvs/commit.html</code>'</li>
378     * <li>'<code>*</code>' and '<code>/docs/cvs/commit.html</code> -> ' <code>/docs/cvs/commit.html</code>'</li>
379     * </ul>
380     * <p>
381     * Assumes that {@link #match} returns <code>true</code> for ' <code>pattern</code>' and '<code>path</code>', but
382     * does <strong>not</strong> enforce this.
383     */
384    public String extractPathWithinPattern(String pattern, String path) {
385        String[] patternParts = tokenizeToStringArray(pattern, this.pathSeparator);
386        String[] pathParts = tokenizeToStringArray(path, this.pathSeparator);
387
388        StringBuilder buffer = new StringBuilder();
389
390        // Add any path parts that have a wildcarded pattern part.
391        int puts = 0;
392        for (int i = 0; i < patternParts.length; i++) {
393            String patternPart = patternParts[i];
394            if ((patternPart.indexOf('*') > -1 || patternPart.indexOf('?') > -1) && pathParts.length >= i + 1) {
395                if (puts > 0 || (i == 0 && !pattern.startsWith(this.pathSeparator))) {
396                    buffer.append(this.pathSeparator);
397                }
398                buffer.append(pathParts[i]);
399                puts++;
400            }
401        }
402
403        // Append any trailing path parts.
404        for (int i = patternParts.length; i < pathParts.length; i++) {
405            if (puts > 0 || i > 0) {
406                buffer.append(this.pathSeparator);
407            }
408            buffer.append(pathParts[i]);
409        }
410
411        return buffer.toString();
412    }
413
414    /**
415     * Tokenize the given String into a String array via a StringTokenizer. Trims tokens and omits empty tokens.
416     * <p>
417     * The given delimiters string is supposed to consist of any number of delimiter characters. Each of those
418     * characters can be used to separate tokens. A delimiter is always a single character; for multi-character
419     * delimiters, consider using <code>delimitedListToStringArray</code>
420     * 
421     * @param  str        the String to tokenize
422     * @param  delimiters the delimiter characters, assembled as String (each of those characters is individually
423     *                    considered as delimiter).
424     * @return            an array of the tokens
425     * @see               java.util.StringTokenizer
426     * @see               java.lang.String#trim()
427     */
428    public static String[] tokenizeToStringArray(String str, String delimiters) {
429        if (str == null) {
430            return null;
431        }
432        StringTokenizer st = new StringTokenizer(str, delimiters);
433        List<String> tokens = new ArrayList<>();
434        while (st.hasMoreTokens()) {
435            String token = st.nextToken();
436            token = token.trim();
437            if (token.length() > 0) {
438                tokens.add(token);
439            }
440        }
441        return tokens.toArray(new String[tokens.size()]);
442    }
443
444    private static boolean different(boolean caseSensitive, char ch, char other) {
445        return caseSensitive ? ch != other : Character.toUpperCase(ch) != Character.toUpperCase(other);
446    }
447
448    /**
449     * Determine the root directory for the given location.
450     * <p>
451     * Used for determining the starting point for file matching, resolving the root directory location
452     * <p>
453     * Will return "/WEB-INF/" for the pattern "/WEB-INF/*.xml", for example.
454     *
455     * @param  location the location to check
456     * @return          the part of the location that denotes the root directory
457     */
458    public String determineRootDir(String location) {
459        int prefixEnd = location.indexOf(':') + 1;
460        int rootDirEnd = location.length();
461        while (rootDirEnd > prefixEnd && isPattern(location.substring(prefixEnd, rootDirEnd))) {
462            rootDirEnd = location.lastIndexOf('/', rootDirEnd - 2) + 1;
463        }
464        if (rootDirEnd == 0) {
465            rootDirEnd = prefixEnd;
466        }
467        return location.substring(0, rootDirEnd);
468    }
469
470}