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ö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 }