View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.shared.model.fileset.util;
20  
21  import java.io.File;
22  import java.io.IOException;
23  import java.nio.file.Files;
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      private static final String[] EMPTY_STRING_ARRAY = new String[0];
53  
54      private final boolean verbose;
55  
56      private final Logger logger;
57  
58      // ----------------------------------------------------------------------
59      // Constructors
60      // ----------------------------------------------------------------------
61  
62      /**
63       * Create a new manager instance with the supplied log instance and flag for whether to output verbose messages.
64       *
65       * @param logger the logger instance
66       * @param verbose whether to output verbose messages
67       */
68      public FileSetManager(Logger logger, boolean verbose) {
69          this.logger = requireNonNull(logger);
70          this.verbose = verbose;
71      }
72  
73      /**
74       * Create a new manager instance with the supplied log instance. Verbose flag is set to false.
75       *
76       * @param logger The log instance
77       */
78      public FileSetManager(Logger logger) {
79          this(logger, false);
80      }
81  
82      /**
83       * Create a new manager instance with an own logger. Verbose flag is set to false.
84       */
85      public FileSetManager() {
86          this(LoggerFactory.getLogger(FileSetManager.class), false);
87      }
88  
89      // ----------------------------------------------------------------------
90      // Public methods
91      // ----------------------------------------------------------------------
92  
93      /**
94       * @param fileSet {@link FileSet}
95       * @return the included files as map
96       * @throws MapperException if any
97       * @see #getIncludedFiles(FileSet)
98       */
99      public Map<String, String> mapIncludedFiles(FileSet fileSet) throws MapperException {
100         String[] sourcePaths = getIncludedFiles(fileSet);
101         Map<String, String> mappedPaths = new LinkedHashMap<>();
102 
103         FileNameMapper fileMapper = MapperUtil.getFileNameMapper(fileSet.getMapper());
104 
105         for (String sourcePath : sourcePaths) {
106             String destPath;
107             if (fileMapper != null) {
108                 destPath = fileMapper.mapFileName(sourcePath);
109             } else {
110                 destPath = sourcePath;
111             }
112 
113             mappedPaths.put(sourcePath, destPath);
114         }
115 
116         return mappedPaths;
117     }
118 
119     /**
120      * Get all the filenames which have been included by the rules in this fileset.
121      *
122      * @param fileSet The fileset defining rules for inclusion/exclusion, and base directory.
123      * @return the array of matching filenames, relative to the basedir of the file-set.
124      */
125     public String[] getIncludedFiles(FileSet fileSet) {
126         DirectoryScanner scanner = scan(fileSet);
127 
128         if (scanner != null) {
129             return scanner.getIncludedFiles();
130         }
131 
132         return EMPTY_STRING_ARRAY;
133     }
134 
135     /**
136      * Get all the directory names which have been included by the rules in this fileset.
137      *
138      * @param fileSet The fileset defining rules for inclusion/exclusion, and base directory.
139      * @return the array of matching dirnames, relative to the basedir of the file-set.
140      */
141     public String[] getIncludedDirectories(FileSet fileSet) {
142         DirectoryScanner scanner = scan(fileSet);
143 
144         if (scanner != null) {
145             return scanner.getIncludedDirectories();
146         }
147 
148         return EMPTY_STRING_ARRAY;
149     }
150 
151     /**
152      * Get all the filenames which have been excluded by the rules in this fileset.
153      *
154      * @param fileSet The fileset defining rules for inclusion/exclusion, and base directory.
155      * @return the array of non-matching filenames, relative to the basedir of the file-set.
156      */
157     public String[] getExcludedFiles(FileSet fileSet) {
158         DirectoryScanner scanner = scan(fileSet);
159 
160         if (scanner != null) {
161             return scanner.getExcludedFiles();
162         }
163 
164         return EMPTY_STRING_ARRAY;
165     }
166 
167     /**
168      * Get all the directory names which have been excluded by the rules in this fileset.
169      *
170      * @param fileSet The fileset defining rules for inclusion/exclusion, and base directory.
171      * @return the array of non-matching dirnames, relative to the basedir of the file-set.
172      */
173     public String[] getExcludedDirectories(FileSet fileSet) {
174         DirectoryScanner scanner = scan(fileSet);
175 
176         if (scanner != null) {
177             return scanner.getExcludedDirectories();
178         }
179 
180         return EMPTY_STRING_ARRAY;
181     }
182 
183     /**
184      * Delete the matching files and directories for the given file-set definition.
185      *
186      * @param fileSet The file-set matching rules, along with search base directory
187      * @throws IOException If a matching file cannot be deleted
188      */
189     public void delete(FileSet fileSet) throws IOException {
190         delete(fileSet, true);
191     }
192 
193     /**
194      * Delete the matching files and directories for the given file-set definition.
195      *
196      * @param fileSet the file-set matching rules, along with search base directory
197      * @param throwsError throw IOException when errors have occurred by deleting files or directories
198      * @throws IOException if a matching file cannot be deleted and <code>throwsError=true</code>, otherwise print
199      *             warning messages
200      */
201     public void delete(FileSet fileSet, boolean throwsError) throws IOException {
202         Set<String> deletablePaths = findDeletablePaths(fileSet);
203 
204         if (logger.isDebugEnabled()) {
205             String paths = String.valueOf(deletablePaths).replace(',', '\n');
206             logger.debug("Found deletable paths: " + paths);
207         }
208 
209         List<String> warnMessages = new LinkedList<>();
210 
211         for (String path : deletablePaths) {
212             File file = new File(fileSet.getDirectory(), path);
213 
214             if (file.exists()) {
215                 if (file.isDirectory()) {
216                     if (fileSet.isFollowSymlinks() || !Files.isSymbolicLink(file.toPath())) {
217                         if (verbose) {
218                             logger.info("Deleting directory: " + file);
219                         }
220 
221                         removeDir(file, fileSet.isFollowSymlinks(), throwsError, warnMessages);
222                     } else { // delete a symlink to a directory without follow
223                         if (verbose) {
224                             logger.info("Deleting symlink to directory: " + file);
225                         }
226 
227                         if (!file.delete()) {
228                             String message = "Unable to delete symlink " + file.getAbsolutePath();
229                             if (throwsError) {
230                                 throw new IOException(message);
231                             }
232 
233                             if (!warnMessages.contains(message)) {
234                                 warnMessages.add(message);
235                             }
236                         }
237                     }
238                 } else {
239                     if (verbose) {
240                         logger.info("Deleting file: " + file);
241                     }
242 
243                     if (!FileUtils.deleteQuietly(file)) {
244                         String message = "Failed to delete file " + file.getAbsolutePath() + ". Reason is unknown.";
245                         if (throwsError) {
246                             throw new IOException(message);
247                         }
248 
249                         warnMessages.add(message);
250                     }
251                 }
252             }
253         }
254 
255         if (logger.isWarnEnabled() && !throwsError && (warnMessages.size() > 0)) {
256             for (String warnMessage : warnMessages) {
257                 logger.warn(warnMessage);
258             }
259         }
260     }
261 
262     // ----------------------------------------------------------------------
263     // Private methods
264     // ----------------------------------------------------------------------
265 
266     private Set<String> findDeletablePaths(FileSet fileSet) {
267         Set<String> includes = findDeletableDirectories(fileSet);
268         includes.addAll(findDeletableFiles(fileSet, includes));
269 
270         return includes;
271     }
272 
273     private Set<String> findDeletableDirectories(FileSet fileSet) {
274         if (verbose) {
275             logger.info("Scanning for deletable directories.");
276         }
277 
278         DirectoryScanner scanner = scan(fileSet);
279 
280         if (scanner == null) {
281             return Collections.emptySet();
282         }
283 
284         Set<String> includes = new HashSet<>(Arrays.asList(scanner.getIncludedDirectories()));
285         List<String> excludes = new ArrayList<>(Arrays.asList(scanner.getExcludedDirectories()));
286         List<String> linksForDeletion = new ArrayList<>();
287 
288         if (!fileSet.isFollowSymlinks()) {
289             if (verbose) {
290                 logger.info("Adding symbolic link dirs which were previously excluded" + " to the list being deleted.");
291             }
292 
293             // we need to see which entries were only excluded because they're symlinks...
294             scanner.setFollowSymlinks(true);
295             scanner.scan();
296 
297             if (logger.isDebugEnabled()) {
298                 logger.debug("Originally marked for delete: " + includes);
299                 logger.debug("Marked for preserve (with followSymlinks == false): " + excludes);
300             }
301 
302             List<String> includedDirsAndSymlinks = Arrays.asList(scanner.getIncludedDirectories());
303 
304             linksForDeletion.addAll(excludes);
305             linksForDeletion.retainAll(includedDirsAndSymlinks);
306 
307             if (logger.isDebugEnabled()) {
308                 logger.debug("Symlinks marked for deletion (originally mismarked): " + linksForDeletion);
309             }
310 
311             excludes.removeAll(includedDirsAndSymlinks);
312         }
313 
314         excludeParentDirectoriesOfExcludedPaths(excludes, includes);
315 
316         includes.addAll(linksForDeletion);
317 
318         return includes;
319     }
320 
321     private Set<String> findDeletableFiles(FileSet fileSet, Set<String> deletableDirectories) {
322         if (verbose) {
323             logger.info("Re-scanning for deletable files.");
324         }
325 
326         DirectoryScanner scanner = scan(fileSet);
327 
328         if (scanner == null) {
329             return deletableDirectories;
330         }
331 
332         deletableDirectories.addAll(Arrays.asList(scanner.getIncludedFiles()));
333         List<String> excludes = new ArrayList<>(Arrays.asList(scanner.getExcludedFiles()));
334         List<String> linksForDeletion = new ArrayList<>();
335 
336         if (!fileSet.isFollowSymlinks()) {
337             if (verbose) {
338                 logger.info(
339                         "Adding symbolic link files which were previously excluded " + "to the list being deleted.");
340             }
341 
342             // we need to see which entries were only excluded because they're symlinks...
343             scanner.setFollowSymlinks(true);
344             scanner.scan();
345 
346             if (logger.isDebugEnabled()) {
347                 logger.debug("Originally marked for delete: " + deletableDirectories);
348                 logger.debug("Marked for preserve (with followSymlinks == false): " + excludes);
349             }
350 
351             List<String> includedFilesAndSymlinks = Arrays.asList(scanner.getIncludedFiles());
352 
353             linksForDeletion.addAll(excludes);
354             linksForDeletion.retainAll(includedFilesAndSymlinks);
355 
356             if (logger.isDebugEnabled()) {
357                 logger.debug("Symlinks marked for deletion (originally mismarked): " + linksForDeletion);
358             }
359 
360             excludes.removeAll(includedFilesAndSymlinks);
361         }
362 
363         excludeParentDirectoriesOfExcludedPaths(excludes, deletableDirectories);
364 
365         deletableDirectories.addAll(linksForDeletion);
366 
367         return deletableDirectories;
368     }
369 
370     /**
371      * Removes all parent directories of the already excluded files/directories from the given set of deletable
372      * directories. I.e. if "subdir/excluded.txt" should not be deleted, "subdir" should be excluded from deletion, too.
373      *
374      * @param excludedPaths The relative paths of the files/directories which are excluded from deletion, must not be
375      *            <code>null</code>.
376      * @param deletablePaths The relative paths to files/directories which are scheduled for deletion, must not be
377      *            <code>null</code>.
378      */
379     private void excludeParentDirectoriesOfExcludedPaths(List<String> excludedPaths, Set<String> deletablePaths) {
380         for (String path : excludedPaths) {
381             String parentPath = new File(path).getParent();
382 
383             while (parentPath != null) {
384                 if (logger.isDebugEnabled()) {
385                     logger.debug("Verifying path " + parentPath + " is not present; contains file which is excluded.");
386                 }
387 
388                 boolean removed = deletablePaths.remove(parentPath);
389 
390                 if (removed && logger.isDebugEnabled()) {
391                     logger.debug("Path " + parentPath + " was removed from delete list.");
392                 }
393 
394                 parentPath = new File(parentPath).getParent();
395             }
396         }
397 
398         if (!excludedPaths.isEmpty()) {
399             if (logger.isDebugEnabled()) {
400                 logger.debug("Verifying path " + "." + " is not present; contains file which is excluded.");
401             }
402 
403             boolean removed = deletablePaths.remove("");
404 
405             if (removed && logger.isDebugEnabled()) {
406                 logger.debug("Path " + "." + " was removed from delete list.");
407             }
408         }
409     }
410 
411     /**
412      * Delete a directory
413      *
414      * @param dir the directory to delete
415      * @param followSymlinks whether to follow symbolic links, or simply delete the link
416      * @param throwsError Throw IOException when errors have occurred by deleting files or directories.
417      * @param warnMessages A list of warning messages used when <code>throwsError=false</code>.
418      * @throws IOException If a matching file cannot be deleted and <code>throwsError=true</code>.
419      */
420     private void removeDir(File dir, boolean followSymlinks, boolean throwsError, List<String> warnMessages)
421             throws IOException {
422         String[] list = dir.list();
423         if (list == null) {
424             list = new String[0];
425         }
426 
427         for (String s : list) {
428             File f = new File(dir, s);
429             if (f.isDirectory() && (followSymlinks || !Files.isSymbolicLink(f.toPath()))) {
430                 removeDir(f, followSymlinks, throwsError, warnMessages);
431             } else {
432                 if (!FileUtils.deleteQuietly(f)) {
433                     String message = "Unable to delete file " + f.getAbsolutePath();
434                     if (throwsError) {
435                         throw new IOException(message);
436                     }
437 
438                     if (!warnMessages.contains(message)) {
439                         warnMessages.add(message);
440                     }
441                 }
442             }
443         }
444 
445         if (!FileUtils.deleteQuietly(dir)) {
446             String message = "Unable to delete directory " + dir.getAbsolutePath();
447             if (throwsError) {
448                 throw new IOException(message);
449             }
450 
451             if (!warnMessages.contains(message)) {
452                 warnMessages.add(message);
453             }
454         }
455     }
456 
457     private DirectoryScanner scan(FileSet fileSet) {
458         File basedir = new File(fileSet.getDirectory());
459         if (!basedir.exists() || !basedir.isDirectory()) {
460             return null;
461         }
462 
463         DirectoryScanner scanner = new DirectoryScanner();
464 
465         String[] includesArray = fileSet.getIncludesArray();
466         String[] excludesArray = fileSet.getExcludesArray();
467 
468         if (includesArray.length > 0) {
469             scanner.setIncludes(includesArray);
470         }
471 
472         if (excludesArray.length > 0) {
473             scanner.setExcludes(excludesArray);
474         }
475 
476         if (fileSet.isUseDefaultExcludes()) {
477             scanner.addDefaultExcludes();
478         }
479 
480         scanner.setBasedir(basedir);
481         scanner.setFollowSymlinks(fileSet.isFollowSymlinks());
482 
483         scanner.scan();
484 
485         return scanner;
486     }
487 }