View Javadoc
1   package org.apache.maven.shared.model.fileset.util;
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.Arrays;
26  import java.util.Collections;
27  import java.util.HashSet;
28  import java.util.Iterator;
29  import java.util.LinkedHashMap;
30  import java.util.LinkedList;
31  import java.util.List;
32  import java.util.Map;
33  import java.util.Set;
34  
35  import org.apache.maven.plugin.logging.Log;
36  import org.apache.maven.shared.io.logging.DefaultMessageHolder;
37  import org.apache.maven.shared.io.logging.MessageHolder;
38  import org.apache.maven.shared.io.logging.MessageLevels;
39  import org.apache.maven.shared.io.logging.MojoLogSink;
40  import org.apache.maven.shared.io.logging.PlexusLoggerSink;
41  import org.apache.maven.shared.model.fileset.FileSet;
42  import org.apache.maven.shared.model.fileset.mappers.FileNameMapper;
43  import org.apache.maven.shared.model.fileset.mappers.MapperException;
44  import org.apache.maven.shared.model.fileset.mappers.MapperUtil;
45  import org.apache.maven.shared.utils.io.DirectoryScanner;
46  import org.apache.maven.shared.utils.io.FileUtils;
47  import org.codehaus.plexus.logging.Logger;
48  
49  /**
50   * Provides operations for use with FileSet instances, such as retrieving the included/excluded files, deleting all
51   * matching entries, etc.
52   *
53   * @author jdcasey
54   * @version $Id: FileSetManager.html 976363 2015-12-25 13:37:48Z khmarbaise $
55   */
56  public class FileSetManager
57  {
58      private static final String[] EMPTY_STRING_ARRAY = new String[0];
59  
60      private final boolean verbose;
61  
62      private MessageHolder messages;
63  
64      // ----------------------------------------------------------------------
65      // Constructors
66      // ----------------------------------------------------------------------
67  
68      /**
69       * Create a new manager instance with the supplied log instance and flag for whether to output verbose messages.
70       *
71       * @param log The mojo log instance
72       * @param verbose Whether to output verbose messages
73       */
74      public FileSetManager( Log log, boolean verbose )
75      {
76          if ( verbose )
77          {
78              this.messages =
79                  new DefaultMessageHolder( MessageLevels.LEVEL_DEBUG, MessageLevels.LEVEL_INFO, new MojoLogSink( log ) );
80          }
81          else
82          {
83              this.messages =
84                  new DefaultMessageHolder( MessageLevels.LEVEL_INFO, MessageLevels.LEVEL_INFO, new MojoLogSink( log ) );
85          }
86  
87          this.verbose = verbose;
88      }
89  
90      /**
91       * Create a new manager instance with the supplied log instance. Verbose flag is set to false.
92       *
93       * @param log The mojo log instance
94       */
95      public FileSetManager( Log log )
96      {
97          this.messages =
98              new DefaultMessageHolder( MessageLevels.LEVEL_INFO, MessageLevels.LEVEL_INFO, new MojoLogSink( log ) );
99          this.verbose = false;
100     }
101 
102     /**
103      * Create a new manager instance with the supplied log instance and flag for whether to output verbose messages.
104      *
105      * @param log The mojo log instance
106      * @param verbose Whether to output verbose messages
107      */
108     public FileSetManager( Logger log, boolean verbose )
109     {
110         if ( verbose )
111         {
112             this.messages = new DefaultMessageHolder( MessageLevels.LEVEL_DEBUG, MessageLevels.LEVEL_INFO,
113                                                       new PlexusLoggerSink( log ) );
114         }
115         else
116         {
117             this.messages = new DefaultMessageHolder( MessageLevels.LEVEL_INFO, MessageLevels.LEVEL_INFO,
118                                                       new PlexusLoggerSink( log ) );
119         }
120 
121         this.verbose = verbose;
122     }
123 
124     /**
125      * Create a new manager instance with the supplied log instance. Verbose flag is set to false.
126      *
127      * @param log The mojo log instance
128      */
129     public FileSetManager( Logger log )
130     {
131         this.messages =
132             new DefaultMessageHolder( MessageLevels.LEVEL_INFO, MessageLevels.LEVEL_INFO, new PlexusLoggerSink( log ) );
133         this.verbose = false;
134     }
135 
136     /**
137      * Create a new manager instance with an empty messages. Verbose flag is set to false.
138      */
139     public FileSetManager()
140     {
141         this.verbose = false;
142     }
143 
144     // ----------------------------------------------------------------------
145     // Public methods
146     // ----------------------------------------------------------------------
147 
148     /**
149      * @param fileSet {@link FileSet}
150      * @return the included files as map
151      * @throws MapperException if any
152      * @see #getIncludedFiles(FileSet)
153      */
154     public Map<String, String> mapIncludedFiles( FileSet fileSet )
155         throws MapperException
156     {
157         String[] sourcePaths = getIncludedFiles( fileSet );
158         Map<String, String> mappedPaths = new LinkedHashMap<String, String>();
159 
160         FileNameMapper fileMapper = MapperUtil.getFileNameMapper( fileSet.getMapper() );
161 
162         for ( int i = 0; i < sourcePaths.length; i++ )
163         {
164             String sourcePath = sourcePaths[i];
165 
166             String destPath;
167             if ( fileMapper != null )
168             {
169                 destPath = fileMapper.mapFileName( sourcePath );
170             }
171             else
172             {
173                 destPath = sourcePath;
174             }
175 
176             mappedPaths.put( sourcePath, destPath );
177         }
178 
179         return mappedPaths;
180     }
181 
182     /**
183      * Get all the filenames which have been included by the rules in this fileset.
184      *
185      * @param fileSet The fileset defining rules for inclusion/exclusion, and base directory.
186      * @return the array of matching filenames, relative to the basedir of the file-set.
187      */
188     public String[] getIncludedFiles( FileSet fileSet )
189     {
190         DirectoryScanner scanner = scan( fileSet );
191 
192         if ( scanner != null )
193         {
194             return scanner.getIncludedFiles();
195         }
196 
197         return EMPTY_STRING_ARRAY;
198     }
199 
200     /**
201      * Get all the directory names which have been included by the rules in this fileset.
202      *
203      * @param fileSet The fileset defining rules for inclusion/exclusion, and base directory.
204      * @return the array of matching dirnames, relative to the basedir of the file-set.
205      */
206     public String[] getIncludedDirectories( FileSet fileSet )
207     {
208         DirectoryScanner scanner = scan( fileSet );
209 
210         if ( scanner != null )
211         {
212             return scanner.getIncludedDirectories();
213         }
214 
215         return EMPTY_STRING_ARRAY;
216     }
217 
218     /**
219      * Get all the filenames which have been excluded by the rules in this fileset.
220      *
221      * @param fileSet The fileset defining rules for inclusion/exclusion, and base directory.
222      * @return the array of non-matching filenames, relative to the basedir of the file-set.
223      */
224     public String[] getExcludedFiles( FileSet fileSet )
225     {
226         DirectoryScanner scanner = scan( fileSet );
227 
228         if ( scanner != null )
229         {
230             return scanner.getExcludedFiles();
231         }
232 
233         return EMPTY_STRING_ARRAY;
234     }
235 
236     /**
237      * Get all the directory names which have been excluded by the rules in this fileset.
238      *
239      * @param fileSet The fileset defining rules for inclusion/exclusion, and base directory.
240      * @return the array of non-matching dirnames, relative to the basedir of the file-set.
241      */
242     public String[] getExcludedDirectories( FileSet fileSet )
243     {
244         DirectoryScanner scanner = scan( fileSet );
245 
246         if ( scanner != null )
247         {
248             return scanner.getExcludedDirectories();
249         }
250 
251         return EMPTY_STRING_ARRAY;
252     }
253 
254     /**
255      * Delete the matching files and directories for the given file-set definition.
256      *
257      * @param fileSet The file-set matching rules, along with search base directory
258      * @throws IOException If a matching file cannot be deleted
259      */
260     public void delete( FileSet fileSet )
261         throws IOException
262     {
263         delete( fileSet, true );
264     }
265 
266     /**
267      * Delete the matching files and directories for the given file-set definition.
268      *
269      * @param fileSet The file-set matching rules, along with search base directory.
270      * @param throwsError Throw IOException when errors have occurred by deleting files or directories.
271      * @throws IOException If a matching file cannot be deleted and <code>throwsError=true</code>, otherwise print
272      *             warning messages.
273      */
274     public void delete( FileSet fileSet, boolean throwsError )
275         throws IOException
276     {
277         Set<String> deletablePaths = findDeletablePaths( fileSet );
278 
279         if ( messages != null && messages.isDebugEnabled() )
280         {
281             String paths = String.valueOf( deletablePaths ).replace( ',', '\n' );
282             messages.addDebugMessage( "Found deletable paths: " + paths ).flush();
283         }
284 
285         List<String> warnMessages = new LinkedList<String>();
286 
287         for ( Iterator<String> it = deletablePaths.iterator(); it.hasNext(); )
288         {
289             String path = it.next();
290 
291             File file = new File( fileSet.getDirectory(), path );
292 
293             if ( file.exists() )
294             {
295                 if ( file.isDirectory() )
296                 {
297                     if ( fileSet.isFollowSymlinks() || !isSymlink( file ) )
298                     {
299                         if ( verbose && messages != null )
300                         {
301                             messages.addInfoMessage( "Deleting directory: " + file ).flush();
302                         }
303 
304                         removeDir( file, fileSet.isFollowSymlinks(), throwsError, warnMessages );
305                     }
306                     else
307                     { // delete a symlink to a directory without follow
308                         if ( verbose && messages != null )
309                         {
310                             messages.addInfoMessage( "Deleting symlink to directory: " + file ).flush();
311                         }
312 
313                         if ( !file.delete() )
314                         {
315                             String message = "Unable to delete symlink " + file.getAbsolutePath();
316                             if ( throwsError )
317                             {
318                                 throw new IOException( message );
319                             }
320 
321                             if ( !warnMessages.contains( message ) )
322                             {
323                                 warnMessages.add( message );
324                             }
325                         }
326                     }
327                 }
328                 else
329                 {
330                     if ( verbose && messages != null )
331                     {
332                         messages.addInfoMessage( "Deleting file: " + file ).flush();
333                     }
334 
335                     if ( !delete( file ) )
336                     {
337                         String message = "Failed to delete file " + file.getAbsolutePath() + ". Reason is unknown.";
338                         if ( throwsError )
339                         {
340                             throw new IOException( message );
341                         }
342 
343                         warnMessages.add( message );
344                     }
345                 }
346             }
347         }
348 
349         if ( messages != null && messages.isWarningEnabled() && !throwsError && ( warnMessages.size() > 0 ) )
350         {
351             for ( Iterator<String> it = warnMessages.iterator(); it.hasNext(); )
352             {
353                 messages.addWarningMessage( it.next() ).flush();
354             }
355         }
356     }
357 
358     // ----------------------------------------------------------------------
359     // Private methods
360     // ----------------------------------------------------------------------
361 
362     private boolean isSymlink( File file )
363         throws IOException
364     {
365         File fileInCanonicalParent = null;
366         File parentDir = file.getParentFile();
367         if ( parentDir == null )
368         {
369             fileInCanonicalParent = file;
370         }
371         else
372         {
373             fileInCanonicalParent = new File( parentDir.getCanonicalPath(), file.getName() );
374         }
375         if ( messages != null && messages.isDebugEnabled() )
376         {
377             messages.addDebugMessage( "Checking for symlink:\nFile's canonical path: "
378                 + fileInCanonicalParent.getCanonicalPath() + "\nFile's absolute path with canonical parent: "
379                 + fileInCanonicalParent.getPath() ).flush();
380         }
381         return !fileInCanonicalParent.getCanonicalFile().equals( fileInCanonicalParent.getAbsoluteFile() );
382     }
383 
384     private Set<String> findDeletablePaths( FileSet fileSet )
385     {
386         Set<String> includes = findDeletableDirectories( fileSet );
387         includes.addAll( findDeletableFiles( fileSet, includes ) );
388 
389         return includes;
390     }
391 
392     private Set<String> findDeletableDirectories( FileSet fileSet )
393     {
394         if ( verbose && messages != null )
395         {
396             messages.addInfoMessage( "Scanning for deletable directories." ).flush();
397         }
398 
399         DirectoryScanner scanner = scan( fileSet );
400 
401         if ( scanner == null )
402         {
403             return Collections.<String>emptySet();
404         }
405 
406         Set<String> includes = new HashSet<String>( Arrays.asList( scanner.getIncludedDirectories() ) );
407         List<String> excludes = new ArrayList<String>( Arrays.asList( scanner.getExcludedDirectories() ) );
408         List<String> linksForDeletion = new ArrayList<String>();
409 
410         if ( !fileSet.isFollowSymlinks() )
411         {
412             if ( verbose && messages != null )
413             {
414                 messages.addInfoMessage( "Adding symbolic link dirs which were previously excluded"
415                     + " to the list being deleted." ).flush();
416             }
417 
418             // we need to see which entries were only excluded because they're symlinks...
419             scanner.setFollowSymlinks( true );
420             scanner.scan();
421 
422             if ( messages != null && messages.isDebugEnabled() )
423             {
424                 messages.addDebugMessage( "Originally marked for delete: " + includes ).flush();
425                 messages.addDebugMessage( "Marked for preserve (with followSymlinks == false): " + excludes ).flush();
426             }
427 
428             List<String> includedDirsAndSymlinks = Arrays.asList( scanner.getIncludedDirectories() );
429 
430             linksForDeletion.addAll( excludes );
431             linksForDeletion.retainAll( includedDirsAndSymlinks );
432 
433             if ( messages != null && messages.isDebugEnabled() )
434             {
435                 messages.addDebugMessage( "Symlinks marked for deletion (originally mismarked): "
436                     + linksForDeletion ).flush();
437             }
438 
439             excludes.removeAll( includedDirsAndSymlinks );
440         }
441 
442         excludeParentDirectoriesOfExcludedPaths( excludes, includes );
443 
444         includes.addAll( linksForDeletion );
445 
446         return includes;
447     }
448 
449     private Set<String> findDeletableFiles( FileSet fileSet, Set<String> deletableDirectories )
450     {
451         if ( verbose && messages != null )
452         {
453             messages.addInfoMessage( "Re-scanning for deletable files." ).flush();
454         }
455 
456         DirectoryScanner scanner = scan( fileSet );
457 
458         if ( scanner == null )
459         {
460             return deletableDirectories;
461         }
462 
463         Set<String> includes = deletableDirectories;
464         includes.addAll( Arrays.asList( scanner.getIncludedFiles() ) );
465         List<String> excludes = new ArrayList<String>( Arrays.asList( scanner.getExcludedFiles() ) );
466         List<String> linksForDeletion = new ArrayList<String>();
467 
468         if ( !fileSet.isFollowSymlinks() )
469         {
470             if ( verbose && messages != null )
471             {
472                 messages.addInfoMessage( "Adding symbolic link files which were previously excluded "
473                     + "to the list being deleted." ).flush();
474             }
475 
476             // we need to see which entries were only excluded because they're symlinks...
477             scanner.setFollowSymlinks( true );
478             scanner.scan();
479 
480             if ( messages != null && messages.isDebugEnabled() )
481             {
482                 messages.addDebugMessage( "Originally marked for delete: " + includes ).flush();
483                 messages.addDebugMessage( "Marked for preserve (with followSymlinks == false): " + excludes ).flush();
484             }
485 
486             List<String> includedFilesAndSymlinks = Arrays.asList( scanner.getIncludedFiles() );
487 
488             linksForDeletion.addAll( excludes );
489             linksForDeletion.retainAll( includedFilesAndSymlinks );
490 
491             if ( messages != null && messages.isDebugEnabled() )
492             {
493                 messages.addDebugMessage( "Symlinks marked for deletion (originally mismarked): "
494                     + linksForDeletion ).flush();
495             }
496 
497             excludes.removeAll( includedFilesAndSymlinks );
498         }
499 
500         excludeParentDirectoriesOfExcludedPaths( excludes, includes );
501 
502         includes.addAll( linksForDeletion );
503 
504         return includes;
505     }
506 
507     /**
508      * Removes all parent directories of the already excluded files/directories from the given set of deletable
509      * directories. I.e. if "subdir/excluded.txt" should not be deleted, "subdir" should be excluded from deletion, too.
510      * 
511      * @param excludedPaths The relative paths of the files/directories which are excluded from deletion, must not be
512      *            <code>null</code>.
513      * @param deletablePaths The relative paths to files/directories which are scheduled for deletion, must not be
514      *            <code>null</code>.
515      */
516     private void excludeParentDirectoriesOfExcludedPaths( List<String> excludedPaths, Set<String> deletablePaths )
517     {
518         for ( Iterator<String> it = excludedPaths.iterator(); it.hasNext(); )
519         {
520             String path = it.next();
521 
522             String parentPath = new File( path ).getParent();
523 
524             while ( parentPath != null )
525             {
526                 if ( messages != null && messages.isDebugEnabled() )
527                 {
528                     messages.addDebugMessage( "Verifying path " + parentPath
529                         + " is not present; contains file which is excluded." ).flush();
530                 }
531 
532                 boolean removed = deletablePaths.remove( parentPath );
533 
534                 if ( removed && messages != null && messages.isDebugEnabled() )
535                 {
536                     messages.addDebugMessage( "Path " + parentPath + " was removed from delete list." ).flush();
537                 }
538 
539                 parentPath = new File( parentPath ).getParent();
540             }
541         }
542 
543         if ( !excludedPaths.isEmpty() )
544         {
545             if ( messages != null && messages.isDebugEnabled() )
546             {
547                 messages.addDebugMessage( "Verifying path " + "."
548                     + " is not present; contains file which is excluded." ).flush();
549             }
550 
551             boolean removed = deletablePaths.remove( "" );
552 
553             if ( removed && messages != null && messages.isDebugEnabled() )
554             {
555                 messages.addDebugMessage( "Path " + "." + " was removed from delete list." ).flush();
556             }
557         }
558     }
559 
560     /**
561      * Delete a directory
562      *
563      * @param dir the directory to delete
564      * @param followSymlinks whether to follow symbolic links, or simply delete the link
565      * @param throwsError Throw IOException when errors have occurred by deleting files or directories.
566      * @param warnMessages A list of warning messages used when <code>throwsError=false</code>.
567      * @throws IOException If a matching file cannot be deleted and <code>throwsError=true</code>.
568      */
569     private void removeDir( File dir, boolean followSymlinks, boolean throwsError, List<String> warnMessages )
570         throws IOException
571     {
572         String[] list = dir.list();
573         if ( list == null )
574         {
575             list = new String[0];
576         }
577 
578         for ( int i = 0; i < list.length; i++ )
579         {
580             String s = list[i];
581             File f = new File( dir, s );
582             if ( f.isDirectory() && ( followSymlinks || !isSymlink( f ) ) )
583             {
584                 removeDir( f, followSymlinks, throwsError, warnMessages );
585             }
586             else
587             {
588                 if ( !delete( f ) )
589                 {
590                     String message = "Unable to delete file " + f.getAbsolutePath();
591                     if ( throwsError )
592                     {
593                         throw new IOException( message );
594                     }
595 
596                     if ( !warnMessages.contains( message ) )
597                     {
598                         warnMessages.add( message );
599                     }
600                 }
601             }
602         }
603 
604         if ( !delete( dir ) )
605         {
606             String message = "Unable to delete directory " + dir.getAbsolutePath();
607             if ( throwsError )
608             {
609                 throw new IOException( message );
610             }
611 
612             if ( !warnMessages.contains( message ) )
613             {
614                 warnMessages.add( message );
615             }
616         }
617     }
618 
619     /**
620      * Delete a file
621      *
622      * @param f a file
623      */
624     private boolean delete( File f )
625     {
626         try
627         {
628             FileUtils.forceDelete( f );
629         }
630         catch ( IOException e )
631         {
632             return false;
633         }
634 
635         return true;
636     }
637 
638     private DirectoryScanner scan( FileSet fileSet )
639     {
640         File basedir = new File( fileSet.getDirectory() );
641         if ( !basedir.exists() || !basedir.isDirectory() )
642         {
643             return null;
644         }
645 
646         DirectoryScanner scanner = new DirectoryScanner();
647 
648         String[] includesArray = fileSet.getIncludesArray();
649         String[] excludesArray = fileSet.getExcludesArray();
650 
651         if ( includesArray.length > 0 )
652         {
653             scanner.setIncludes( includesArray );
654         }
655 
656         if ( excludesArray.length > 0 )
657         {
658             scanner.setExcludes( excludesArray );
659         }
660 
661         if ( fileSet.isUseDefaultExcludes() )
662         {
663             scanner.addDefaultExcludes();
664         }
665 
666         scanner.setBasedir( basedir );
667         scanner.setFollowSymlinks( fileSet.isFollowSymlinks() );
668 
669         scanner.scan();
670 
671         return scanner;
672     }
673 
674 }