001/*
002 * The Apache Software License, Version 1.1
003 *
004 * Copyright (c) 2000-2003 The Apache Software Foundation.  All rights
005 * reserved.
006 *
007 * Redistribution and use in source and binary forms, with or without
008 * modification, are permitted provided that the following conditions
009 * are met:
010 *
011 * 1. Redistributions of source code must retain the above copyright
012 *    notice, this list of conditions and the following disclaimer.
013 *
014 * 2. Redistributions in binary form must reproduce the above copyright
015 *    notice, this list of conditions and the following disclaimer in
016 *    the documentation and/or other materials provided with the
017 *    distribution.
018 *
019 * 3. The end-user documentation included with the redistribution, if
020 *    any, must include the following acknowlegement:
021 *       "This product includes software developed by the
022 *        Apache Software Foundation (http://www.codehaus.org/)."
023 *    Alternately, this acknowlegement may appear in the software itself,
024 *    if and wherever such third-party acknowlegements normally appear.
025 *
026 * 4. The names "Ant" and "Apache Software
027 *    Foundation" must not be used to endorse or promote products derived
028 *    from this software without prior written permission. For written
029 *    permission, please contact codehaus@codehaus.org.
030 *
031 * 5. Products derived from this software may not be called "Apache"
032 *    nor may "Apache" appear in their names without prior written
033 *    permission of the Apache Group.
034 *
035 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
036 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
037 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
038 * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
039 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
040 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
041 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
042 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
043 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
044 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
045 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
046 * SUCH DAMAGE.
047 * ====================================================================
048 *
049 * This software consists of voluntary contributions made by many
050 * individuals on behalf of the Apache Software Foundation.  For more
051 * information on the Apache Software Foundation, please see
052 * <http://www.codehaus.org/>.
053 */
054
055package org.jszip.pseudo.io;
056
057import org.codehaus.plexus.util.AbstractScanner;
058import org.codehaus.plexus.util.SelectorUtils;
059import org.codehaus.plexus.util.StringUtils;
060
061import java.io.File;
062import java.io.IOException;
063import java.util.Vector;
064
065/**
066 * Class for scanning a directory for files/directories which match certain
067 * criteria.
068 * <p>
069 * These criteria consist of selectors and patterns which have been specified.
070 * With the selectors you can select which files you want to have included.
071 * Files which are not selected are excluded. With patterns you can include
072 * or exclude files based on their filename.
073 * <p>
074 * The idea is simple. A given directory is recursively scanned for all files
075 * and directories. Each file/directory is matched against a set of selectors,
076 * including special support for matching against filenames with include and
077 * and exclude patterns. Only files/directories which match at least one
078 * pattern of the include pattern list or other file selector, and don't match
079 * any pattern of the exclude pattern list or fail to match against a required
080 * selector will be placed in the list of files/directories found.
081 * <p>
082 * When no list of include patterns is supplied, "**" will be used, which
083 * means that everything will be matched. When no list of exclude patterns is
084 * supplied, an empty list is used, such that nothing will be excluded. When
085 * no selectors are supplied, none are applied.
086 * <p>
087 * The filename pattern matching is done as follows:
088 * The name to be matched is split up in path segments. A path segment is the
089 * name of a directory or file, which is bounded by
090 * <code>File.separator</code> ('/' under UNIX, '\' under Windows).
091 * For example, "abc/def/ghi/xyz.java" is split up in the segments "abc",
092 * "def","ghi" and "xyz.java".
093 * The same is done for the pattern against which should be matched.
094 * <p>
095 * The segments of the name and the pattern are then matched against each
096 * other. When '**' is used for a path segment in the pattern, it matches
097 * zero or more path segments of the name.
098 * <p>
099 * There is a special case regarding the use of <code>File.separator</code>s
100 * at the beginning of the pattern and the string to match:<br>
101 * When a pattern starts with a <code>File.separator</code>, the string
102 * to match must also start with a <code>File.separator</code>.
103 * When a pattern does not start with a <code>File.separator</code>, the
104 * string to match may not start with a <code>File.separator</code>.
105 * When one of these rules is not obeyed, the string will not
106 * match.
107 * <p>
108 * When a name path segment is matched against a pattern path segment, the
109 * following special characters can be used:<br>
110 * '*' matches zero or more characters<br>
111 * '?' matches one character.
112 * <p>
113 * Examples:
114 * <p>
115 * "**\*.class" matches all .class files/dirs in a directory tree.
116 * <p>
117 * "test\a??.java" matches all files/dirs which start with an 'a', then two
118 * more characters and then ".java", in a directory called test.
119 * <p>
120 * "**" matches everything in a directory tree.
121 * <p>
122 * "**\test\**\XYZ*" matches all files/dirs which start with "XYZ" and where
123 * there is a parent directory called test (e.g. "abc\test\def\ghi\XYZ123").
124 * <p>
125 * Case sensitivity may be turned off if necessary. By default, it is
126 * turned on.
127 * <p>
128 * Example of usage:
129 * <pre>
130 *   String[] includes = {"**\\*.class"};
131 *   String[] excludes = {"modules\\*\\**"};
132 *   ds.setIncludes(includes);
133 *   ds.setExcludes(excludes);
134 *   ds.setBasedir(new File("test"));
135 *   ds.setCaseSensitive(true);
136 *   ds.scan();
137 *
138 *   System.out.println("FILES:");
139 *   String[] files = ds.getIncludedFiles();
140 *   for (int i = 0; i < files.length; i++) {
141 *     System.out.println(files[i]);
142 *   }
143 * </pre>
144 * This will scan a directory called test for .class files, but excludes all
145 * files in all proper subdirectories of a directory called "modules"
146 *
147 * @author Arnout J. Kuiper
148 * <a href="mailto:ajkuiper@wxs.nl">ajkuiper@wxs.nl</a>
149 * @author Magesh Umasankar
150 * @author <a href="mailto:bruce@callenish.com">Bruce Atherton</a>
151 * @author <a href="mailto:levylambert@tiscali-dsl.de">Antoine Levy-Lambert</a>
152 */
153public class PseudoDirectoryScanner
154{
155
156    public static final String[] DEFAULTEXCLUDES = AbstractScanner.DEFAULTEXCLUDES;
157
158    protected PseudoFileSystem fs;
159
160    /** The base directory to be scanned. */
161    protected PseudoFile basedir;
162
163    /** The files which matched at least one include and no excludes
164     *  and were selected.
165     */
166    protected Vector<String> filesIncluded;
167
168    /** The files which did not match any includes or selectors. */
169    protected Vector<String> filesNotIncluded;
170
171    /**
172     * The files which matched at least one include and at least
173     * one exclude.
174     */
175    protected Vector<String> filesExcluded;
176
177    /** The directories which matched at least one include and no excludes
178     *  and were selected.
179     */
180    protected Vector<String> dirsIncluded;
181
182    /** The directories which were found and did not match any includes. */
183    protected Vector<String> dirsNotIncluded;
184
185    /**
186     * The directories which matched at least one include and at least one
187     * exclude.
188     */
189    protected Vector<String> dirsExcluded;
190
191    /** The files which matched at least one include and no excludes and
192     *  which a selector discarded.
193     */
194    protected Vector<String> filesDeselected;
195
196    /** The directories which matched at least one include and no excludes
197     *  but which a selector discarded.
198     */
199    protected Vector<String> dirsDeselected;
200
201    /** Whether or not our results were built by a slow scan. */
202    protected boolean haveSlowResults = false;
203
204    /**
205     * Whether or not symbolic links should be followed.
206     *
207     * @since Ant 1.5
208     */
209    private boolean followSymlinks = true;
210
211    /** Whether or not everything tested so far has been included. */
212    protected boolean everythingIncluded = true;
213
214    /**
215     * Sole constructor.
216     */
217    public PseudoDirectoryScanner()
218    {
219    }
220
221    public PseudoFileSystem getFileSystem() {
222        return fs == null ? PseudoFileSystem.current() : fs;
223    }
224
225    public void setFileSystem(PseudoFileSystem fs) {
226        this.fs = fs;
227    }
228
229    /**
230     * Sets the base directory to be scanned. This is the directory which is
231     * scanned recursively.
232     *
233     * @param basedir The base directory for scanning.
234     *                Should not be <code>null</code>.
235     */
236    public void setBasedir( PseudoFile basedir )
237    {
238        this.basedir = basedir;
239    }
240
241    /**
242     * Returns the base directory to be scanned.
243     * This is the directory which is scanned recursively.
244     *
245     * @return the base directory to be scanned
246     */
247    public PseudoFile getBasedir()
248    {
249        return basedir;
250    }
251
252    /**
253     * Sets whether or not symbolic links should be followed.
254     *
255     * @param followSymlinks whether or not symbolic links should be followed
256     */
257    public void setFollowSymlinks( boolean followSymlinks )
258    {
259        this.followSymlinks = followSymlinks;
260    }
261
262    /**
263     * Returns whether or not the scanner has included all the files or
264     * directories it has come across so far.
265     *
266     * @return <code>true</code> if all files and directories which have
267     *         been found so far have been included.
268     */
269    public boolean isEverythingIncluded()
270    {
271        return everythingIncluded;
272    }
273
274    /**
275     * Scans the base directory for files which match at least one include
276     * pattern and don't match any exclude patterns. If there are selectors
277     * then the files must pass muster there, as well.
278     *
279     * @exception IllegalStateException if the base directory was set
280     *            incorrectly (i.e. if it is <code>null</code>, doesn't exist,
281     *            or isn't a directory).
282     */
283    public void scan() throws IllegalStateException
284    {
285        if ( basedir == null )
286        {
287            throw new IllegalStateException( "No basedir set" );
288        }
289        if ( !basedir.exists() )
290        {
291            throw new IllegalStateException( "basedir " + basedir
292                                             + " does not exist" );
293        }
294        if ( !basedir.isDirectory() )
295        {
296            throw new IllegalStateException( "basedir " + basedir
297                                             + " is not a directory" );
298        }
299
300        setupDefaultFilters();
301
302        filesIncluded = new Vector<String>();
303        filesNotIncluded = new Vector<String>();
304        filesExcluded = new Vector<String>();
305        filesDeselected = new Vector<String>();
306        dirsIncluded = new Vector<String>();
307        dirsNotIncluded = new Vector<String>();
308        dirsExcluded = new Vector<String>();
309        dirsDeselected = new Vector<String>();
310
311        if ( isIncluded( "" ) )
312        {
313            if ( !isExcluded( "" ) )
314            {
315                if ( isSelected( "", basedir ) )
316                {
317                    dirsIncluded.addElement( "" );
318                }
319                else
320                {
321                    dirsDeselected.addElement( "" );
322                }
323            }
324            else
325            {
326                dirsExcluded.addElement( "" );
327            }
328        }
329        else
330        {
331            dirsNotIncluded.addElement( "" );
332        }
333        scandir( basedir, "", true );
334    }
335
336    /**
337     * Top level invocation for a slow scan. A slow scan builds up a full
338     * list of excluded/included files/directories, whereas a fast scan
339     * will only have full results for included files, as it ignores
340     * directories which can't possibly hold any included files/directories.
341     * <p>
342     * Returns immediately if a slow scan has already been completed.
343     */
344    protected void slowScan()
345    {
346        if ( haveSlowResults )
347        {
348            return;
349        }
350
351        String[] excl = new String[dirsExcluded.size()];
352        dirsExcluded.copyInto( excl );
353
354        String[] notIncl = new String[dirsNotIncluded.size()];
355        dirsNotIncluded.copyInto( notIncl );
356
357        for ( String anExcl : excl )
358        {
359            if ( !couldHoldIncluded( anExcl ) )
360            {
361                scandir( getFileSystem().getPseudoFile(basedir, anExcl), anExcl + File.separator, false );
362            }
363        }
364
365        for ( String aNotIncl : notIncl )
366        {
367            if ( !couldHoldIncluded( aNotIncl ) )
368            {
369                scandir( getFileSystem().getPseudoFile(basedir, aNotIncl), aNotIncl + File.separator, false );
370            }
371        }
372
373        haveSlowResults = true;
374    }
375
376    /**
377     * Scans the given directory for files and directories. Found files and
378     * directories are placed in their respective collections, based on the
379     * matching of includes, excludes, and the selectors.  When a directory
380     * is found, it is scanned recursively.
381     *
382     * @param dir   The directory to scan. Must not be <code>null</code>.
383     * @param vpath The path relative to the base directory (needed to
384     *              prevent problems with an absolute path when using
385     *              dir). Must not be <code>null</code>.
386     * @param fast  Whether or not this call is part of a fast scan.
387     *
388     * @see #filesIncluded
389     * @see #filesNotIncluded
390     * @see #filesExcluded
391     * @see #dirsIncluded
392     * @see #dirsNotIncluded
393     * @see #dirsExcluded
394     * @see #slowScan
395     */
396    protected void scandir( PseudoFile dir, String vpath, boolean fast )
397    {
398        PseudoFile[] children = getFileSystem().listChildren(dir, PseudoFileFilter.FILTER_NONE);
399        String[] newfiles = new String[children.length];
400        for (int i = 0; i < children.length; i++) {
401            newfiles[i] = children[i].getName();
402        }
403
404        if ( !followSymlinks )
405        {
406            Vector<String> noLinks = new Vector<String>();
407            for ( String newfile : newfiles )
408            {
409                try
410                {
411                    if ( isSymbolicLink( dir, newfile ) )
412                    {
413                        String name = vpath + newfile;
414                        PseudoFile file = getFileSystem().getPseudoFile(dir, newfile);
415                        if ( file.isDirectory() )
416                        {
417                            dirsExcluded.addElement( name );
418                        }
419                        else
420                        {
421                            filesExcluded.addElement( name );
422                        }
423                    }
424                    else
425                    {
426                        noLinks.addElement( newfile );
427                    }
428                }
429                catch ( IOException ioe )
430                {
431                    String msg = "IOException caught while checking " + "for links, couldn't get cannonical path!";
432                    // will be caught and redirected to Ant's logging system
433                    System.err.println( msg );
434                    noLinks.addElement( newfile );
435                }
436            }
437            newfiles = new String[noLinks.size()];
438            noLinks.copyInto( newfiles );
439        }
440
441        for ( String newfile : newfiles )
442        {
443            String name = vpath + newfile;
444            PseudoFile file = getFileSystem().getPseudoFile(dir, newfile);
445            if ( file.isDirectory() )
446            {
447                if ( isIncluded( name ) )
448                {
449                    if ( !isExcluded( name ) )
450                    {
451                        if ( isSelected( name, file ) )
452                        {
453                            dirsIncluded.addElement( name );
454                            if ( fast )
455                            {
456                                scandir( file, name + getFileSystem().getPathSeparator(), fast );
457                            }
458                        }
459                        else
460                        {
461                            everythingIncluded = false;
462                            dirsDeselected.addElement( name );
463                            if ( fast && couldHoldIncluded( name ) )
464                            {
465                                scandir( file, name + getFileSystem().getPathSeparator(), fast );
466                            }
467                        }
468
469                    }
470                    else
471                    {
472                        everythingIncluded = false;
473                        dirsExcluded.addElement( name );
474                        if ( fast && couldHoldIncluded( name ) )
475                        {
476                            scandir( file, name + getFileSystem().getPathSeparator(), fast );
477                        }
478                    }
479                }
480                else
481                {
482                    everythingIncluded = false;
483                    dirsNotIncluded.addElement( name );
484                    if ( fast && couldHoldIncluded( name ) )
485                    {
486                        scandir( file, name + getFileSystem().getPathSeparator(), fast );
487                    }
488                }
489                if ( !fast )
490                {
491                    scandir( file, name + getFileSystem().getPathSeparator(), fast );
492                }
493            }
494            else if ( file.isFile() )
495            {
496                if ( isIncluded( name ) )
497                {
498                    if ( !isExcluded( name ) )
499                    {
500                        if ( isSelected( name, file ) )
501                        {
502                            filesIncluded.addElement( name );
503                        }
504                        else
505                        {
506                            everythingIncluded = false;
507                            filesDeselected.addElement( name );
508                        }
509                    }
510                    else
511                    {
512                        everythingIncluded = false;
513                        filesExcluded.addElement( name );
514                    }
515                }
516                else
517                {
518                    everythingIncluded = false;
519                    filesNotIncluded.addElement( name );
520                }
521            }
522        }
523    }
524
525    /**
526     * Tests whether a name should be selected.
527     *
528     * @param name the filename to check for selecting
529     * @param file the PseudoFile object for this filename
530     * @return <code>false</code> when the selectors says that the file
531     *         should not be selected, <code>true</code> otherwise.
532     */
533    protected boolean isSelected( String name, PseudoFile file )
534    {
535        return true;
536    }
537
538    /**
539     * Returns the names of the files which matched at least one of the
540     * include patterns and none of the exclude patterns.
541     * The names are relative to the base directory.
542     *
543     * @return the names of the files which matched at least one of the
544     *         include patterns and none of the exclude patterns.
545     */
546    public String[] getIncludedFiles()
547    {
548        String[] files = new String[filesIncluded.size()];
549        filesIncluded.copyInto( files );
550        return files;
551    }
552
553    /**
554     * Returns the names of the files which matched none of the include
555     * patterns. The names are relative to the base directory. This involves
556     * performing a slow scan if one has not already been completed.
557     *
558     * @return the names of the files which matched none of the include
559     *         patterns.
560     *
561     * @see #slowScan
562     */
563    public String[] getNotIncludedFiles()
564    {
565        slowScan();
566        String[] files = new String[filesNotIncluded.size()];
567        filesNotIncluded.copyInto( files );
568        return files;
569    }
570
571    /**
572     * Returns the names of the files which matched at least one of the
573     * include patterns and at least one of the exclude patterns.
574     * The names are relative to the base directory. This involves
575     * performing a slow scan if one has not already been completed.
576     *
577     * @return the names of the files which matched at least one of the
578     *         include patterns and at at least one of the exclude patterns.
579     *
580     * @see #slowScan
581     */
582    public String[] getExcludedFiles()
583    {
584        slowScan();
585        String[] files = new String[filesExcluded.size()];
586        filesExcluded.copyInto( files );
587        return files;
588    }
589
590    /**
591     * <p>Returns the names of the files which were selected out and
592     * therefore not ultimately included.</p>
593     *
594     * <p>The names are relative to the base directory. This involves
595     * performing a slow scan if one has not already been completed.</p>
596     *
597     * @return the names of the files which were deselected.
598     *
599     * @see #slowScan
600     */
601    public String[] getDeselectedFiles()
602    {
603        slowScan();
604        String[] files = new String[filesDeselected.size()];
605        filesDeselected.copyInto( files );
606        return files;
607    }
608
609    /**
610     * Returns the names of the directories which matched at least one of the
611     * include patterns and none of the exclude patterns.
612     * The names are relative to the base directory.
613     *
614     * @return the names of the directories which matched at least one of the
615     * include patterns and none of the exclude patterns.
616     */
617    public String[] getIncludedDirectories()
618    {
619        String[] directories = new String[dirsIncluded.size()];
620        dirsIncluded.copyInto( directories );
621        return directories;
622    }
623
624    /**
625     * Returns the names of the directories which matched none of the include
626     * patterns. The names are relative to the base directory. This involves
627     * performing a slow scan if one has not already been completed.
628     *
629     * @return the names of the directories which matched none of the include
630     * patterns.
631     *
632     * @see #slowScan
633     */
634    public String[] getNotIncludedDirectories()
635    {
636        slowScan();
637        String[] directories = new String[dirsNotIncluded.size()];
638        dirsNotIncluded.copyInto( directories );
639        return directories;
640    }
641
642    /**
643     * Returns the names of the directories which matched at least one of the
644     * include patterns and at least one of the exclude patterns.
645     * The names are relative to the base directory. This involves
646     * performing a slow scan if one has not already been completed.
647     *
648     * @return the names of the directories which matched at least one of the
649     * include patterns and at least one of the exclude patterns.
650     *
651     * @see #slowScan
652     */
653    public String[] getExcludedDirectories()
654    {
655        slowScan();
656        String[] directories = new String[dirsExcluded.size()];
657        dirsExcluded.copyInto( directories );
658        return directories;
659    }
660
661    /**
662     * <p>Returns the names of the directories which were selected out and
663     * therefore not ultimately included.</p>
664     *
665     * <p>The names are relative to the base directory. This involves
666     * performing a slow scan if one has not already been completed.</p>
667     *
668     * @return the names of the directories which were deselected.
669     *
670     * @see #slowScan
671     */
672    public String[] getDeselectedDirectories()
673    {
674        slowScan();
675        String[] directories = new String[dirsDeselected.size()];
676        dirsDeselected.copyInto( directories );
677        return directories;
678    }
679
680    /**
681     * Checks whether a given file is a symbolic link.
682     *
683     * <p>It doesn't really test for symbolic links but whether the
684     * canonical and absolute paths of the file are identical - this
685     * may lead to false positives on some platforms.</p>
686     *
687     * @param parent the parent directory of the file to test
688     * @param name the name of the file to test.
689     *
690     * @since Ant 1.5
691     * @return true if it's a symbolic link
692     * @throws java.io.IOException .
693     */
694    public boolean isSymbolicLink( PseudoFile parent, String name )
695        throws IOException
696    {
697        return false;
698    }
699
700    /** The patterns for the files to be included. */
701    protected String[] includes;
702
703    /** The patterns for the files to be excluded. */
704    protected String[] excludes;
705
706    /**
707     * Whether or not the file system should be treated as a case sensitive
708     * one.
709     */
710    protected boolean isCaseSensitive = true;
711
712    /**
713     * Sets whether or not the file system should be regarded as case sensitive.
714     *
715     * @param isCaseSensitive whether or not the file system should be
716     *                        regarded as a case sensitive one
717     */
718    public void setCaseSensitive( boolean isCaseSensitive )
719    {
720        this.isCaseSensitive = isCaseSensitive;
721    }
722
723    /**
724     * Tests whether or not a given path matches the start of a given
725     * pattern up to the first "**".
726     * <p>
727     * This is not a general purpose test and should only be used if you
728     * can live with false positives. For example, <code>pattern=**\a</code>
729     * and <code>str=b</code> will yield <code>true</code>.
730     *
731     * @param pattern The pattern to match against. Must not be
732     *                <code>null</code>.
733     * @param str     The path to match, as a String. Must not be
734     *                <code>null</code>.
735     *
736     * @return whether or not a given path matches the start of a given
737     * pattern up to the first "**".
738     */
739    protected static boolean matchPatternStart( String pattern, String str )
740    {
741        return SelectorUtils.matchPatternStart(pattern, str);
742    }
743
744    /**
745     * Tests whether or not a given path matches the start of a given
746     * pattern up to the first "**".
747     * <p>
748     * This is not a general purpose test and should only be used if you
749     * can live with false positives. For example, <code>pattern=**\a</code>
750     * and <code>str=b</code> will yield <code>true</code>.
751     *
752     * @param pattern The pattern to match against. Must not be
753     *                <code>null</code>.
754     * @param str     The path to match, as a String. Must not be
755     *                <code>null</code>.
756     * @param isCaseSensitive Whether or not matching should be performed
757     *                        case sensitively.
758     *
759     * @return whether or not a given path matches the start of a given
760     * pattern up to the first "**".
761     */
762    protected static boolean matchPatternStart( String pattern, String str,
763                                                boolean isCaseSensitive )
764    {
765        return SelectorUtils.matchPatternStart( pattern, str, isCaseSensitive );
766    }
767
768    /**
769     * Tests whether or not a given path matches a given pattern.
770     *
771     * @param pattern The pattern to match against. Must not be
772     *                <code>null</code>.
773     * @param str     The path to match, as a String. Must not be
774     *                <code>null</code>.
775     *
776     * @return <code>true</code> if the pattern matches against the string,
777     *         or <code>false</code> otherwise.
778     */
779    protected static boolean matchPath( String pattern, String str )
780    {
781        return SelectorUtils.matchPath( pattern, str );
782    }
783
784    /**
785     * Tests whether or not a given path matches a given pattern.
786     *
787     * @param pattern The pattern to match against. Must not be
788     *                <code>null</code>.
789     * @param str     The path to match, as a String. Must not be
790     *                <code>null</code>.
791     * @param isCaseSensitive Whether or not matching should be performed
792     *                        case sensitively.
793     *
794     * @return <code>true</code> if the pattern matches against the string,
795     *         or <code>false</code> otherwise.
796     */
797    protected static boolean matchPath( String pattern, String str,
798                                        boolean isCaseSensitive )
799    {
800        return SelectorUtils.matchPath( pattern, str, isCaseSensitive );
801    }
802
803    /**
804     * Tests whether or not a string matches against a pattern.
805     * The pattern may contain two special characters:<br>
806     * '*' means zero or more characters<br>
807     * '?' means one and only one character
808     *
809     * @param pattern The pattern to match against.
810     *                Must not be <code>null</code>.
811     * @param str     The string which must be matched against the pattern.
812     *                Must not be <code>null</code>.
813     *
814     * @return <code>true</code> if the string matches against the pattern,
815     *         or <code>false</code> otherwise.
816     */
817    public static boolean match( String pattern, String str )
818    {
819        return SelectorUtils.match( pattern, str );
820    }
821
822    /**
823     * Tests whether or not a string matches against a pattern.
824     * The pattern may contain two special characters:<br>
825     * '*' means zero or more characters<br>
826     * '?' means one and only one character
827     *
828     * @param pattern The pattern to match against.
829     *                Must not be <code>null</code>.
830     * @param str     The string which must be matched against the pattern.
831     *                Must not be <code>null</code>.
832     * @param isCaseSensitive Whether or not matching should be performed
833     *                        case sensitively.
834     *
835     *
836     * @return <code>true</code> if the string matches against the pattern,
837     *         or <code>false</code> otherwise.
838     */
839    protected static boolean match( String pattern, String str,
840                                    boolean isCaseSensitive )
841    {
842        return SelectorUtils.match( pattern, str, isCaseSensitive );
843    }
844
845
846    /**
847     * Sets the list of include patterns to use. All '/' and '\' characters
848     * are replaced by <code>File.separatorChar</code>, so the separator used
849     * need not match <code>File.separatorChar</code>.
850     * <p>
851     * When a pattern ends with a '/' or '\', "**" is appended.
852     *
853     * @param includes A list of include patterns.
854     *                 May be <code>null</code>, indicating that all files
855     *                 should be included. If a non-<code>null</code>
856     *                 list is given, all elements must be
857     * non-<code>null</code>.
858     */
859    public void setIncludes( String[] includes )
860    {
861        if ( includes == null )
862        {
863            this.includes = null;
864        }
865        else
866        {
867            this.includes = new String[includes.length];
868            for ( int i = 0; i < includes.length; i++ )
869            {
870                this.includes[i] = normalizePattern( includes[i] );
871            }
872        }
873    }
874
875    /**
876     * Sets the list of exclude patterns to use. All '/' and '\' characters
877     * are replaced by <code>File.separatorChar</code>, so the separator used
878     * need not match <code>File.separatorChar</code>.
879     * <p>
880     * When a pattern ends with a '/' or '\', "**" is appended.
881     *
882     * @param excludes A list of exclude patterns.
883     *                 May be <code>null</code>, indicating that no files
884     *                 should be excluded. If a non-<code>null</code> list is
885     *                 given, all elements must be non-<code>null</code>.
886     */
887    public void setExcludes( String[] excludes )
888    {
889        if ( excludes == null )
890        {
891            this.excludes = null;
892        }
893        else
894        {
895            this.excludes = new String[excludes.length];
896            for ( int i = 0; i < excludes.length; i++ )
897            {
898                this.excludes[i] = normalizePattern( excludes[i] );
899            }
900        }
901    }
902
903    /**
904     * Normalizes the pattern, e.g. converts forward and backward slashes to the platform-specific file separator.
905     *
906     * @param pattern The pattern to normalize, must not be <code>null</code>.
907     * @return The normalized pattern, never <code>null</code>.
908     */
909    private String normalizePattern( String pattern )
910    {
911        pattern = pattern.trim();
912
913        if ( pattern.startsWith( SelectorUtils.REGEX_HANDLER_PREFIX ) )
914        {
915            if ( File.separatorChar == '\\' )
916            {
917                pattern = StringUtils.replace(pattern, "/", "\\\\");
918            }
919            else
920            {
921                pattern = StringUtils.replace( pattern, "\\\\", "/" );
922            }
923        }
924        else
925        {
926            pattern = pattern.replace( File.separatorChar == '/' ? '\\' : '/', File.separatorChar );
927
928            if ( pattern.endsWith( File.separator ) )
929            {
930                pattern += "**";
931            }
932        }
933
934        return pattern;
935    }
936
937    /**
938     * Tests whether or not a name matches against at least one include
939     * pattern.
940     *
941     * @param name The name to match. Must not be <code>null</code>.
942     * @return <code>true</code> when the name matches against at least one
943     *         include pattern, or <code>false</code> otherwise.
944     */
945    protected boolean isIncluded( String name )
946    {
947        for ( int i = 0; i < includes.length; i++ )
948        {
949            if ( matchPath( includes[i], name, isCaseSensitive ) )
950            {
951                return true;
952            }
953        }
954        return false;
955    }
956
957    /**
958     * Tests whether or not a name matches the start of at least one include
959     * pattern.
960     *
961     * @param name The name to match. Must not be <code>null</code>.
962     * @return <code>true</code> when the name matches against the start of at
963     *         least one include pattern, or <code>false</code> otherwise.
964     */
965    protected boolean couldHoldIncluded( String name )
966    {
967        for ( int i = 0; i < includes.length; i++ )
968        {
969            if ( matchPatternStart( includes[i], name, isCaseSensitive ) )
970            {
971                return true;
972            }
973        }
974        return false;
975    }
976
977    /**
978     * Tests whether or not a name matches against at least one exclude
979     * pattern.
980     *
981     * @param name The name to match. Must not be <code>null</code>.
982     * @return <code>true</code> when the name matches against at least one
983     *         exclude pattern, or <code>false</code> otherwise.
984     */
985    protected boolean isExcluded( String name )
986    {
987        for ( int i = 0; i < excludes.length; i++ )
988        {
989            if ( matchPath( excludes[i], name, isCaseSensitive ) )
990            {
991                return true;
992            }
993        }
994        return false;
995    }
996
997    /**
998     * Adds default exclusions to the current exclusions set.
999     */
1000    public void addDefaultExcludes()
1001    {
1002        int excludesLength = excludes == null ? 0 : excludes.length;
1003        String[] newExcludes;
1004        newExcludes = new String[excludesLength + DEFAULTEXCLUDES.length];
1005        if ( excludesLength > 0 )
1006        {
1007            System.arraycopy( excludes, 0, newExcludes, 0, excludesLength );
1008        }
1009        for ( int i = 0; i < DEFAULTEXCLUDES.length; i++ )
1010        {
1011            newExcludes[i + excludesLength] = DEFAULTEXCLUDES[i].replace( '/', File.separatorChar );
1012        }
1013        excludes = newExcludes;
1014    }
1015
1016    protected void setupDefaultFilters()
1017    {
1018        if ( includes == null )
1019        {
1020            // No includes supplied, so set it to 'matches all'
1021            includes = new String[1];
1022            includes[0] = "**";
1023        }
1024        if ( excludes == null )
1025        {
1026            excludes = new String[0];
1027        }
1028    }
1029}