1/*
2 *  Licensed to the Apache Software Foundation (ASF) under one or more
3 *  contributor license agreements.  See the NOTICE file distributed with
4 *  this work for additional information regarding copyright ownership.
5 *  The ASF licenses this file to You under the Apache License, Version 2.0
6 *  (the "License"); you may not use this file except in compliance with
7 *  the License.  You may obtain a copy of the License at
8 *
9 *      http://www.apache.org/licenses/LICENSE-2.0
10 *
11 *  Unless required by applicable law or agreed to in writing, software
12 *  distributed under the License is distributed on an "AS IS" BASIS,
13 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 *  See the License for the specific language governing permissions and
15 *  limitations under the License.
16 *
17 */
18
19package org.apache.tools.ant.types.selectors;
20
21import org.apache.tools.ant.util.FileUtils;
22
23import java.io.File;
24import java.util.StringTokenizer;
25import java.util.Vector;
26
27/**
28 * <p>This is a utility class used by selectors and DirectoryScanner. The
29 * functionality more properly belongs just to selectors, but unfortunately
30 * DirectoryScanner exposed these as protected methods. Thus we have to
31 * support any subclasses of DirectoryScanner that may access these methods.
32 * </p>
33 * <p>This is a Singleton.</p>
34 *
35 * @since 1.5
36 */
37public final class SelectorUtils {
38
39    /**
40     * The pattern that matches an arbitrary number of directories.
41     * @since Ant 1.8.0
42     */
43    public static final String DEEP_TREE_MATCH = "**";
44
45    private static final SelectorUtils instance = new SelectorUtils();
46    private static final FileUtils FILE_UTILS = FileUtils.getFileUtils();
47
48    /**
49     * Private Constructor
50     */
51    private SelectorUtils() {
52    }
53
54    /**
55     * Retrieves the instance of the Singleton.
56     * @return singleton instance
57     */
58    public static SelectorUtils getInstance() {
59        return instance;
60    }
61
62    /**
63     * Tests whether or not a given path matches the start of a given
64     * pattern up to the first "**".
65     * <p>
66     * This is not a general purpose test and should only be used if you
67     * can live with false positives. For example, <code>pattern=**\a</code>
68     * and <code>str=b</code> will yield <code>true</code>.
69     *
70     * @param pattern The pattern to match against. Must not be
71     *                <code>null</code>.
72     * @param str     The path to match, as a String. Must not be
73     *                <code>null</code>.
74     *
75     * @return whether or not a given path matches the start of a given
76     * pattern up to the first "**".
77     */
78    public static boolean matchPatternStart(String pattern, String str) {
79        return matchPatternStart(pattern, str, true);
80    }
81
82    /**
83     * Tests whether or not a given path matches the start of a given
84     * pattern up to the first "**".
85     * <p>
86     * This is not a general purpose test and should only be used if you
87     * can live with false positives. For example, <code>pattern=**\a</code>
88     * and <code>str=b</code> will yield <code>true</code>.
89     *
90     * @param pattern The pattern to match against. Must not be
91     *                <code>null</code>.
92     * @param str     The path to match, as a String. Must not be
93     *                <code>null</code>.
94     * @param isCaseSensitive Whether or not matching should be performed
95     *                        case sensitively.
96     *
97     * @return whether or not a given path matches the start of a given
98     * pattern up to the first "**".
99     */
100    public static boolean matchPatternStart(String pattern, String str,
101                                            boolean isCaseSensitive) {
102        // When str starts with a File.separator, pattern has to start with a
103        // File.separator.
104        // When pattern starts with a File.separator, str has to start with a
105        // File.separator.
106        if (str.startsWith(File.separator)
107                != pattern.startsWith(File.separator)) {
108            return false;
109        }
110
111        String[] patDirs = tokenizePathAsArray(pattern);
112        String[] strDirs = tokenizePathAsArray(str);
113        return matchPatternStart(patDirs, strDirs, isCaseSensitive);
114    }
115
116
117    /**
118     * Tests whether or not a given path matches the start of a given
119     * pattern up to the first "**".
120     * <p>
121     * This is not a general purpose test and should only be used if you
122     * can live with false positives. For example, <code>pattern=**\a</code>
123     * and <code>str=b</code> will yield <code>true</code>.
124     *
125     * @param patDirs The tokenized pattern to match against. Must not be
126     *                <code>null</code>.
127     * @param strDirs The tokenized path to match. Must not be
128     *                <code>null</code>.
129     * @param isCaseSensitive Whether or not matching should be performed
130     *                        case sensitively.
131     *
132     * @return whether or not a given path matches the start of a given
133     * pattern up to the first "**".
134     */
135    static boolean matchPatternStart(String[] patDirs, String[] strDirs,
136                                     boolean isCaseSensitive) {
137        int patIdxStart = 0;
138        int patIdxEnd = patDirs.length - 1;
139        int strIdxStart = 0;
140        int strIdxEnd = strDirs.length - 1;
141
142        // up to first '**'
143        while (patIdxStart <= patIdxEnd && strIdxStart <= strIdxEnd) {
144            String patDir = patDirs[patIdxStart];
145            if (patDir.equals(DEEP_TREE_MATCH)) {
146                break;
147            }
148            if (!match(patDir, strDirs[strIdxStart], isCaseSensitive)) {
149                return false;
150            }
151            patIdxStart++;
152            strIdxStart++;
153        }
154
155        // CheckStyle:SimplifyBooleanReturnCheck OFF
156        // Check turned off as the code needs the comments for the various
157        // code paths.
158        if (strIdxStart > strIdxEnd) {
159            // String is exhausted
160            return true;
161        } else if (patIdxStart > patIdxEnd) {
162            // String not exhausted, but pattern is. Failure.
163            return false;
164        } else {
165            // pattern now holds ** while string is not exhausted
166            // this will generate false positives but we can live with that.
167            return true;
168        }
169    }
170
171    /**
172     * Tests whether or not a given path matches a given pattern.
173     *
174     * If you need to call this method multiple times with the same
175     * pattern you should rather use TokenizedPath
176     *
177     * @see TokenizedPath
178     *
179     * @param pattern The pattern to match against. Must not be
180     *                <code>null</code>.
181     * @param str     The path to match, as a String. Must not be
182     *                <code>null</code>.
183     *
184     * @return <code>true</code> if the pattern matches against the string,
185     *         or <code>false</code> otherwise.
186     */
187    public static boolean matchPath(String pattern, String str) {
188        String[] patDirs = tokenizePathAsArray(pattern);
189        return matchPath(patDirs, tokenizePathAsArray(str), true);
190    }
191
192    /**
193     * Tests whether or not a given path matches a given pattern.
194     *
195     * If you need to call this method multiple times with the same
196     * pattern you should rather use TokenizedPattern
197     *
198     * @see TokenizedPattern
199     *
200     * @param pattern The pattern to match against. Must not be
201     *                <code>null</code>.
202     * @param str     The path to match, as a String. Must not be
203     *                <code>null</code>.
204     * @param isCaseSensitive Whether or not matching should be performed
205     *                        case sensitively.
206     *
207     * @return <code>true</code> if the pattern matches against the string,
208     *         or <code>false</code> otherwise.
209     */
210    public static boolean matchPath(String pattern, String str,
211                                    boolean isCaseSensitive) {
212        String[] patDirs = tokenizePathAsArray(pattern);
213        return matchPath(patDirs, tokenizePathAsArray(str), isCaseSensitive);
214    }
215
216    /**
217     * Core implementation of matchPath.  It is isolated so that it
218     * can be called from TokenizedPattern.
219     */
220    static boolean matchPath(String[] tokenizedPattern, String[] strDirs,
221                             boolean isCaseSensitive) {
222        int patIdxStart = 0;
223        int patIdxEnd = tokenizedPattern.length - 1;
224        int strIdxStart = 0;
225        int strIdxEnd = strDirs.length - 1;
226
227        // up to first '**'
228        while (patIdxStart <= patIdxEnd && strIdxStart <= strIdxEnd) {
229            String patDir = tokenizedPattern[patIdxStart];
230            if (patDir.equals(DEEP_TREE_MATCH)) {
231                break;
232            }
233            if (!match(patDir, strDirs[strIdxStart], isCaseSensitive)) {
234                return false;
235            }
236            patIdxStart++;
237            strIdxStart++;
238        }
239        if (strIdxStart > strIdxEnd) {
240            // String is exhausted
241            for (int i = patIdxStart; i <= patIdxEnd; i++) {
242                if (!tokenizedPattern[i].equals(DEEP_TREE_MATCH)) {
243                    return false;
244                }
245            }
246            return true;
247        } else {
248            if (patIdxStart > patIdxEnd) {
249                // String not exhausted, but pattern is. Failure.
250                return false;
251            }
252        }
253
254        // up to last '**'
255        while (patIdxStart <= patIdxEnd && strIdxStart <= strIdxEnd) {
256            String patDir = tokenizedPattern[patIdxEnd];
257            if (patDir.equals(DEEP_TREE_MATCH)) {
258                break;
259            }
260            if (!match(patDir, strDirs[strIdxEnd], isCaseSensitive)) {
261                return false;
262            }
263            patIdxEnd--;
264            strIdxEnd--;
265        }
266        if (strIdxStart > strIdxEnd) {
267            // String is exhausted
268            for (int i = patIdxStart; i <= patIdxEnd; i++) {
269                if (!tokenizedPattern[i].equals(DEEP_TREE_MATCH)) {
270                    return false;
271                }
272            }
273            return true;
274        }
275
276        while (patIdxStart != patIdxEnd && strIdxStart <= strIdxEnd) {
277            int patIdxTmp = -1;
278            for (int i = patIdxStart + 1; i <= patIdxEnd; i++) {
279                if (tokenizedPattern[i].equals(DEEP_TREE_MATCH)) {
280                    patIdxTmp = i;
281                    break;
282                }
283            }
284            if (patIdxTmp == patIdxStart + 1) {
285                // '**/**' situation, so skip one
286                patIdxStart++;
287                continue;
288            }
289            // Find the pattern between padIdxStart & padIdxTmp in str between
290            // strIdxStart & strIdxEnd
291            int patLength = (patIdxTmp - patIdxStart - 1);
292            int strLength = (strIdxEnd - strIdxStart + 1);
293            int foundIdx = -1;
294            strLoop:
295                        for (int i = 0; i <= strLength - patLength; i++) {
296                            for (int j = 0; j < patLength; j++) {
297                                String subPat = tokenizedPattern[patIdxStart + j + 1];
298                                String subStr = strDirs[strIdxStart + i + j];
299                                if (!match(subPat, subStr, isCaseSensitive)) {
300                                    continue strLoop;
301                                }
302                            }
303
304                            foundIdx = strIdxStart + i;
305                            break;
306                        }
307
308            if (foundIdx == -1) {
309                return false;
310            }
311
312            patIdxStart = patIdxTmp;
313            strIdxStart = foundIdx + patLength;
314        }
315
316        for (int i = patIdxStart; i <= patIdxEnd; i++) {
317            if (!tokenizedPattern[i].equals(DEEP_TREE_MATCH)) {
318                return false;
319            }
320        }
321
322        return true;
323    }
324
325    /**
326     * Tests whether or not a string matches against a pattern.
327     * The pattern may contain two special characters:<br>
328     * '*' means zero or more characters<br>
329     * '?' means one and only one character
330     *
331     * @param pattern The pattern to match against.
332     *                Must not be <code>null</code>.
333     * @param str     The string which must be matched against the pattern.
334     *                Must not be <code>null</code>.
335     *
336     * @return <code>true</code> if the string matches against the pattern,
337     *         or <code>false</code> otherwise.
338     */
339    public static boolean match(String pattern, String str) {
340        return match(pattern, str, true);
341    }
342
343    /**
344     * Tests whether or not a string matches against a pattern.
345     * The pattern may contain two special characters:<br>
346     * '*' means zero or more characters<br>
347     * '?' means one and only one character
348     *
349     * @param pattern The pattern to match against.
350     *                Must not be <code>null</code>.
351     * @param str     The string which must be matched against the pattern.
352     *                Must not be <code>null</code>.
353     * @param caseSensitive Whether or not matching should be performed
354     *                        case sensitively.
355     *
356     *
357     * @return <code>true</code> if the string matches against the pattern,
358     *         or <code>false</code> otherwise.
359     */
360    public static boolean match(String pattern, String str,
361                                boolean caseSensitive) {
362        char[] patArr = pattern.toCharArray();
363        char[] strArr = str.toCharArray();
364        int patIdxStart = 0;
365        int patIdxEnd = patArr.length - 1;
366        int strIdxStart = 0;
367        int strIdxEnd = strArr.length - 1;
368        char ch;
369
370        boolean containsStar = false;
371        for (int i = 0; i < patArr.length; i++) {
372            if (patArr[i] == '*') {
373                containsStar = true;
374                break;
375            }
376        }
377
378        if (!containsStar) {
379            // No '*'s, so we make a shortcut
380            if (patIdxEnd != strIdxEnd) {
381                return false; // Pattern and string do not have the same size
382            }
383            for (int i = 0; i <= patIdxEnd; i++) {
384                ch = patArr[i];
385                if (ch != '?') {
386                    if (different(caseSensitive, ch, strArr[i])) {
387                        return false; // Character mismatch
388                    }
389                }
390            }
391            return true; // String matches against pattern
392        }
393
394        if (patIdxEnd == 0) {
395            return true; // Pattern contains only '*', which matches anything
396        }
397
398        // Process characters before first star
399        while (true) {
400            ch = patArr[patIdxStart];
401            if (ch == '*' || strIdxStart > strIdxEnd) {
402                break;
403            }
404            if (ch != '?') {
405                if (different(caseSensitive, ch, strArr[strIdxStart])) {
406                    return false; // Character mismatch
407                }
408            }
409            patIdxStart++;
410            strIdxStart++;
411        }
412        if (strIdxStart > strIdxEnd) {
413            // All characters in the string are used. Check if only '*'s are
414            // left in the pattern. If so, we succeeded. Otherwise failure.
415            return allStars(patArr, patIdxStart, patIdxEnd);
416        }
417
418        // Process characters after last star
419        while (true) {
420            ch = patArr[patIdxEnd];
421            if (ch == '*' || strIdxStart > strIdxEnd) {
422                break;
423            }
424            if (ch != '?') {
425                if (different(caseSensitive, ch, strArr[strIdxEnd])) {
426                    return false; // Character mismatch
427                }
428            }
429            patIdxEnd--;
430            strIdxEnd--;
431        }
432        if (strIdxStart > strIdxEnd) {
433            // All characters in the string are used. Check if only '*'s are
434            // left in the pattern. If so, we succeeded. Otherwise failure.
435            return allStars(patArr, patIdxStart, patIdxEnd);
436        }
437
438        // process pattern between stars. padIdxStart and patIdxEnd point
439        // always to a '*'.
440        while (patIdxStart != patIdxEnd && strIdxStart <= strIdxEnd) {
441            int patIdxTmp = -1;
442            for (int i = patIdxStart + 1; i <= patIdxEnd; i++) {
443                if (patArr[i] == '*') {
444                    patIdxTmp = i;
445                    break;
446                }
447            }
448            if (patIdxTmp == patIdxStart + 1) {
449                // Two stars next to each other, skip the first one.
450                patIdxStart++;
451                continue;
452            }
453            // Find the pattern between padIdxStart & padIdxTmp in str between
454            // strIdxStart & strIdxEnd
455            int patLength = (patIdxTmp - patIdxStart - 1);
456            int strLength = (strIdxEnd - strIdxStart + 1);
457            int foundIdx = -1;
458            strLoop:
459            for (int i = 0; i <= strLength - patLength; i++) {
460                for (int j = 0; j < patLength; j++) {
461                    ch = patArr[patIdxStart + j + 1];
462                    if (ch != '?') {
463                        if (different(caseSensitive, ch,
464                                      strArr[strIdxStart + i + j])) {
465                            continue strLoop;
466                        }
467                    }
468                }
469
470                foundIdx = strIdxStart + i;
471                break;
472            }
473
474            if (foundIdx == -1) {
475                return false;
476            }
477
478            patIdxStart = patIdxTmp;
479            strIdxStart = foundIdx + patLength;
480        }
481
482        // All characters in the string are used. Check if only '*'s are left
483        // in the pattern. If so, we succeeded. Otherwise failure.
484        return allStars(patArr, patIdxStart, patIdxEnd);
485    }
486
487    private static boolean allStars(char[] chars, int start, int end) {
488        for (int i = start; i <= end; ++i) {
489            if (chars[i] != '*') {
490                return false;
491            }
492        }
493        return true;
494    }
495
496    private static boolean different(
497        boolean caseSensitive, char ch, char other) {
498        return caseSensitive
499            ? ch != other
500            : Character.toUpperCase(ch) != Character.toUpperCase(other);
501    }
502
503    /**
504     * Breaks a path up into a Vector of path elements, tokenizing on
505     * <code>File.separator</code>.
506     *
507     * @param path Path to tokenize. Must not be <code>null</code>.
508     *
509     * @return a Vector of path elements from the tokenized path
510     */
511    @SuppressWarnings("rawtypes")
512    public static Vector tokenizePath (String path) {
513        return tokenizePath(path, File.separator);
514    }
515
516    /**
517     * Breaks a path up into a Vector of path elements, tokenizing on
518     *
519     * @param path Path to tokenize. Must not be <code>null</code>.
520     * @param separator the separator against which to tokenize.
521     *
522     * @return a Vector of path elements from the tokenized path
523     * @since Ant 1.6
524     */
525    @SuppressWarnings({
526            "rawtypes", "unchecked"
527    })
528    public static Vector tokenizePath (String path, String separator) {
529        Vector ret = new Vector();
530        if (FileUtils.isAbsolutePath(path)) {
531            String[] s = FILE_UTILS.dissect(path);
532            ret.add(s[0]);
533            path = s[1];
534        }
535        StringTokenizer st = new StringTokenizer(path, separator);
536        while (st.hasMoreTokens()) {
537            ret.addElement(st.nextToken());
538        }
539        return ret;
540    }
541
542    /**
543     * Same as {@link #tokenizePath tokenizePath} but hopefully faster.
544     */
545    /*package*/ static String[] tokenizePathAsArray(String path) {
546        String root = null;
547        if (FileUtils.isAbsolutePath(path)) {
548            String[] s = FILE_UTILS.dissect(path);
549            root = s[0];
550            path = s[1];
551        }
552        char sep = File.separatorChar;
553        int start = 0;
554        int len = path.length();
555        int count = 0;
556        for (int pos = 0; pos < len; pos++) {
557            if (path.charAt(pos) == sep) {
558                if (pos != start) {
559                    count++;
560                }
561                start = pos + 1;
562            }
563        }
564        if (len != start) {
565            count++;
566        }
567        String[] l = new String[count + ((root == null) ? 0 : 1)];
568
569        if (root != null) {
570            l[0] = root;
571            count = 1;
572        } else {
573            count = 0;
574        }
575        start = 0;
576        for (int pos = 0; pos < len; pos++) {
577            if (path.charAt(pos) == sep) {
578                if (pos != start) {
579                    String tok = path.substring(start, pos);
580                    l[count++] = tok;
581                }
582                start = pos + 1;
583            }
584        }
585        if (len != start) {
586            String tok = path.substring(start);
587            l[count/*++*/] = tok;
588        }
589        return l;
590    }
591
592    /**
593     * Returns dependency information on these two files. If src has been
594     * modified later than target, it returns true. If target doesn't exist,
595     * it likewise returns true. Otherwise, target is newer than src and
596     * is not out of date, thus the method returns false. It also returns
597     * false if the src file doesn't even exist, since how could the
598     * target then be out of date.
599     *
600     * @param src the original file
601     * @param target the file being compared against
602     * @param granularity the amount in seconds of slack we will give in
603     *        determining out of dateness
604     * @return whether the target is out of date
605     */
606    public static boolean isOutOfDate(File src, File target, int granularity) {
607        if (!src.exists()) {
608            return false;
609        }
610        if (!target.exists()) {
611            return true;
612        }
613        if ((src.lastModified() - granularity) > target.lastModified()) {
614            return true;
615        }
616        return false;
617    }
618
619    /**
620     * "Flattens" a string by removing all whitespace (space, tab, linefeed,
621     * carriage return, and formfeed). This uses StringTokenizer and the
622     * default set of tokens as documented in the single arguement constructor.
623     *
624     * @param input a String to remove all whitespace.
625     * @return a String that has had all whitespace removed.
626     */
627    public static String removeWhitespace(String input) {
628        StringBuffer result = new StringBuffer();
629        if (input != null) {
630            StringTokenizer st = new StringTokenizer(input);
631            while (st.hasMoreTokens()) {
632                result.append(st.nextToken());
633            }
634        }
635        return result.toString();
636    }
637
638    /**
639     * Tests if a string contains stars or question marks
640     * @param input a String which one wants to test for containing wildcard
641     * @return true if the string contains at least a star or a question mark
642     */
643    public static boolean hasWildcards(String input) {
644        return (input.indexOf('*') != -1 || input.indexOf('?') != -1);
645    }
646}
647
648