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