View Javadoc

1   // Copyright (C) 2004 - 2012 Philip Aston
2   // All rights reserved.
3   //
4   // This file is part of The Grinder software distribution. Refer to
5   // the file LICENSE which is part of The Grinder distribution for
6   // licensing details. The Grinder distribution is available on the
7   // Internet at http://grinder.sourceforge.net/
8   //
9   // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
10  // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
11  // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
12  // FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
13  // COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
14  // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
15  // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
16  // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
17  // HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
18  // STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
19  // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
20  // OF THE POSSIBILITY OF SUCH DAMAGE.
21  
22  package net.grinder.util;
23  
24  import java.io.File;
25  import java.io.FileFilter;
26  import java.io.FileInputStream;
27  import java.io.FileOutputStream;
28  import java.io.IOException;
29  import java.io.Serializable;
30  import java.util.ArrayList;
31  import java.util.HashSet;
32  import java.util.List;
33  import java.util.Set;
34  
35  import net.grinder.common.Closer;
36  
37  
38  /**
39   * Wrapper around a directory path that behaves in a similar manner to
40   * <code>java.io.File</code>. Provides utility methods for working
41   * with the directory represented by the path.
42   *
43   * <p>A <code>Directory</code> be constructed with a path that
44   * represents an existing directory, or a path that represents no
45   * existing file. The physical directory can be created later using
46   * {@link #create}.</p>
47   *
48   * @author Philip Aston
49   */
50  public final class Directory implements Serializable {
51    private static final long serialVersionUID = 1;
52  
53    private static final FileFilter s_matchAllFilesFilter =
54      new MatchAllFilesFilter();
55  
56    private final File m_directory;
57    private final List<String> m_warnings = new ArrayList<String>();
58  
59    /**
60     * Returns a filter matching all files.
61     *
62     * @return A filter matching all files.
63     */
64    public static FileFilter getMatchAllFilesFilter() {
65      return s_matchAllFilesFilter;
66    }
67  
68    /**
69     * Constructor that builds a Directory for the current working directory.
70     */
71    public Directory() {
72      m_directory = new File(".");
73    }
74  
75    /**
76     * Constructor.
77     *
78     * @param directory The directory path upon which this
79     * <code>Directory</code> operates.
80     * @exception DirectoryException If the path <code>directory</code>
81     * represents a file that exists but is not a directory.
82     */
83    public Directory(final File directory) throws DirectoryException {
84      if (directory == null) {
85        m_directory = new File(".");
86      }
87      else {
88        if (directory.exists() && !directory.isDirectory()) {
89          throw new DirectoryException(
90            "'" + directory.getPath() + "' is not a directory");
91        }
92  
93        m_directory = directory;
94      }
95    }
96  
97    /**
98     * Create the directory if it doesn't exist.
99     *
100    * @exception DirectoryException If the directory could not be created.
101    */
102   public void create() throws DirectoryException {
103     if (!getFile().exists()) {
104       if (!getFile().mkdirs()) {
105         throw new DirectoryException(
106           "Could not create directory '" + getFile() + "'");
107       }
108     }
109   }
110 
111   /**
112    * Get as a {@link File}.
113    *
114    * @return The <code>File</code>.
115    */
116   public File getFile() {
117     return m_directory;
118   }
119 
120   /**
121    * Return a {@link File} representing the absolute path of a file in this
122    * directory.
123    *
124    * @param child
125    *            Relative file in this directory. If <code>null</code>, the
126    *            result is equivalent to {@link #getFile()}.
127    * @return The <code>File</code>.
128    */
129   public File getFile(final File child) {
130     if (child == null) {
131       return getFile();
132     }
133     else {
134       return new File(getFile(), child.getPath());
135     }
136   }
137 
138   /**
139    * Equivalent to <code>listContents(filter, false, false)</code>.
140    *
141    * @param filter
142    *          Filter that controls the files that are returned.
143    * @return The list of files. Files are relative to the directory, not
144    *         absolute. More deeply nested files are later in the list. The list
145    *         is empty if the directory does not exist.
146    * @see #listContents(FileFilter, boolean, boolean)
147    */
148   public File[] listContents(final FileFilter filter) {
149     return listContents(filter, false, false);
150   }
151 
152   /**
153    * List the files in the hierarchy below the directory that have been modified
154    * after <code>since</code>.
155    *
156    * @param filter
157    *          Filter that controls the files that are returned.
158    * @param includeDirectories
159    *          Whether to include directories in the returned files. Only
160    *          directories that match the filter will be returned.
161    * @param absolutePaths
162    *          Whether returned files should be relative to the directory or
163    *          absolute.
164    * @return The list of files. More deeply nested files are later in the list.
165    *         The list is empty if the directory does not exist.
166    */
167   public File[] listContents(final FileFilter filter,
168                              final boolean includeDirectories,
169                              final boolean absolutePaths) {
170 
171     final List<File> resultList = new ArrayList<File>();
172     final Set<File> visited = new HashSet<File>();
173     final List<File> directoriesToVisit = new ArrayList<File>();
174 
175     final File rootFile = getFile();
176 
177     if (rootFile.exists() && filter.accept(rootFile)) {
178 
179       // We use null here rather than File("") as it helps below. File(File(""),
180       // "blah") is "/blah", but File(null, "blah") is "blah".
181       directoriesToVisit.add(null);
182 
183       if (includeDirectories) {
184         resultList.add(absolutePaths ? rootFile : new File(""));
185       }
186     }
187 
188     while (directoriesToVisit.size() > 0) {
189       final File[] directories =
190         directoriesToVisit.toArray(new File[directoriesToVisit.size()]);
191 
192       directoriesToVisit.clear();
193 
194       for (final File relativeDirectory : directories) {
195         final File absoluteDirectory = getFile(relativeDirectory);
196 
197         visited.add(relativeDirectory);
198 
199         // We use list() rather than listFiles() so the results are
200         // relative, not absolute.
201         final String[] children = absoluteDirectory.list();
202 
203         if (children == null) {
204           // This can happen if the user does not have permission to
205           // list the directory.
206           synchronized (m_warnings) {
207             m_warnings.add("Could not list '" + absoluteDirectory);
208           }
209           continue;
210         }
211 
212         for (final String element : children) {
213           final File relativeChild = new File(relativeDirectory, element);
214           final File absoluteChild = new File(absoluteDirectory, element);
215 
216           if (filter.accept(absoluteChild)) {
217             // Links (hard or symbolic) are transparent to isFile(),
218             // isDirectory(); but we're careful to filter things that are
219             // neither (e.g. FIFOs).
220             if (includeDirectories && absoluteChild.isDirectory() ||
221                 absoluteChild.isFile()) {
222               resultList.add(absolutePaths ? absoluteChild : relativeChild);
223             }
224 
225             if (absoluteChild.isDirectory() &&
226                 !visited.contains(relativeChild)) {
227               directoriesToVisit.add(relativeChild);
228             }
229           }
230         }
231       }
232     }
233 
234     return resultList.toArray(new File[resultList.size()]);
235   }
236 
237   /**
238    * Delete the contents of the directory.
239    *
240    * <p>Does nothing if the directory does not exist.</p>
241    *
242    * @throws DirectoryException If a file could not be deleted. The
243    * contents of the directory are left in an indeterminate state.
244    * @see #delete
245    */
246   public void deleteContents() throws DirectoryException {
247     // We rely on the order of the listContents result: more deeply
248     // nested files are later in the list.
249     final File[] deleteList = listContents(s_matchAllFilesFilter, true, true);
250 
251     for (int i = deleteList.length - 1; i >= 0; --i) {
252       if (deleteList[i].equals(getFile())) {
253         continue;
254       }
255 
256       if (!deleteList[i].delete()) {
257         throw new DirectoryException(
258           "Could not delete '" + deleteList[i] + "'");
259       }
260     }
261   }
262 
263   /**
264    * Delete the directory. This will fail if the directory is not
265    * empty.
266    *
267    * @throws DirectoryException If the directory could not be deleted.
268    * @see #deleteContents
269    */
270   public void delete() throws DirectoryException {
271     if (!getFile().delete()) {
272       throw new DirectoryException("Could not delete '" + getFile() + "'");
273     }
274   }
275 
276   /**
277    * If possible, convert a path relative to the current working directory to
278    * a path relative to this directory. If not possible, return the absolute
279    * version of the path.
280    *
281    * @param file The input path. Relative to the CWD, or absolute.
282    * @return The simplified path.
283    */
284   public File rebaseFromCWD(final File file) {
285     final File absolute = file.getAbsoluteFile();
286     final File relativeResult =  relativeFile(absolute, true);
287 
288     if (relativeResult == null) {
289       return absolute;
290     }
291 
292     return relativeResult;
293   }
294 
295   /**
296    * Convert the supplied {@code file}, to a path relative to this directory.
297    * The file need not exist.
298    *
299    * <p>
300    * If {@code file} is relative, this directory is considered its base.
301    * </p>
302    *
303    * @param file
304    *          The file to search for.
305    * @param mustBeChild
306    *          If {@code true} and {@code path} belongs to another file system;
307    *          is absolute with a different base path; or is a relative path
308    *          outside of the directory ({@code ../somewhere/else}), then
309    *          {@code null} will be returned.
310    * @return A path relative to this directory, or {@code null}.
311    */
312   public File relativeFile(final File file, final boolean mustBeChild) {
313 
314     final File f;
315 
316     if (file.isAbsolute()) {
317       f = file;
318     }
319     else if (!mustBeChild) {
320       return file;
321     }
322     else {
323       f = getFile(file);
324     }
325 
326     return relativePath(getFile(), f, mustBeChild);
327   }
328 
329   /**
330    * Calculate a relative path from {@code from} to {@code to}.
331    *
332    * <p>
333    * Package scope for unit tests.
334    * </p>
335    *
336    * @param from
337    *          Source file or directory.
338    * @param to
339    *          Target file or directory.
340    * @param mustBeChild
341    *          If {@code true} and {@code to} is not a child of {@code from} or
342    *          equivalent to {@from}, return {@code null}.
343    * @return The relative path. {@code null} if {@code to} belongs to a
344    *         different file system, or {@code mustBeChild} is {@code true} and
345    *         {@code to} is not a child path of {@code from}.
346    * @throws UnexpectedIOException
347    *           If a canonical path could not be calculated.
348    */
349   static File relativePath(final File from,
350                            final File to,
351                            final boolean mustBeChild) {
352     final String[] fromPaths;
353     final String[] toPaths;
354 
355     try {
356       fromPaths = splitPath(from.getCanonicalPath());
357       toPaths = splitPath(to.getCanonicalFile().getPath());
358     }
359     catch (final IOException e) {
360       throw new UnexpectedIOException(e);
361     }
362 
363     int i = 0;
364 
365     while (i < fromPaths.length &&
366            i < toPaths.length &&
367            fromPaths[i].equals(toPaths[i])) {
368       ++i;
369     }
370 
371     if (mustBeChild && i != fromPaths.length) {
372       return null;
373     }
374 
375     // i == 0: The root file is different.
376     // i == 1: The root file is the same, but there's no common path.
377     if (i <= 1) {
378       return null;
379     }
380 
381     final StringBuilder result = new StringBuilder();
382 
383     for (int j = i; j < fromPaths.length; ++j) {
384       result.append("..");
385       result.append(File.separator);
386     }
387 
388     for (int j = i; j < toPaths.length; ++j) {
389       result.append(toPaths[j]);
390 
391       if (j != toPaths.length - 1) {
392         result.append(File.separator);
393       }
394     }
395 
396     if (result.length() == 0) {
397       return new File(".");
398     }
399 
400     return new File(result.toString());
401   }
402 
403   private static String[] splitPath(final String path) {
404     return path.split(File.separatorChar == '\\' ? "\\\\" : File.separator);
405   }
406 
407   /**
408    * Rebase a whole path by calling {@link #rebaseFile} on each of its
409    * elements and joining the result.
410    *
411    * @param path
412    *          The path.
413    * @return The result.
414    */
415   public List<File> rebasePath(final String path) {
416     final String[] elements = path.split(File.pathSeparator);
417 
418     final List<File> result = new ArrayList<File>(elements.length);
419 
420     for (final String e : elements) {
421       if (!e.isEmpty()) {
422         result.add(rebaseFromCWD(new File(e)));
423       }
424     }
425 
426     return result;
427   }
428 
429   /**
430    * Test whether a File represents the name of a file that is a descendant of
431    * the directory.
432    *
433    * @param file File to test.
434    * @return {@code boolean} => file is a descendant.
435    */
436   public boolean isParentOf(final File file) {
437     final File thisFile = getFile();
438 
439     File candidate = file.getParentFile();
440 
441     while (candidate != null) {
442       if (thisFile.equals(candidate)) {
443         return true;
444       }
445 
446       candidate = candidate.getParentFile();
447     }
448 
449 
450     return false;
451   }
452 
453   /**
454    * Copy contents of the directory to the target directory.
455    *
456    * @param target Target directory.
457    * @param incremental <code>true</code> => copy newer files to the
458    * directory. <code>false</code> => overwrite the target directory.
459    * @throws IOException If a file could not be copied. The contents
460    * of the target directory are left in an indeterminate state.
461    */
462   public void copyTo(final Directory target, final boolean incremental)
463     throws IOException {
464 
465     if (!getFile().exists()) {
466       throw new DirectoryException(
467         "Source directory '" + getFile() + "' does not exist");
468     }
469 
470     target.create();
471 
472     if (!incremental) {
473       target.deleteContents();
474     }
475 
476     final File[] files = listContents(s_matchAllFilesFilter, true, false);
477     final StreamCopier streamCopier = new StreamCopier(4096, false);
478 
479     for (final File file : files) {
480       final File source = getFile(file);
481       final File destination = target.getFile(file);
482 
483       if (source.isDirectory()) {
484         destination.mkdirs();
485       }
486       else {
487         // Copy file.
488         if (!incremental ||
489             !destination.exists() ||
490             source.lastModified() > destination.lastModified()) {
491 
492           FileInputStream in = null;
493           FileOutputStream out = null;
494 
495           try {
496             in = new FileInputStream(source);
497             out = new FileOutputStream(destination);
498             streamCopier.copy(in, out);
499           }
500           finally {
501             Closer.close(in);
502             Closer.close(out);
503           }
504         }
505       }
506     }
507   }
508 
509   /**
510    * Return a list of warnings that have occurred since the last time
511    * {@link #getWarnings} was called.
512    *
513    * @return The list of warnings.
514    */
515   public String[] getWarnings() {
516     synchronized (m_warnings) {
517       try {
518         return m_warnings.toArray(new String[m_warnings.size()]);
519       }
520       finally {
521         m_warnings.clear();
522       }
523     }
524   }
525 
526   /**
527    * An exception type used to report <code>Directory</code> related
528    * problems.
529    */
530   public static final class DirectoryException extends IOException {
531     DirectoryException(final String message) {
532       super(message);
533     }
534   }
535 
536   /**
537    * Delegate equality to our <code>File</code>.
538    *
539    * @return The hash code.
540    */
541   @Override
542   public int hashCode() {
543     return getFile().hashCode();
544   }
545 
546   /**
547    * Delegate equality to our <code>File</code>.
548    *
549    * @param o Object to compare.
550    * @return <code>true</code> => equal.
551    */
552   @Override
553   public boolean equals(final Object o) {
554     if (o == this) {
555       return true;
556     }
557 
558     if (o == null || o.getClass() != Directory.class) {
559       return false;
560     }
561 
562     return getFile().equals(((Directory)o).getFile());
563   }
564 
565   private static class MatchAllFilesFilter implements FileFilter {
566     @Override
567     public boolean accept(final File file) {
568       return true;
569     }
570   }
571 }