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