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