View Javadoc
1   package org.codehaus.mojo.jaxb2.shared;
2   
3   import org.apache.maven.plugin.MojoExecutionException;
4   import org.apache.maven.plugin.logging.Log;
5   import org.codehaus.mojo.jaxb2.AbstractJaxbMojo;
6   import org.codehaus.mojo.jaxb2.shared.filters.Filter;
7   import org.codehaus.mojo.jaxb2.shared.filters.Filters;
8   import org.codehaus.plexus.util.FileUtils;
9   import org.codehaus.plexus.util.Os;
10  import org.codehaus.plexus.util.StringUtils;
11  
12  import java.io.File;
13  import java.io.FileFilter;
14  import java.io.IOException;
15  import java.net.MalformedURLException;
16  import java.net.URL;
17  import java.net.URLDecoder;
18  import java.util.ArrayList;
19  import java.util.List;
20  import java.util.Map;
21  import java.util.SortedMap;
22  import java.util.TreeMap;
23  
24  /**
25   * The Jaxb2 Maven Plugin needs to fiddle with the filesystem a great deal, to create and optionally prune
26   * directories or detect/create various files. This utility class contains all such algorithms, and serves as
27   * an entry point to any Plexus Utils methods.
28   *
29   * @author <a href="mailto:lj@jguru.se">Lennart J&ouml;relid</a>
30   * @since 2.0
31   */
32  public final class FileSystemUtilities {
33  
34      /*
35       * Hide the constructor for utility classes.
36       */
37      private FileSystemUtilities() {
38          // Do nothing
39      }
40  
41      /**
42       * FileFilter which accepts Files that exist and for which {@code File.isFile() } is {@code true}.
43       */
44      public static final FileFilter EXISTING_FILE = new FileFilter() {
45          @Override
46          public boolean accept(final File candidate) {
47              return candidate != null && candidate.exists() && candidate.isFile();
48          }
49      };
50  
51      /**
52       * FileFilter which accepts Files that exist and for which {@code File.isDirectory() } is {@code true}.
53       */
54      public static final FileFilter EXISTING_DIRECTORY = new FileFilter() {
55          @Override
56          public boolean accept(final File candidate) {
57              return candidate != null && candidate.exists() && candidate.isDirectory();
58          }
59      };
60  
61      /**
62       * Acquires the canonical path for the supplied file.
63       *
64       * @param file A non-null File for which the canonical path should be retrieved.
65       * @return The canonical path of the supplied file.
66       */
67      public static String getCanonicalPath(final File file) {
68          return getCanonicalFile(file).getPath();
69      }
70  
71      /**
72       * Non-valid Characters for naming files, folders under Windows: <code>":", "*", "?", "\"", "<", ">", "|"</code>
73       *
74       * @see <a href="http://support.microsoft.com/?scid=kb%3Ben-us%3B177506&x=12&y=13">
75       * http://support.microsoft.com/?scid=kb%3Ben-us%3B177506&x=12&y=13</a>;
76       * @see {@code org.codehaus.plexus.util.FileUtils}
77       */
78      private static final String[] INVALID_CHARACTERS_FOR_WINDOWS_FILE_NAME = {":", "*", "?", "\"", "<", ">", "|"};
79  
80      /**
81       * Acquires the canonical File for the supplied file.
82       *
83       * @param file A non-null File for which the canonical File should be retrieved.
84       * @return The canonical File of the supplied file.
85       */
86      public static File getCanonicalFile(final File file) {
87  
88          // Check sanity
89          Validate.notNull(file, "file");
90  
91          // All done
92          try {
93              return file.getCanonicalFile();
94          } catch (IOException e) {
95              throw new IllegalArgumentException("Could not acquire the canonical file for ["
96                      + file.getAbsolutePath() + "]", e);
97          }
98      }
99  
100     /**
101      * <p>Retrieves the canonical File matching the supplied path in the following order or priority:</p>
102      * <ol>
103      * <li><strong>Absolute path:</strong> The path is used by itself (i.e. {@code new File(path);}). If an
104      * existing file or directory matches the provided path argument, its canonical path will be returned.</li>
105      * <li><strong>Relative path:</strong> The path is appended to the baseDir (i.e.
106      * {@code new File(baseDir, path);}). If an existing file or directory matches the provided path argument,
107      * its canonical path will be returned. Only in this case will be baseDir argument be considered.</li>
108      * </ol>
109      * <p>If no file or directory could be derived from the supplied path and baseDir, {@code null} is returned.</p>
110      *
111      * @param path    A non-null path which will be used to find an existing file or directory.
112      * @param baseDir A directory to which the path will be appended to search for the existing file or directory in
113      *                case the file was nonexistent when interpreted as an absolute path.
114      * @return either a canonical File for the path, or {@code null} if no file or directory matched
115      * the supplied path and baseDir.
116      */
117     public static File getExistingFile(final String path, final File baseDir) {
118 
119         // Check sanity
120         Validate.notEmpty(path, "path");
121         final File theFile = new File(path);
122         File toReturn = null;
123 
124         // Is 'path' absolute?
125         if (theFile.isAbsolute() && (EXISTING_FILE.accept(theFile) || EXISTING_DIRECTORY.accept(theFile))) {
126             toReturn = getCanonicalFile(theFile);
127         }
128 
129         // Is 'path' relative?
130         if (!theFile.isAbsolute()) {
131 
132             // In this case, baseDir cannot be null.
133             Validate.notNull(baseDir, "baseDir");
134             final File relativeFile = new File(baseDir, path);
135 
136             if (EXISTING_FILE.accept(relativeFile) || EXISTING_DIRECTORY.accept(relativeFile)) {
137                 toReturn = getCanonicalFile(relativeFile);
138             }
139         }
140 
141         // The path provided did not point to an existing File or Directory.
142         return toReturn;
143     }
144 
145     /**
146      * Retrieves the URL for the supplied File. Convenience method which hides exception handling
147      * for the operation in question.
148      *
149      * @param aFile A File for which the URL should be retrieved.
150      * @return The URL for the supplied aFile.
151      * @throws java.lang.IllegalArgumentException if getting the URL yielded a MalformedURLException.
152      */
153     public static URL getUrlFor(final File aFile) throws IllegalArgumentException {
154 
155         // Check sanity
156         Validate.notNull(aFile, "aFile");
157 
158         try {
159             return aFile.toURI().normalize().toURL();
160         } catch (MalformedURLException e) {
161             throw new IllegalArgumentException("Could not retrieve the URL from file ["
162                     + getCanonicalPath(aFile) + "]", e);
163         }
164     }
165 
166     /**
167      * Acquires the file for a supplied URL, provided that its protocol is is either a file or a jar.
168      *
169      * @param anURL    a non-null URL.
170      * @param encoding The encoding to be used by the URLDecoder to decode the path found.
171      * @return The File pointing to the supplied URL, for file or jar protocol URLs and null otherwise.
172      */
173     public static File getFileFor(final URL anURL, final String encoding) {
174 
175         // Check sanity
176         Validate.notNull(anURL, "anURL");
177         Validate.notNull(encoding, "encoding");
178 
179         final String protocol = anURL.getProtocol();
180         File toReturn = null;
181         if ("file".equalsIgnoreCase(protocol)) {
182             try {
183                 final String decodedPath = URLDecoder.decode(anURL.getPath(), encoding);
184                 toReturn = new File(decodedPath);
185             } catch (Exception e) {
186                 throw new IllegalArgumentException("Could not get the File for [" + anURL + "]", e);
187             }
188         } else if ("jar".equalsIgnoreCase(protocol)) {
189 
190             try {
191 
192                 // Decode the JAR
193                 final String tmp = URLDecoder.decode(anURL.getFile(), encoding);
194 
195                 // JAR URLs generally contain layered protocols, such as:
196                 // jar:file:/some/path/to/nazgul-tools-validation-aspect-4.0.2.jar!/the/package/ValidationAspect.class
197                 final URL innerURL = new URL(tmp);
198 
199                 // We can handle File protocol URLs here.
200                 if ("file".equalsIgnoreCase(innerURL.getProtocol())) {
201 
202                     // Peel off the inner protocol
203                     final String innerUrlPath = innerURL.getPath();
204                     final String filePath = innerUrlPath.contains("!")
205                             ? innerUrlPath.substring(0, innerUrlPath.indexOf("!"))
206                             : innerUrlPath;
207                     toReturn = new File(filePath);
208                 }
209             } catch (Exception e) {
210                 throw new IllegalArgumentException("Could not get the File for [" + anURL + "]", e);
211             }
212         }
213 
214         // All done.
215         return toReturn;
216     }
217 
218 
219     /**
220      * Filters files found either in the sources paths (or in the standardDirectory if no explicit sources are given),
221      * and retrieves a List holding those files that do not match any of the supplied Java Regular Expression
222      * excludePatterns.
223      *
224      * @param baseDir             The non-null basedir Directory.
225      * @param sources             The sources which should be either absolute or relative (to the given baseDir)
226      *                            paths to files or to directories that should be searched recursively for files.
227      * @param standardDirectories If no sources are given, revert to searching all files under these standard
228      *                            directories. Each path is {@code relativize()}-d to the supplied baseDir to
229      *                            reach a directory path.
230      * @param log                 A non-null Maven Log for logging any operations performed.
231      * @param fileTypeDescription A human-readable short description of what kind of files are searched for, such as
232      *                            "xsdSources" or "xjbSources".
233      * @param excludePatterns     An optional List of patterns used to construct an ExclusionRegExpFileFilter used to
234      *                            identify files which should be excluded from the result.
235      * @return URLs to all Files under the supplied sources (or standardDirectories, if no explicit sources
236      * are given) which do not match the supplied Java Regular excludePatterns.
237      */
238     public static List<URL> filterFiles(final File baseDir,
239                                         final List<String> sources,
240                                         final List<String> standardDirectories,
241                                         final Log log,
242                                         final String fileTypeDescription,
243                                         final List<Filter<File>> excludePatterns) {
244 
245         final SortedMap<String, File> pathToResolvedSourceMap = new TreeMap<String, File>();
246 
247         for (String current : standardDirectories) {
248             for (File currentResolvedSource : FileSystemUtilities.filterFiles(
249                     baseDir,
250                     sources,
251                     FileSystemUtilities.relativize(current, baseDir),
252                     log,
253                     fileTypeDescription,
254                     excludePatterns)) {
255 
256                 // Add the source
257                 pathToResolvedSourceMap.put(
258                         FileSystemUtilities.getCanonicalPath(currentResolvedSource),
259                         currentResolvedSource);
260             }
261         }
262 
263         final List<URL> toReturn = new ArrayList<URL>();
264 
265         // Extract the URLs for all resolved Java sources.
266         for (Map.Entry<String, File> current : pathToResolvedSourceMap.entrySet()) {
267             toReturn.add(FileSystemUtilities.getUrlFor(current.getValue()));
268         }
269 
270         if (log.isDebugEnabled()) {
271 
272             final StringBuilder builder = new StringBuilder();
273             builder.append("\n+=================== [Filtered " + fileTypeDescription + "]\n");
274 
275             builder.append("|\n");
276             builder.append("| " + excludePatterns.size() + " Exclude patterns:\n");
277             for (int i = 0; i < excludePatterns.size(); i++) {
278                 builder.append("| [" + (i + 1) + "/" + excludePatterns.size() + "]: " + excludePatterns.get(i) + "\n");
279             }
280 
281             builder.append("|\n");
282             builder.append("| " + standardDirectories.size() + " Standard Directories:\n");
283             for (int i = 0; i < standardDirectories.size(); i++) {
284                 builder.append("| [" + (i + 1) + "/" + standardDirectories.size() + "]: "
285                         + standardDirectories.get(i) + "\n");
286             }
287 
288             builder.append("|\n");
289             builder.append("| " + toReturn.size() + " Results:\n");
290             for (int i = 0; i < toReturn.size(); i++) {
291                 builder.append("| [" + (i + 1) + "/" + toReturn.size() + "]: " + toReturn.get(i) + "\n");
292             }
293             builder.append("|\n");
294             builder.append("+=================== [End Filtered " + fileTypeDescription + "]\n\n");
295 
296             // Log all.
297             log.debug(builder.toString().replace("\n", AbstractJaxbMojo.NEWLINE));
298         }
299 
300         // All done.
301         return toReturn;
302     }
303 
304     /**
305      * Filters files found either in the sources paths (or in the standardDirectory if no explicit sources are given),
306      * and retrieves a List holding those files that do not match any of the supplied Java Regular Expression
307      * excludePatterns.
308      *
309      * @param baseDir             The non-null basedir Directory.
310      * @param sources             The sources which should be either absolute or relative (to the given baseDir)
311      *                            paths to files or to directories that should be searched recursively for files.
312      * @param standardDirectory   If no sources are given, revert to searching all files under this standard directory.
313      *                            This is the path appended to the baseDir to reach a directory.
314      * @param log                 A non-null Maven Log for logging any operations performed.
315      * @param fileTypeDescription A human-readable short description of what kind of files are searched for, such as
316      *                            "xsdSources" or "xjbSources".
317      * @param excludeFilters      An optional List of Filters used to identify files which should be excluded from
318      *                            the result.
319      * @return All files under the supplied sources (or standardDirectory, if no explicit sources are given) which
320      * do not match the supplied Java Regular excludePatterns.
321      */
322     public static List<File> filterFiles(final File baseDir,
323                                          final List<String> sources,
324                                          final String standardDirectory,
325                                          final Log log,
326                                          final String fileTypeDescription,
327                                          final List<Filter<File>> excludeFilters) {
328 
329         // Check sanity
330         Validate.notNull(baseDir, "baseDir");
331         Validate.notNull(log, "log");
332         Validate.notEmpty(standardDirectory, "standardDirectory");
333         Validate.notEmpty(fileTypeDescription, "fileTypeDescription");
334 
335         // No sources provided? Fallback to the standard (which should be a relative path).
336         List<String> effectiveSources = sources;
337         if (sources == null || sources.isEmpty()) {
338             effectiveSources = new ArrayList<String>();
339 
340             final File tmp = new File(standardDirectory);
341             final File rootDirectory = tmp.isAbsolute() ? tmp : new File(baseDir, standardDirectory);
342             effectiveSources.add(FileSystemUtilities.getCanonicalPath(rootDirectory));
343         }
344 
345         // First, remove the non-existent sources.
346         List<File> existingSources = new ArrayList<File>();
347         for (String current : effectiveSources) {
348 
349             final File existingFile = FileSystemUtilities.getExistingFile(current, baseDir);
350             if (existingFile != null) {
351                 existingSources.add(existingFile);
352 
353                 if (log.isDebugEnabled()) {
354                     log.debug("Accepted configured " + fileTypeDescription + " ["
355                             + FileSystemUtilities.getCanonicalFile(existingFile) + "]");
356                 }
357             } else {
358                 if (log.isInfoEnabled()) {
359                     log.info("Ignored given or default " + fileTypeDescription + " [" + current
360                             + "], since it is not an existent file or directory.");
361                 }
362             }
363         }
364 
365         if (log.isDebugEnabled() && existingSources.size() > 0) {
366 
367             final int size = existingSources.size();
368 
369             log.debug(" [" + size + " existing " + fileTypeDescription + "] ...");
370             for (int i = 0; i < size; i++) {
371                 log.debug("   " + (i + 1) + "/" + size + ": " + existingSources.get(i));
372             }
373             log.debug(" ... End [" + size + " existing " + fileTypeDescription + "]");
374         }
375 
376         // All Done.
377         return FileSystemUtilities.resolveRecursively(existingSources, excludeFilters, log);
378     }
379 
380     /**
381      * Retrieves a List of Files containing all the existing files within the supplied files List, including all
382      * files found in directories recursive to any directories provided in the files list. Each file included in the
383      * result must pass an ExclusionRegExpFileFilter synthesized from the supplied exclusions pattern(s).
384      *
385      * @param files            The list of files to resolve, filter and return. If the {@code files} List
386      *                         contains directories, they are searched for Files recursively. Any found Files in such
387      *                         a search are included in the resulting File List if they do not match any of the
388      *                         exclusionFilters supplied.
389      * @param exclusionFilters A List of Filters which identify files to remove from the result - implying that any
390      *                         File matched by any of these exclusionFilters will not be included in the result.
391      * @param log              The active Maven Log.
392      * @return All files in (or files in subdirectories of directories provided in) the files List, provided that each
393      * file is accepted by an ExclusionRegExpFileFilter.
394      */
395     public static List<File> resolveRecursively(final List<File> files,
396                                                 final List<Filter<File>> exclusionFilters,
397                                                 final Log log) {
398 
399         // Check sanity
400         Validate.notNull(files, "files");
401 
402         final List<Filter<File>> effectiveExclusions = exclusionFilters == null
403                 ? new ArrayList<Filter<File>>()
404                 : exclusionFilters;
405 
406         final List<File> toReturn = new ArrayList<File>();
407 
408         if (files.size() > 0) {
409             for (File current : files) {
410 
411                 final boolean isAcceptedFile = EXISTING_FILE.accept(current)
412                         && Filters.noFilterMatches(current, effectiveExclusions);
413                 final boolean isAcceptedDirectory = EXISTING_DIRECTORY.accept(current)
414                         && Filters.noFilterMatches(current, effectiveExclusions);
415 
416                 if (isAcceptedFile) {
417                     toReturn.add(current);
418                 } else if (isAcceptedDirectory) {
419                     recurseAndPopulate(toReturn, effectiveExclusions, current, true, log);
420                 }
421             }
422         }
423 
424         // All done
425         return toReturn;
426     }
427 
428     /**
429      * Convenience method to successfully create a directory - or throw an exception if failing to create it.
430      *
431      * @param aDirectory        The directory to create.
432      * @param cleanBeforeCreate if {@code true}, the directory and all its content will be deleted before being
433      *                          re-created. This will ensure that the created directory is really clean.
434      * @throws MojoExecutionException if the aDirectory could not be created (and/or cleaned).
435      */
436     public static void createDirectory(final File aDirectory, final boolean cleanBeforeCreate)
437             throws MojoExecutionException {
438 
439         // Check sanity
440         Validate.notNull(aDirectory, "aDirectory");
441         validateFileOrDirectoryName(aDirectory);
442 
443         // Clean an existing directory?
444         if (cleanBeforeCreate) {
445             try {
446                 FileUtils.deleteDirectory(aDirectory);
447             } catch (IOException e) {
448                 throw new MojoExecutionException("Could not clean directory [" + getCanonicalPath(aDirectory) + "]", e);
449             }
450         }
451 
452         // Now, make the required directory, if it does not already exist as a directory.
453         final boolean existsAsFile = aDirectory.exists() && aDirectory.isFile();
454         if (existsAsFile) {
455             throw new MojoExecutionException("[" + getCanonicalPath(aDirectory) + "] exists and is a file. "
456                     + "Cannot make directory");
457         } else if (!aDirectory.exists() && !aDirectory.mkdirs()) {
458             throw new MojoExecutionException("Could not create directory [" + getCanonicalPath(aDirectory) + "]");
459         }
460     }
461 
462     /**
463      * If the supplied path refers to a file or directory below the supplied basedir, the returned
464      * path is identical to the part below the basedir.
465      *
466      * @param path      The path to strip off basedir path from, and return.
467      * @param parentDir The maven project basedir.
468      * @return The path relative to basedir, if it is situated below the basedir. Otherwise the supplied path.
469      */
470     public static String relativize(final String path, final File parentDir) {
471 
472         // Check sanity
473         Validate.notNull(path, "path");
474         Validate.notNull(parentDir, "parentDir");
475 
476         final String basedirPath = FileSystemUtilities.getCanonicalPath(parentDir);
477         String toReturn = path;
478 
479         // Compare case insensitive
480         if (path.toLowerCase().startsWith(basedirPath.toLowerCase())) {
481             toReturn = path.substring(basedirPath.length() + 1);
482         }
483 
484         // Handle whitespace in the argument.
485         return toReturn;
486     }
487 
488     /**
489      * If the supplied fileOrDir is a File, it is added to the returned List if any of the filters Match.
490      * If the supplied fileOrDir is a Directory, it is listed and any of the files immediately within the fileOrDir
491      * directory are returned within the resulting List provided that they match any of the supplied filters.
492      *
493      * @param fileOrDir   A File or Directory.
494      * @param fileFilters A List of filter of which at least one must match to add the File
495      *                    (or child Files, in case of a directory) to the resulting List.
496      * @param log         The active Maven Log
497      * @return A List holding the supplied File (or child Files, in case fileOrDir is a Directory) given that at
498      * least one Filter accepts them.
499      */
500     public static List<File> listFiles(final File fileOrDir,
501                                        final List<Filter<File>> fileFilters,
502                                        final Log log) {
503         return listFiles(fileOrDir, fileFilters, false, log);
504     }
505 
506     /**
507      * If the supplied fileOrDir is a File, it is added to the returned List if any of the filters Match.
508      * If the supplied fileOrDir is a Directory, it is listed and any of the files immediately within the fileOrDir
509      * directory are returned within the resulting List provided that they match any of the supplied filters.
510      *
511      * @param fileOrDir              A File or Directory.
512      * @param fileFilters            A List of filter of which at least one must match to add the File (or child Files, in case
513      *                               of a directory) to the resulting List.
514      * @param excludeFilterOperation if {@code true}, all fileFilters are considered exclude filter, i.e. if
515      *                               any of the Filters match the fileOrDir, that fileOrDir will be excluded from the
516      *                               result.
517      * @param log                    The active Maven Log
518      * @return A List holding the supplied File (or child Files, in case fileOrDir is a Directory) given that at
519      * least one Filter accepts them.
520      */
521     public static List<File> listFiles(final File fileOrDir,
522                                        final List<Filter<File>> fileFilters,
523                                        final boolean excludeFilterOperation,
524                                        final Log log) {
525 
526         // Check sanity
527         Validate.notNull(log, "log");
528         Validate.notNull(fileFilters, "fileFilters");
529         final List<File> toReturn = new ArrayList<File>();
530 
531         if (EXISTING_FILE.accept(fileOrDir)) {
532             checkAndAdd(toReturn, fileOrDir, fileFilters, excludeFilterOperation, log);
533         } else if (EXISTING_DIRECTORY.accept(fileOrDir)) {
534 
535             final File[] listedFiles = fileOrDir.listFiles();
536             if (listedFiles != null) {
537                 for (File current : listedFiles) {
538                     checkAndAdd(toReturn, current, fileFilters, excludeFilterOperation, log);
539                 }
540             }
541         }
542 
543         // All done.
544         return toReturn;
545     }
546 
547     //
548     // Private helpers
549     //
550 
551     private static void checkAndAdd(final List<File> toPopulate,
552                                     final File current,
553                                     final List<Filter<File>> fileFilters,
554                                     final boolean excludeFilterOperation,
555                                     final Log log) {
556 
557         //
558         // When no filters are supplied...
559         // [Include Operation]: all files will be rejected
560         // [Exclude Operation]: all files will be included
561         //
562         final boolean noFilters = fileFilters == null || fileFilters.isEmpty();
563         final boolean addFile = excludeFilterOperation
564                 ? noFilters || Filters.rejectAtLeastOnce(current, fileFilters)
565                 : noFilters || Filters.matchAtLeastOnce(current, fileFilters);
566         final String logPrefix = (addFile ? "Accepted " : "Rejected ")
567                 + (current.isDirectory() ? "directory" : "file") + " [";
568 
569         if (addFile) {
570             toPopulate.add(current);
571         }
572 
573         if (log.isDebugEnabled()) {
574             log.debug(logPrefix + getCanonicalPath(current) + "]");
575         }
576     }
577 
578     private static void validateFileOrDirectoryName(final File fileOrDir) {
579 
580         if (Os.isFamily(Os.FAMILY_WINDOWS) && !FileUtils.isValidWindowsFileName(fileOrDir)) {
581             throw new IllegalArgumentException(
582                     "The file (" + fileOrDir + ") cannot contain any of the following characters: \n"
583                             + StringUtils.join(INVALID_CHARACTERS_FOR_WINDOWS_FILE_NAME, " "));
584         }
585     }
586 
587     private static void recurseAndPopulate(final List<File> toPopulate,
588                                            final List<Filter<File>> fileFilters,
589                                            final File aDirectory,
590                                            final boolean excludeOperation,
591                                            final Log log) {
592 
593         final List<File> files = listFiles(aDirectory, fileFilters, excludeOperation, log);
594         for (File current : files) {
595             if (EXISTING_FILE.accept(current)) {
596                 toPopulate.add(current);
597             }
598 
599             if (EXISTING_DIRECTORY.accept(current)) {
600                 recurseAndPopulate(toPopulate, fileFilters, current, excludeOperation, log);
601             }
602         }
603     }
604 }