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