View Javadoc
1   package org.apache.maven.shared.utils.io;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.File;
23  import java.io.IOException;
24  import java.util.ArrayList;
25  import java.util.Collections;
26  import java.util.HashSet;
27  import java.util.List;
28  import java.util.Set;
29  
30  import javax.annotation.Nonnull;
31  import javax.annotation.Nullable;
32  
33  /**
34   * Class for scanning a directory for files/directories which match certain criteria.
35   * <p/>
36   * These criteria consist of selectors and patterns which have been specified. With the selectors you can select which
37   * files you want to have included. Files which are not selected are excluded. With patterns you can include or exclude
38   * files based on their filename.
39   * <p/>
40   * The idea is simple. A given directory is recursively scanned for all files and directories. Each file/directory is
41   * matched against a set of selectors, including special support for matching against filenames with include and and
42   * exclude patterns. Only files/directories which match at least one pattern of the include pattern list or other file
43   * selector, and don't match any pattern of the exclude pattern list or fail to match against a required selector will
44   * be placed in the list of files/directories found.
45   * <p/>
46   * When no list of include patterns is supplied, "**" will be used, which means that everything will be matched. When no
47   * list of exclude patterns is supplied, an empty list is used, such that nothing will be excluded. When no selectors
48   * are supplied, none are applied.
49   * <p/>
50   * The filename pattern matching is done as follows: The name to be matched is split up in path segments. A path segment
51   * is the name of a directory or file, which is bounded by <code>File.separator</code> ('/' under UNIX, '\' under
52   * Windows). For example, "abc/def/ghi/xyz.java" is split up in the segments "abc", "def","ghi" and "xyz.java". The same
53   * is done for the pattern against which should be matched.
54   * <p/>
55   * The segments of the name and the pattern are then matched against each other. When '**' is used for a path segment in
56   * the pattern, it matches zero or more path segments of the name.
57   * <p/>
58   * There is a special case regarding the use of <code>File.separator</code>s at the beginning of the pattern and the
59   * string to match:<br>
60   * When a pattern starts with a <code>File.separator</code>, the string to match must also start with a
61   * <code>File.separator</code>. When a pattern does not start with a <code>File.separator</code>, the string to match
62   * may not start with a <code>File.separator</code>. When one of these rules is not obeyed, the string will not match.
63   * <p/>
64   * When a name path segment is matched against a pattern path segment, the following special characters can be used:<br>
65   * '*' matches zero or more characters<br>
66   * '?' matches one character.
67   * <p/>
68   * Examples:
69   * <p/>
70   * "**\*.class" matches all .class files/dirs in a directory tree.
71   * <p/>
72   * "test\a??.java" matches all files/dirs which start with an 'a', then two more characters and then ".java", in a
73   * directory called test.
74   * <p/>
75   * "**" matches everything in a directory tree.
76   * <p/>
77   * "**\test\**\XYZ*" matches all files/dirs which start with "XYZ" and where there is a parent directory called test
78   * (e.g. "abc\test\def\ghi\XYZ123").
79   * <p/>
80   * Case sensitivity may be turned off if necessary. By default, it is turned on.
81   * <p/>
82   * Example of usage:
83   * <p/>
84   * <pre>
85   * String[] includes = { &quot;**\\*.class&quot; };
86   * String[] excludes = { &quot;modules\\*\\**&quot; };
87   * ds.setIncludes( includes );
88   * ds.setExcludes( excludes );
89   * ds.setBasedir( new File( &quot;test&quot; ) );
90   * ds.setCaseSensitive( true );
91   * ds.scan();
92   *
93   * System.out.println( &quot;FILES:&quot; );
94   * String[] files = ds.getIncludedFiles();
95   * for ( int i = 0; i &lt; files.length; i++ )
96   * {
97   *     System.out.println( files[i] );
98   * }
99   * </pre>
100  * <p/>
101  * This will scan a directory called test for .class files, but excludes all files in all proper subdirectories of a
102  * directory called "modules"
103  * <p/>
104  * This class must not be used from multiple Threads concurrently!
105  *
106  * @author Arnout J. Kuiper <a href="mailto:ajkuiper@wxs.nl">ajkuiper@wxs.nl</a>
107  * @author Magesh Umasankar
108  * @author <a href="mailto:bruce@callenish.com">Bruce Atherton</a>
109  * @author <a href="mailto:levylambert@tiscali-dsl.de">Antoine Levy-Lambert</a>
110  */
111 public class DirectoryScanner
112 {
113     /**
114      * Patterns which should be excluded by default.
115      *
116      * @see #addDefaultExcludes()
117      */
118     public static final String[] DEFAULTEXCLUDES = {
119         // Miscellaneous typical temporary files
120         "**/*~", "**/#*#", "**/.#*", "**/%*%", "**/._*",
121 
122         // CVS
123         "**/CVS", "**/CVS/**", "**/.cvsignore",
124 
125         // RCS
126         "**/RCS", "**/RCS/**",
127 
128         // SCCS
129         "**/SCCS", "**/SCCS/**",
130 
131         // Visual SourceSafe
132         "**/vssver.scc",
133 
134         // Subversion
135         "**/.svn", "**/.svn/**",
136 
137         // Arch
138         "**/.arch-ids", "**/.arch-ids/**",
139 
140         // Bazaar
141         "**/.bzr", "**/.bzr/**",
142 
143         // SurroundSCM
144         "**/.MySCMServerInfo",
145 
146         // Mac
147         "**/.DS_Store",
148 
149         // Serena Dimensions Version 10
150         "**/.metadata", "**/.metadata/**",
151 
152         // Mercurial
153         "**/.hg", "**/.hg/**",
154 
155         // git
156         "**/.git", "**/.git/**",
157 
158         // BitKeeper
159         "**/BitKeeper", "**/BitKeeper/**", "**/ChangeSet", "**/ChangeSet/**",
160 
161         // darcs
162         "**/_darcs", "**/_darcs/**", "**/.darcsrepo", "**/.darcsrepo/**", "**/-darcs-backup*", "**/.darcs-temp-mail" };
163 
164     /**
165      * The base directory to be scanned.
166      */
167     private File basedir;
168 
169     /**
170      * The patterns for the files to be included.
171      */
172     private String[] includes;
173 
174     /**
175      * The patterns for the files to be excluded.
176      */
177     private String[] excludes;
178 
179     private MatchPatterns excludesPatterns;
180 
181     private MatchPatterns includesPatterns;
182 
183 
184     /**
185      * The files which matched at least one include and no excludes and were selected.
186      */
187     private List<String> filesIncluded;
188 
189     /**
190      * The files which did not match any includes or selectors.
191      */
192     private List<String> filesNotIncluded;
193 
194     /**
195      * The files which matched at least one include and at least one exclude.
196      */
197     private List<String> filesExcluded;
198 
199     /**
200      * The directories which matched at least one include and no excludes and were selected.
201      */
202     private List<String> dirsIncluded;
203 
204     /**
205      * The directories which were found and did not match any includes.
206      */
207     private List<String> dirsNotIncluded;
208 
209     /**
210      * The directories which matched at least one include and at least one exclude.
211      */
212     private List<String> dirsExcluded;
213 
214     /**
215      * Whether or not our results were built by a slow scan.
216      */
217     private boolean haveSlowResults = false;
218 
219     /**
220      * Whether or not the file system should be treated as a case sensitive one.
221      */
222     private boolean isCaseSensitive = true;
223 
224     /**
225      * Whether or not symbolic links should be followed.
226      *
227      * 
228      */
229     private boolean followSymlinks = true;
230 
231 
232     /**
233      * A {@link ScanConductor} an control the scanning process.
234      */
235     private ScanConductor scanConductor = null;
236 
237     /**
238      * The last ScanAction. We need to store this in the instance as the scan() method doesn't return
239      */
240     private ScanConductor.ScanAction scanAction = null;
241 
242     /**
243      * Sole constructor.
244      */
245     public DirectoryScanner()
246     {
247     }
248 
249     /**
250      * Sets the base directory to be scanned. This is the directory which is scanned recursively. All '/' and '\'
251      * characters are replaced by <code>File.separatorChar</code>, so the separator used need not match
252      * <code>File.separatorChar</code>.
253      *
254      * @param basedir The base directory to scan. Must not be <code>null</code>.
255      */
256     public void setBasedir( final String basedir )
257     {
258         setBasedir( new File( basedir.replace( '/', File.separatorChar ).replace( '\\', File.separatorChar ) ) );
259     }
260 
261     /**
262      * Sets the base directory to be scanned. This is the directory which is scanned recursively.
263      *
264      * @param basedir The base directory for scanning. Should not be <code>null</code>.
265      */
266     public void setBasedir( @Nonnull final File basedir )
267     {
268         this.basedir = basedir;
269     }
270 
271     /**
272      * Returns the base directory to be scanned. This is the directory which is scanned recursively.
273      *
274      * @return the base directory to be scanned
275      */
276     public File getBasedir()
277     {
278         return basedir;
279     }
280 
281     /**
282      * Sets whether or not the file system should be regarded as case sensitive.
283      *
284      * @param isCaseSensitiveParameter whether or not the file system should be regarded as a case sensitive one
285      */
286     public void setCaseSensitive( final boolean isCaseSensitiveParameter )
287     {
288         this.isCaseSensitive = isCaseSensitiveParameter;
289     }
290 
291     /**
292      * Sets whether or not symbolic links should be followed.
293      *
294      * @param followSymlinks whether or not symbolic links should be followed
295      */
296     public void setFollowSymlinks( final boolean followSymlinks )
297     {
298         this.followSymlinks = followSymlinks;
299     }
300 
301     /**
302      * Sets the list of include patterns to use. All '/' and '\' characters are replaced by
303      * <code>File.separatorChar</code>, so the separator used need not match <code>File.separatorChar</code>.
304      * <p/>
305      * When a pattern ends with a '/' or '\', "**" is appended.
306      *
307      * @param includes A list of include patterns. May be <code>null</code>, indicating that all files should be
308      *                 included. If a non-<code>null</code> list is given, all elements must be non-<code>null</code>.
309      */
310     public void setIncludes( final String... includes )
311     {
312         if ( includes == null )
313         {
314             this.includes = null;
315         }
316         else
317         {
318             this.includes = new String[includes.length];
319             for ( int i = 0; i < includes.length; i++ )
320             {
321                 String pattern;
322                 pattern = includes[i].trim().replace( '/', File.separatorChar ).replace( '\\', File.separatorChar );
323                 if ( pattern.endsWith( File.separator ) )
324                 {
325                     pattern += "**";
326                 }
327                 this.includes[i] = pattern;
328             }
329         }
330     }
331 
332     /**
333      * Sets the list of exclude patterns to use. All '/' and '\' characters are replaced by
334      * <code>File.separatorChar</code>, so the separator used need not match <code>File.separatorChar</code>.
335      * <p/>
336      * When a pattern ends with a '/' or '\', "**" is appended.
337      *
338      * @param excludes A list of exclude patterns. May be <code>null</code>, indicating that no files should be
339      *                 excluded. If a non-<code>null</code> list is given, all elements must be non-<code>null</code>.
340      */
341     public void setExcludes( final String... excludes )
342     {
343         if ( excludes == null )
344         {
345             this.excludes = null;
346         }
347         else
348         {
349             this.excludes = new String[excludes.length];
350             for ( int i = 0; i < excludes.length; i++ )
351             {
352                 String pattern;
353                 pattern = excludes[i].trim().replace( '/', File.separatorChar ).replace( '\\', File.separatorChar );
354                 if ( pattern.endsWith( File.separator ) )
355                 {
356                     pattern += "**";
357                 }
358                 this.excludes[i] = pattern;
359             }
360         }
361     }
362 
363     /**
364      * @param scanConductor {@link #scanConductor}
365      */
366     public void setScanConductor( final ScanConductor scanConductor )
367     {
368         this.scanConductor = scanConductor;
369     }
370 
371     /**
372      * Scans the base directory for files which match at least one include pattern and don't match any exclude patterns.
373      * If there are selectors then the files must pass muster there, as well.
374      *
375      * @throws IllegalStateException if the base directory was set incorrectly (i.e. if it is <code>null</code>,
376      *                               doesn't exist, or isn't a directory).
377      */
378     public void scan()
379         throws IllegalStateException
380     {
381         if ( basedir == null )
382         {
383             throw new IllegalStateException( "No basedir set" );
384         }
385         if ( !basedir.exists() )
386         {
387             throw new IllegalStateException( "basedir " + basedir + " does not exist" );
388         }
389         if ( !basedir.isDirectory() )
390         {
391             throw new IllegalStateException( "basedir " + basedir + " is not a directory" );
392         }
393 
394         setupDefaultFilters();
395         setupMatchPatterns();
396 
397         filesIncluded = new ArrayList<String>();
398         filesNotIncluded = new ArrayList<String>();
399         filesExcluded = new ArrayList<String>();
400         dirsIncluded = new ArrayList<String>();
401         dirsNotIncluded = new ArrayList<String>();
402         dirsExcluded = new ArrayList<String>();
403         scanAction = ScanConductor.ScanAction.CONTINUE;
404 
405         if ( isIncluded( "" ) )
406         {
407             if ( !isExcluded( "" ) )
408             {
409                 if ( scanConductor != null )
410                 {
411                     scanAction = scanConductor.visitDirectory( "", basedir );
412 
413                     if ( ScanConductor.ScanAction.ABORT.equals( scanAction )
414                         || ScanConductor.ScanAction.ABORT_DIRECTORY.equals( scanAction )
415                         || ScanConductor.ScanAction.NO_RECURSE.equals( scanAction ) )
416                     {
417                         return;
418                     }
419                 }
420 
421                 dirsIncluded.add( "" );
422             }
423             else
424             {
425                 dirsExcluded.add( "" );
426             }
427         }
428         else
429         {
430             dirsNotIncluded.add( "" );
431         }
432         scandir( basedir, "", true );
433     }
434 
435     /**
436      * Determine the file differences between the currently included files and
437      * a previously captured list of files.
438      * This method will not look for a changed in content but sole in the
439      * list of files given.
440      * <p/>
441      * The method will compare the given array of file Strings with the result
442      * of the last directory scan. It will execute a {@link #scan()} if no
443      * result of a previous scan could be found.
444      * <p/>
445      * The result of the diff can be queried by the methods
446      * {@link DirectoryScanResult#getFilesAdded()} and {@link DirectoryScanResult#getFilesRemoved()}
447      *
448      * @param oldFiles the list of previously captured files names.
449      * @return the result of the directory scan.
450      */
451     public DirectoryScanResult diffIncludedFiles( String... oldFiles )
452     {
453         if ( filesIncluded == null )
454         {
455             // perform a scan if the directory didn't got scanned yet
456             scan();
457         }
458 
459         return diffFiles( oldFiles, filesIncluded.toArray( new String[filesIncluded.size()] ) );
460     }
461 
462     /**
463      * @param oldFiles array of old files.
464      * @param newFiles array of new files.
465      * @return calculated differerence.
466      */
467     public static DirectoryScanResult diffFiles( @Nullable String[] oldFiles, @Nullable  String[] newFiles )
468     {
469         Set<String> oldFileSet = arrayAsHashSet( oldFiles );
470         Set<String> newFileSet = arrayAsHashSet( newFiles );
471 
472         List<String> added = new ArrayList<String>();
473         List<String> removed = new ArrayList<String>();
474 
475         for ( String oldFile : oldFileSet )
476         {
477             if ( !newFileSet.contains( oldFile ) )
478             {
479                 removed.add( oldFile );
480             }
481         }
482 
483         for ( String newFile : newFileSet )
484         {
485             if ( !oldFileSet.contains( newFile ) )
486             {
487                 added.add( newFile );
488             }
489         }
490 
491         String[] filesAdded = added.toArray( new String[added.size()] );
492         String[] filesRemoved = removed.toArray( new String[removed.size()] );
493 
494         return new DirectoryScanResult( filesAdded, filesRemoved );
495     }
496 
497 
498     /**
499      * Take an array of type T and convert it into a HashSet of type T.
500      * If <code>null</code> or an empty array gets passed, an empty Set will be returned.
501      *
502      * @param array  The array
503      * @return the filled HashSet of type T
504      */
505     private static <T> Set<T> arrayAsHashSet( @Nullable T[] array )
506     {
507         if ( array == null || array.length == 0 )
508         {
509             return Collections.emptySet();
510         }
511 
512         Set<T> set = new HashSet<T>( array.length );
513         Collections.addAll( set, array );
514 
515         return set;
516     }
517 
518     /**
519      * Top level invocation for a slow scan. A slow scan builds up a full list of excluded/included files/directories,
520      * whereas a fast scan will only have full results for included files, as it ignores directories which can't
521      * possibly hold any included files/directories.
522      * <p/>
523      * Returns immediately if a slow scan has already been completed.
524      */
525     void slowScan()
526     {
527         if ( haveSlowResults )
528         {
529             return;
530         }
531 
532         final String[] excl = dirsExcluded.toArray( new String[dirsExcluded.size()] );
533 
534         final String[] notIncl = dirsNotIncluded.toArray( new String[dirsNotIncluded.size()] );
535 
536         for ( String anExcl : excl )
537         {
538             if ( !couldHoldIncluded( anExcl ) )
539             {
540                 scandir( new File( basedir, anExcl ), anExcl + File.separator, false );
541             }
542         }
543 
544         for ( String aNotIncl : notIncl )
545         {
546             if ( !couldHoldIncluded( aNotIncl ) )
547             {
548                 scandir( new File( basedir, aNotIncl ), aNotIncl + File.separator, false );
549             }
550         }
551 
552         haveSlowResults = true;
553     }
554 
555     /**
556      * Scans the given directory for files and directories. Found files and directories are placed in their respective
557      * collections, based on the matching of includes, excludes, and the selectors. When a directory is found, it is
558      * scanned recursively.
559      *
560      * @param dir   The directory to scan. Must not be <code>null</code>.
561      * @param vpath The path relative to the base directory (needed to prevent problems with an absolute path when using
562      *              dir). Must not be <code>null</code>.
563      * @param fast  Whether or not this call is part of a fast scan.
564      * @see #filesIncluded
565      * @see #filesNotIncluded
566      * @see #filesExcluded
567      * @see #dirsIncluded
568      * @see #dirsNotIncluded
569      * @see #dirsExcluded
570      * @see #slowScan
571      */
572     void scandir( @Nonnull final File dir, @Nonnull final String vpath, final boolean fast )
573     {
574         String[] newfiles = dir.list();
575 
576         if ( newfiles == null )
577         {
578             /*
579              * two reasons are mentioned in the API docs for File.list (1) dir is not a directory. This is impossible as
580              * we wouldn't get here in this case. (2) an IO error occurred (why doesn't it throw an exception then???)
581              */
582 
583             /*
584              * [jdcasey] (2) is apparently happening to me, as this is killing one of my tests... this is affecting the
585              * assembly plugin, fwiw. I will initialize the newfiles array as zero-length for now. NOTE: I can't find
586              * the problematic code, as it appears to come from a native method in UnixFileSystem...
587              */
588             newfiles = new String[0];
589 
590             // throw new IOException( "IO error scanning directory " + dir.getAbsolutePath() );
591         }
592 
593         if ( !followSymlinks )
594         {
595             newfiles = doNotFollowSymbolicLinks( dir, vpath, newfiles );
596         }
597 
598         for ( final String newfile : newfiles )
599         {
600             final String name = vpath + newfile;
601             final File file = new File( dir, newfile );
602             if ( file.isDirectory() )
603             {
604                 if ( isIncluded( name ) )
605                 {
606                     if ( !isExcluded( name ) )
607                     {
608                         if ( scanConductor != null )
609                         {
610                             scanAction = scanConductor.visitDirectory( name, file );
611 
612                             if ( ScanConductor.ScanAction.ABORT.equals( scanAction )
613                                 || ScanConductor.ScanAction.ABORT_DIRECTORY.equals( scanAction ) )
614                             {
615                                 return;
616                             }
617                         }
618 
619                         if ( !ScanConductor.ScanAction.NO_RECURSE.equals( scanAction ) )
620                         {
621                             dirsIncluded.add( name );
622                             if ( fast )
623                             {
624                                 scandir( file, name + File.separator, fast );
625 
626                                 if ( ScanConductor.ScanAction.ABORT.equals( scanAction ) )
627                                 {
628                                     return;
629                                 }
630                             }
631                         }
632                         scanAction = null;
633 
634                     }
635                     else
636                     {
637                         dirsExcluded.add( name );
638                         if ( fast && couldHoldIncluded( name ) )
639                         {
640                             scandir( file, name + File.separator, fast );
641                             if ( ScanConductor.ScanAction.ABORT.equals( scanAction ) )
642                             {
643                                 return;
644                             }
645                             scanAction = null;
646                         }
647                     }
648                 }
649                 else
650                 {
651                     if ( fast && couldHoldIncluded( name ) )
652                     {
653                         if ( scanConductor != null )
654                         {
655                             scanAction = scanConductor.visitDirectory( name, file );
656 
657                             if ( ScanConductor.ScanAction.ABORT.equals( scanAction )
658                                 || ScanConductor.ScanAction.ABORT_DIRECTORY.equals( scanAction ) )
659                             {
660                                 return;
661                             }
662                         }
663                         if ( !ScanConductor.ScanAction.NO_RECURSE.equals( scanAction ) )
664                         {
665                             dirsNotIncluded.add( name );
666 
667                             scandir( file, name + File.separator, fast );
668                             if ( ScanConductor.ScanAction.ABORT.equals( scanAction ) )
669                             {
670                                 return;
671                             }
672                         }
673                         scanAction = null;
674                     }
675                 }
676                 if ( !fast )
677                 {
678                     scandir( file, name + File.separator, fast );
679                     if ( ScanConductor.ScanAction.ABORT.equals( scanAction ) )
680                     {
681                         return;
682                     }
683                     scanAction = null;
684                 }
685             }
686             else if ( file.isFile() )
687             {
688                 if ( isIncluded( name ) )
689                 {
690                     if ( !isExcluded( name ) )
691                     {
692                         if ( scanConductor != null )
693                         {
694                             scanAction = scanConductor.visitFile( name, file );
695                         }
696 
697                         if ( ScanConductor.ScanAction.ABORT.equals( scanAction )
698                             || ScanConductor.ScanAction.ABORT_DIRECTORY.equals( scanAction ) )
699                         {
700                             return;
701                         }
702 
703                         filesIncluded.add( name );
704                     }
705                     else
706                     {
707                         filesExcluded.add( name );
708                     }
709                 }
710                 else
711                 {
712                     filesNotIncluded.add( name );
713                 }
714             }
715         }
716     }
717 
718     private String[] doNotFollowSymbolicLinks( final File dir, final String vpath, String[] newfiles )
719     {
720         final List<String> noLinks = new ArrayList<String>();
721         for ( final String newfile : newfiles )
722         {
723             try
724             {
725                 if ( isSymbolicLink( dir, newfile ) )
726                 {
727                     final String name = vpath + newfile;
728                     final File file = new File( dir, newfile );
729                     if ( file.isDirectory() )
730                     {
731                         dirsExcluded.add( name );
732                     }
733                     else
734                     {
735                         filesExcluded.add( name );
736                     }
737                 }
738                 else
739                 {
740                     noLinks.add( newfile );
741                 }
742             }
743             catch ( final IOException ioe )
744             {
745                 final String msg =
746                     "IOException caught while checking " + "for links, couldn't get cannonical path!";
747                 // will be caught and redirected to Ant's logging system
748                 System.err.println( msg );
749                 noLinks.add( newfile );
750             }
751         }
752         newfiles = noLinks.toArray( new String[noLinks.size()] );
753         return newfiles;
754     }
755 
756     /**
757      * Tests whether or not a name matches against at least one include pattern.
758      *
759      * @param name The name to match. Must not be <code>null</code>.
760      * @return <code>true</code> when the name matches against at least one include pattern, or <code>false</code>
761      *         otherwise.
762      */
763     boolean isIncluded( final String name )
764     {
765         return includesPatterns.matches( name, isCaseSensitive );
766     }
767 
768     /**
769      * Tests whether or not a name matches the start of at least one include pattern.
770      *
771      * @param name The name to match. Must not be <code>null</code>.
772      * @return <code>true</code> when the name matches against the start of at least one include pattern, or
773      *         <code>false</code> otherwise.
774      */
775     boolean couldHoldIncluded( @Nonnull final String name )
776     {
777         return includesPatterns.matchesPatternStart( name, isCaseSensitive );
778     }
779 
780     /**
781      * Tests whether or not a name matches against at least one exclude pattern.
782      *
783      * @param name The name to match. Must not be <code>null</code>.
784      * @return <code>true</code> when the name matches against at least one exclude pattern, or <code>false</code>
785      *         otherwise.
786      */
787     boolean isExcluded( @Nonnull final String name )
788     {
789         return excludesPatterns.matches( name, isCaseSensitive );
790     }
791 
792     /**
793      * Returns the names of the files which matched at least one of the include patterns and none of the exclude
794      * patterns. The names are relative to the base directory.
795      *
796      * @return the names of the files which matched at least one of the include patterns and none of the exclude
797      *         patterns. May also contain symbolic links to files.
798      */
799     public String[] getIncludedFiles()
800     {
801         if ( filesIncluded == null )
802         {
803             return new String[0];
804         }
805         return filesIncluded.toArray( new String[filesIncluded.size()] );
806     }
807 
808     /**
809      * Returns the names of the files which matched none of the include patterns. The names are relative to the base
810      * directory. This involves performing a slow scan if one has not already been completed.
811      *
812      * @return the names of the files which matched none of the include patterns.
813      * @see #slowScan
814      */
815     public String[] getNotIncludedFiles()
816     {
817         slowScan();
818         return filesNotIncluded.toArray( new String[filesNotIncluded.size()] );
819     }
820 
821     /**
822      * Returns the names of the files which matched at least one of the include patterns and at least one of the exclude
823      * patterns. The names are relative to the base directory. This involves performing a slow scan if one has not
824      * already been completed.
825      *
826      * @return the names of the files which matched at least one of the include patterns and at at least one of the
827      *         exclude patterns.
828      * @see #slowScan
829      */
830     public String[] getExcludedFiles()
831     {
832         slowScan();
833         return filesExcluded.toArray( new String[filesExcluded.size()] );
834     }
835 
836     /**
837      * Returns the names of the directories which matched at least one of the include patterns and none of the exclude
838      * patterns. The names are relative to the base directory.
839      *
840      * @return the names of the directories which matched at least one of the include patterns and none of the exclude
841      *         patterns. May also contain symbolic links to directories.
842      */
843     public String[] getIncludedDirectories()
844     {
845         return dirsIncluded.toArray( new String[dirsIncluded.size()] );
846     }
847 
848     /**
849      * Returns the names of the directories which matched none of the include patterns. The names are relative to the
850      * base directory. This involves performing a slow scan if one has not already been completed.
851      *
852      * @return the names of the directories which matched none of the include patterns.
853      * @see #slowScan
854      */
855     public String[] getNotIncludedDirectories()
856     {
857         slowScan();
858         return dirsNotIncluded.toArray( new String[dirsNotIncluded.size()] );
859     }
860 
861     /**
862      * Returns the names of the directories which matched at least one of the include patterns and at least one of the
863      * exclude patterns. The names are relative to the base directory. This involves performing a slow scan if one has
864      * not already been completed.
865      *
866      * @return the names of the directories which matched at least one of the include patterns and at least one of the
867      *         exclude patterns.
868      * @see #slowScan
869      */
870     public String[] getExcludedDirectories()
871     {
872         slowScan();
873         return dirsExcluded.toArray( new String[dirsExcluded.size()] );
874     }
875 
876     /**
877      * Adds default exclusions to the current exclusions set.
878      */
879     public void addDefaultExcludes()
880     {
881         final int excludesLength = excludes == null ? 0 : excludes.length;
882         String[] newExcludes;
883         newExcludes = new String[excludesLength + DEFAULTEXCLUDES.length];
884         if ( excludesLength > 0 )
885         {
886             System.arraycopy( excludes, 0, newExcludes, 0, excludesLength );
887         }
888         for ( int i = 0; i < DEFAULTEXCLUDES.length; i++ )
889         {
890             newExcludes[i + excludesLength] =
891                 DEFAULTEXCLUDES[i].replace( '/', File.separatorChar ).replace( '\\', File.separatorChar );
892         }
893         excludes = newExcludes;
894     }
895 
896     /**
897      * Checks whether a given file is a symbolic link.
898      * <p>
899      * It doesn't really test for symbolic links but whether the canonical and absolute paths of the file are identical
900      * - this may lead to false positives on some platforms.
901      * </p>
902      *
903      * @param parent the parent directory of the file to test
904      * @param name   the name of the file to test.
905      * 
906      */
907     boolean isSymbolicLink( final File parent, final String name )
908         throws IOException
909     {
910         if ( Java7Support.isAtLeastJava7() )
911         {
912             return Java7Support.isSymLink( parent );
913         }
914         final File resolvedParent = new File( parent.getCanonicalPath() );
915         final File toTest = new File( resolvedParent, name );
916         return !toTest.getAbsolutePath().equals( toTest.getCanonicalPath() );
917     }
918 
919     private void setupDefaultFilters()
920     {
921         if ( includes == null )
922         {
923             // No includes supplied, so set it to 'matches all'
924             includes = new String[1];
925             includes[0] = "**";
926         }
927         if ( excludes == null )
928         {
929             excludes = new String[0];
930         }
931     }
932 
933 
934     private void setupMatchPatterns()
935     {
936         includesPatterns = MatchPatterns.from( includes );
937         excludesPatterns = MatchPatterns.from( excludes );
938     }
939 
940 }