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