/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.commons.io; import java.io.File; import java.io.FileFilter; import java.io.IOException; import java.util.Collection; import org.apache.commons.io.filefilter.FileFilterUtils; import org.apache.commons.io.filefilter.IOFileFilter; import org.apache.commons.io.filefilter.TrueFileFilter; /** * Abstract class that walks through a directory hierarchy and provides * subclasses with convenient hooks to add specific behaviour. * <p> * This class operates with a {@link FileFilter} and maximum depth to * limit the files and direcories visited. * Commons IO supplies many common filter implementations in the * <a href="filefilter/package-summary.html"> filefilter</a> package. * <p> * The following sections describe: * <ul> * <li><a href="#example">1. Example Implementation</a> - example * <code>FileCleaner</code> implementation.</li> * <li><a href="#filter">2. Filter Example</a> - using * {@link FileFilter}(s) with <code>DirectoryWalker</code>.</li> * <li><a href="#cancel">3. Cancellation</a> - how to implement cancellation * behaviour.</li> * </ul> * * <a name="example"></a> * <h3>1. Example Implementation</h3> * * There are many possible extensions, for example, to delete all * files and '.svn' directories, and return a list of deleted files: * <pre> * public class FileCleaner extends DirectoryWalker { * * public FileCleaner() { * super(); * } * * public List clean(File startDirectory) { * List results = new ArrayList(); * walk(startDirectory, results); * return results; * } * * protected boolean handleDirectory(File directory, int depth, Collection results) { * // delete svn directories and then skip * if (".svn".equals(directory.getName())) { * directory.delete(); * return false; * } else { * return true; * } * * } * * protected void handleFile(File file, int depth, Collection results) { * // delete file and add to list of deleted * file.delete(); * results.add(file); * } * } * </pre> * * <a name="filter"></a> * <h3>2. Filter Example</h3> * * Choosing which directories and files to process can be a key aspect * of using this class. This information can be setup in three ways, * via three different constructors. * <p> * The first option is to visit all directories and files. * This is achieved via the no-args constructor. * <p> * The second constructor option is to supply a single {@link FileFilter} * that describes the files and directories to visit. Care must be taken * with this option as the same filter is used for both directories * and files. * <p> * For example, if you wanted all directories which are not hidden * and files which end in ".txt": * <pre> * public class FooDirectoryWalker extends DirectoryWalker { * public FooDirectoryWalker(FileFilter filter) { * super(filter, -1); * } * } * * // Build up the filters and create the walker * // Create a filter for Non-hidden directories * IOFileFilter fooDirFilter = * FileFilterUtils.andFileFilter(FileFilterUtils.directoryFileFilter, * HiddenFileFilter.VISIBLE); * * // Create a filter for Files ending in ".txt" * IOFileFilter fooFileFilter = * FileFilterUtils.andFileFilter(FileFilterUtils.fileFileFilter, * FileFilterUtils.suffixFileFilter(".txt")); * * // Combine the directory and file filters using an OR condition * java.io.FileFilter fooFilter = * FileFilterUtils.orFileFilter(fooDirFilter, fooFileFilter); * * // Use the filter to construct a DirectoryWalker implementation * FooDirectoryWalker walker = new FooDirectoryWalker(fooFilter); * </pre> * <p> * The third constructor option is to specify separate filters, one for * directories and one for files. These are combined internally to form * the correct <code>FileFilter</code>, something which is very easy to * get wrong when attempted manually, particularly when trying to * express constructs like 'any file in directories named docs'. * <p> * For example, if you wanted all directories which are not hidden * and files which end in ".txt": * <pre> * public class FooDirectoryWalker extends DirectoryWalker { * public FooDirectoryWalker(IOFileFilter dirFilter, IOFileFilter fileFilter) { * super(dirFilter, fileFilter, -1); * } * } * * // Use the filters to construct the walker * FooDirectoryWalker walker = new FooDirectoryWalker( * HiddenFileFilter.VISIBLE, * FileFilterUtils.suffixFileFilter(".txt"), * ); * </pre> * This is much simpler than the previous example, and is why it is the preferred * option for filtering. * * <a name="cancel"></a> * <h3>3. Cancellation</h3> * * The DirectoryWalker contains some of the logic required for cancel processing. * Subclasses must complete the implementation. * <p> * What <code>DirectoryWalker</code> does provide for cancellation is: * <ul> * <li>{@link CancelException} which can be thrown in any of the * <i>lifecycle</i> methods to stop processing.</li> * <li>The <code>walk()</code> method traps thrown {@link CancelException} * and calls the <code>handleCancelled()</code> method, providing * a place for custom cancel processing.</li> * </ul> * <p> * Implementations need to provide: * <ul> * <li>The decision logic on whether to cancel processing or not.</li> * <li>Constructing and throwing a {@link CancelException}.</li> * <li>Custom cancel processing in the <code>handleCancelled()</code> method. * </ul> * <p> * Two possible scenarios are envisaged for cancellation: * <ul> * <li><a href="#external">3.1 External / Mult-threaded</a> - cancellation being * decided/initiated by an external process.</li> * <li><a href="#internal">3.2 Internal</a> - cancellation being decided/initiated * from within a DirectoryWalker implementation.</li> * </ul> * <p> * The following sections provide example implementations for these two different * scenarios. * * <a name="external"></a> * <h4>3.1 External / Multi-threaded</h4> * * This example provides a public <code>cancel()</code> method that can be * called by another thread to stop the processing. A typical example use-case * would be a cancel button on a GUI. Calling this method sets a * <a href="http://java.sun.com/docs/books/jls/second_edition/html/classes.doc.html#36930"> * volatile</a> flag to ensure it will work properly in a multi-threaded environment. * The flag is returned by the <code>handleIsCancelled()</code> method, which * will cause the walk to stop immediately. The <code>handleCancelled()</code> * method will be the next, and last, callback method received once cancellation * has occurred. * * <pre> * public class FooDirectoryWalker extends DirectoryWalker { * * private volatile boolean cancelled = false; * * public void cancel() { * cancelled = true; * } * * private void handleIsCancelled(File file, int depth, Collection results) { * return cancelled; * } * * protected void handleCancelled(File startDirectory, Collection results, CancelException cancel) { * // implement processing required when a cancellation occurs * } * } * </pre> * * <a name="internal"></a> * <h4>3.2 Internal</h4> * * This shows an example of how internal cancellation processing could be implemented. * <b>Note</b> the decision logic and throwing a {@link CancelException} could be implemented * in any of the <i>lifecycle</i> methods. * * <pre> * public class BarDirectoryWalker extends DirectoryWalker { * * protected boolean handleDirectory(File directory, int depth, Collection results) throws IOException { * // cancel if hidden directory * if (directory.isHidden()) { * throw new CancelException(file, depth); * } * return true; * } * * protected void handleFile(File file, int depth, Collection results) throws IOException { * // cancel if read-only file * if (!file.canWrite()) { * throw new CancelException(file, depth); * } * results.add(file); * } * * protected void handleCancelled(File startDirectory, Collection results, CancelException cancel) { * // implement processing required when a cancellation occurs * } * } * </pre> * * @since Commons IO 1.3 * @version $Revision: 424748 $ */ public abstract class DirectoryWalker { /** * The file filter to use to filter files and directories. */ private final FileFilter filter; /** * The limit on the directory depth to walk. */ private final int depthLimit; /** * Construct an instance with no filtering and unlimited <i>depth</i>. */ protected DirectoryWalker() { this(null, -1); } /** * Construct an instance with a filter and limit the <i>depth</i> navigated to. * <p> * The filter controls which files and directories will be navigated to as * part of the walk. The {@link FileFilterUtils} class is useful for combining * various filters together. A <code>null</code> filter means that no * filtering should occur and all files and directories will be visited. * * @param filter the filter to apply, null means visit all files * @param depthLimit controls how <i>deep</i> the hierarchy is * navigated to (less than 0 means unlimited) */ protected DirectoryWalker(FileFilter filter, int depthLimit) { this.filter = filter; this.depthLimit = depthLimit; } /** * Construct an instance with a directory and a file filter and an optional * limit on the <i>depth</i> navigated to. * <p> * The filters control which files and directories will be navigated to as part * of the walk. This constructor uses {@link FileFilterUtils#makeDirectoryOnly(IOFileFilter)} * and {@link FileFilterUtils#makeFileOnly(IOFileFilter)} internally to combine the filters. * A <code>null</code> filter means that no filtering should occur. * * @param directoryFilter the filter to apply to directories, null means visit all directories * @param fileFilter the filter to apply to files, null means visit all files * @param depthLimit controls how <i>deep</i> the hierarchy is * navigated to (less than 0 means unlimited) */ protected DirectoryWalker(IOFileFilter directoryFilter, IOFileFilter fileFilter, int depthLimit) { if (directoryFilter == null && fileFilter == null) { this.filter = null; } else { directoryFilter = (directoryFilter != null ? directoryFilter : TrueFileFilter.TRUE); fileFilter = (fileFilter != null ? fileFilter : TrueFileFilter.TRUE); directoryFilter = FileFilterUtils.makeDirectoryOnly(directoryFilter); fileFilter = FileFilterUtils.makeFileOnly(fileFilter); this.filter = FileFilterUtils.orFileFilter(directoryFilter, fileFilter); } this.depthLimit = depthLimit; } //----------------------------------------------------------------------- /** * Internal method that walks the directory hierarchy in a depth-first manner. * <p> * Users of this class do not need to call this method. This method will * be called automatically by another (public) method on the specific subclass. * <p> * Writers of subclasses should call this method to start the directory walk. * Once called, this method will emit events as it walks the hierarchy. * The event methods have the prefix <code>handle</code>. * * @param startDirectory the directory to start from, not null * @param results the collection of result objects, may be updated * @throws NullPointerException if the start directory is null * @throws IOException if an I/O Error occurs */ protected final void walk(File startDirectory, Collection results) throws IOException { if (startDirectory == null) { throw new NullPointerException("Start Directory is null"); } try { handleStart(startDirectory, results); walk(startDirectory, 0, results); handleEnd(results); } catch(CancelException cancel) { handleCancelled(startDirectory, results, cancel); } } /** * Main recursive method to examine the directory hierarchy. * * @param directory the directory to examine, not null * @param depth the directory level (starting directory = 0) * @param results the collection of result objects, may be updated * @throws IOException if an I/O Error occurs */ private void walk(File directory, int depth, Collection results) throws IOException { checkIfCancelled(directory, depth, results); if (handleDirectory(directory, depth, results)) { handleDirectoryStart(directory, depth, results); int childDepth = depth + 1; if (depthLimit < 0 || childDepth <= depthLimit) { checkIfCancelled(directory, depth, results); File[] childFiles = (filter == null ? directory.listFiles() : directory.listFiles(filter)); if (childFiles == null) { handleRestricted(directory, childDepth, results); } else { for (int i = 0; i < childFiles.length; i++) { File childFile = childFiles[i]; if (childFile.isDirectory()) { walk(childFile, childDepth, results); } else { checkIfCancelled(childFile, childDepth, results); handleFile(childFile, childDepth, results); checkIfCancelled(childFile, childDepth, results); } } } } handleDirectoryEnd(directory, depth, results); } checkIfCancelled(directory, depth, results); } //----------------------------------------------------------------------- /** * Checks whether the walk has been cancelled by calling {@link #handleIsCancelled}, * throwing a <code>CancelException</code> if it has. * <p> * Writers of subclasses should not normally call this method as it is called * automatically by the walk of the tree. However, sometimes a single method, * typically {@link #handleFile}, may take a long time to run. In that case, * you may wish to check for cancellation by calling this method. * * @param file the current file being processed * @param depth the current file level (starting directory = 0) * @param results the collection of result objects, may be updated * @throws IOException if an I/O Error occurs */ protected final void checkIfCancelled(File file, int depth, Collection results) throws IOException { if (handleIsCancelled(file, depth, results)) { throw new CancelException(file, depth); } } /** * Overridable callback method invoked to determine if the entire walk * operation should be immediately cancelled. * <p> * This method should be implemented by those subclasses that want to * provide a public <code>cancel()</code> method available from another * thread. The design pattern for the subclass should be as follows: * <pre> * public class FooDirectoryWalker extends DirectoryWalker { * private volatile boolean cancelled = false; * * public void cancel() { * cancelled = true; * } * private void handleIsCancelled(File file, int depth, Collection results) { * return cancelled; * } * protected void handleCancelled(File startDirectory, * Collection results, CancelException cancel) { * // implement processing required when a cancellation occurs * } * } * </pre> * <p> * If this method returns true, then the directory walk is immediately * cancelled. The next callback method will be {@link #handleCancelled}. * <p> * This implementation returns false. * * @param file the file or directory being processed * @param depth the current directory level (starting directory = 0) * @param results the collection of result objects, may be updated * @return true if the walk has been cancelled * @throws IOException if an I/O Error occurs */ protected boolean handleIsCancelled( File file, int depth, Collection results) throws IOException { // do nothing - overridable by subclass return false; // not cancelled } /** * Overridable callback method invoked when the operation is cancelled. * The file being processed when the cancellation occurred can be * obtained from the exception. * <p> * This implementation just re-throws the {@link CancelException}. * * @param startDirectory the directory that the walk started from * @param results the collection of result objects, may be updated * @param cancel the exception throw to cancel further processing * containing details at the point of cancellation. * @throws IOException if an I/O Error occurs */ protected void handleCancelled(File startDirectory, Collection results, CancelException cancel) throws IOException { // re-throw exception - overridable by subclass throw cancel; } //----------------------------------------------------------------------- /** * Overridable callback method invoked at the start of processing. * <p> * This implementation does nothing. * * @param startDirectory the directory to start from * @param results the collection of result objects, may be updated * @throws IOException if an I/O Error occurs */ protected void handleStart(File startDirectory, Collection results) throws IOException { // do nothing - overridable by subclass } /** * Overridable callback method invoked to determine if a directory should be processed. * <p> * This method returns a boolean to indicate if the directory should be examined or not. * If you return false, the entire directory and any subdirectories will be skipped. * Note that this functionality is in addition to the filtering by file filter. * <p> * This implementation does nothing and returns true. * * @param directory the current directory being processed * @param depth the current directory level (starting directory = 0) * @param results the collection of result objects, may be updated * @return true to process this directory, false to skip this directory * @throws IOException if an I/O Error occurs */ protected boolean handleDirectory(File directory, int depth, Collection results) throws IOException { // do nothing - overridable by subclass return true; // process directory } /** * Overridable callback method invoked at the start of processing each directory. * <p> * This implementation does nothing. * * @param directory the current directory being processed * @param depth the current directory level (starting directory = 0) * @param results the collection of result objects, may be updated * @throws IOException if an I/O Error occurs */ protected void handleDirectoryStart(File directory, int depth, Collection results) throws IOException { // do nothing - overridable by subclass } /** * Overridable callback method invoked for each (non-directory) file. * <p> * This implementation does nothing. * * @param file the current file being processed * @param depth the current directory level (starting directory = 0) * @param results the collection of result objects, may be updated * @throws IOException if an I/O Error occurs */ protected void handleFile(File file, int depth, Collection results) throws IOException { // do nothing - overridable by subclass } /** * Overridable callback method invoked for each restricted directory. * <p> * This implementation does nothing. * * @param directory the restricted directory * @param depth the current directory level (starting directory = 0) * @param results the collection of result objects, may be updated * @throws IOException if an I/O Error occurs */ protected void handleRestricted(File directory, int depth, Collection results) throws IOException { // do nothing - overridable by subclass } /** * Overridable callback method invoked at the end of processing each directory. * <p> * This implementation does nothing. * * @param directory the directory being processed * @param depth the current directory level (starting directory = 0) * @param results the collection of result objects, may be updated * @throws IOException if an I/O Error occurs */ protected void handleDirectoryEnd(File directory, int depth, Collection results) throws IOException { // do nothing - overridable by subclass } /** * Overridable callback method invoked at the end of processing. * <p> * This implementation does nothing. * * @param results the collection of result objects, may be updated * @throws IOException if an I/O Error occurs */ protected void handleEnd(Collection results) throws IOException { // do nothing - overridable by subclass } //----------------------------------------------------------------------- /** * CancelException is thrown in DirectoryWalker to cancel the current * processing. */ public static class CancelException extends IOException { /** Serialization id. */ private static final long serialVersionUID = 1347339620135041008L; /** The file being processed when the exception was thrown. */ private File file; /** The file depth when the exception was thrown. */ private int depth = -1; /** * Constructs a <code>CancelException</code> with * the file and depth when cancellation occurred. * * @param file the file when the operation was cancelled, may be null * @param depth the depth when the operation was cancelled, may be null */ public CancelException(File file, int depth) { this("Operation Cancelled", file, depth); } /** * Constructs a <code>CancelException</code> with * an appropriate message and the file and depth when * cancellation occurred. * * @param message the detail message * @param file the file when the operation was cancelled * @param depth the depth when the operation was cancelled */ public CancelException(String message, File file, int depth) { super(message); this.file = file; this.depth = depth; } /** * Return the file when the operation was cancelled. * * @return the file when the operation was cancelled */ public File getFile() { return file; } /** * Return the depth when the operation was cancelled. * * @return the depth when the operation was cancelled */ public int getDepth() { return depth; } } }