DirectoryWalker.java

/*
 * 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.nio.file.Files;
import java.util.Collection;
import java.util.Objects;

import org.apache.commons.io.file.PathUtils;
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
 * behavior.
 * <p>
 * This class operates with a {@link FileFilter} and maximum depth to limit the files and directories visited. Commons
 * IO supplies many common filter implementations in the <a href="filefilter/package-summary.html"> filefilter</a>
 * package.
 * </p>
 * <p>
 * The following sections describe:
 * </p>
 * <ul>
 * <li><a href="#example">1. Example Implementation</a> - example {@link FileCleaner} implementation.</li>
 * <li><a href="#filter">2. Filter Example</a> - using {@link FileFilter}(s) with {@link DirectoryWalker}.</li>
 * <li><a href="#cancel">3. Cancellation</a> - how to implement cancellation behavior.</li>
 * </ul>
 *
 * <h2 id="example">1. Example Implementation</h2>
 *
 * 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>
 *
 * <h2 id="filter">2. Filter Example</h2>
 *
 * <p>
 * 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>
 * <p>
 * The first option is to visit all directories and files. This is achieved via the no-args constructor.
 * </p>
 * <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>
 * <p>
 * For example, if you wanted all directories which are not hidden and files which end in ".txt":
 * </p>
 *
 * <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 {@link FileFilter}, 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>
 * <p>
 * For example, if you wanted all directories which are not hidden and files which end in ".txt":
 * </p>
 *
 * <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>
 * <p>
 * This is much simpler than the previous example, and is why it is the preferred option for filtering.
 * </p>
 *
 * <h2 id="cancel">3. Cancellation</h2>
 *
 * <p>
 * The DirectoryWalker contains some of the logic required for cancel processing. Subclasses must complete the
 * implementation.
 * </p>
 * <p>
 * What {@link DirectoryWalker} does provide for cancellation is:
 * </p>
 * <ul>
 * <li>{@link CancelException} which can be thrown in any of the <em>lifecycle</em> methods to stop processing.</li>
 * <li>The {@code walk()} method traps thrown {@link CancelException} and calls the {@code handleCancelled()}
 * method, providing a place for custom cancel processing.</li>
 * </ul>
 * <p>
 * Implementations need to provide:
 * </p>
 * <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()} method.
 * </ul>
 * <p>
 * Two possible scenarios are envisaged for cancellation:
 * </p>
 * <ul>
 * <li><a href="#external">3.1 External / Multi-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.
 * </p>
 *
 * <h3 id="external">3.1 External / Multi-threaded</h3>
 *
 * <p>
 * This example provides a public {@code cancel()} method that can be called by another thread to stop the
 * processing. A typical example use-case is a cancel button on a GUI. Calling this method sets a
 * <a href='https://docs.oracle.com/javase/specs/jls/se8/html/jls-8.html#d5e12277'>(@code volatile}</a>
 * flag to ensure it works properly in a multi-threaded environment.
 * The flag is returned by the {@code handleIsCancelled()} method, which causes the walk to stop
 * immediately. The {@code handleCancelled()} method will be the next, and last, callback method received once cancellation has occurred.
 * </p>
 *
 * <pre>
 * public class FooDirectoryWalker extends DirectoryWalker {
 *
 *     private volatile boolean cancelled = false;
 *
 *     public void cancel() {
 *         cancelled = true;
 *     }
 *
 *     protected boolean 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>
 *
 * <h3 id="internal">3.2 Internal</h3>
 *
 * <p>
 * This shows an example of how internal cancellation processing could be implemented. <strong>Note</strong> the decision logic
 * and throwing a {@link CancelException} could be implemented in any of the <em>lifecycle</em> methods.
 * </p>
 *
 * <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>
 *
 * @param <T> The result type, like {@link File}.
 * @since 1.3
 * @deprecated Apache Commons IO no longer uses this class. Instead, use
 *             {@link PathUtils#walk(java.nio.file.Path, org.apache.commons.io.file.PathFilter, int, boolean, java.nio.file.FileVisitOption...)}
 *             or {@link Files#walkFileTree(java.nio.file.Path, java.util.Set, int, java.nio.file.FileVisitor)}, and
 *             friends.
 */
@Deprecated
public abstract class DirectoryWalker<T> {

    /**
     * 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 final File file;
        /** The file depth when the exception was thrown. */
        private final int depth;

        /**
         * Constructs a {@link CancelException} 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(final File file, final int depth) {
            this("Operation Cancelled", file, depth);
        }

        /**
         * Constructs a {@link CancelException} 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(final String message, final File file, final int depth) {
            super(message);
            this.file = file;
            this.depth = depth;
        }

        /**
         * Returns the depth when the operation was cancelled.
         *
         * @return the depth when the operation was cancelled
         */
        public int getDepth() {
            return depth;
        }

        /**
         * Returns the file when the operation was cancelled.
         *
         * @return the file when the operation was cancelled
         */
        public File getFile() {
            return file;
        }
    }
    /**
     * 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;

    /**
     * Constructs an instance with no filtering and unlimited <em>depth</em>.
     */
    protected DirectoryWalker() {
        this(null, -1);
    }

    /**
     * Constructs an instance with a filter and limit the <em>depth</em> 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} filter means that no
     * filtering should occur and all files and directories will be visited.
     * </p>
     *
     * @param filter  the filter to apply, null means visit all files
     * @param depthLimit  controls how <em>deep</em> the hierarchy is
     *  navigated to (less than 0 means unlimited)
     */
    protected DirectoryWalker(final FileFilter filter, final int depthLimit) {
        this.filter = filter;
        this.depthLimit = depthLimit;
    }

    /**
     * Constructs an instance with a directory and a file filter and an optional
     * limit on the <em>depth</em> 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} filter means that no filtering should occur.
     * </p>
     *
     * @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 <em>deep</em> the hierarchy is
     *  navigated to (less than 0 means unlimited)
     */
    protected DirectoryWalker(IOFileFilter directoryFilter, IOFileFilter fileFilter, final 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 = directoryFilter.or(fileFilter);
        }
        this.depthLimit = depthLimit;
    }

    /**
     * Checks whether the walk has been cancelled by calling {@link #handleIsCancelled},
     * throwing a {@link CancelException} 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.
     * </p>
     *
     * @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(final File file, final int depth, final Collection<T> results) throws
            IOException {
        if (handleIsCancelled(file, depth, results)) {
            throw new CancelException(file, depth);
        }
    }

    /**
     * Overridable callback method invoked with the contents of each directory.
     * <p>
     * This implementation returns the files unchanged
     * </p>
     *
     * @param directory  the current directory being processed
     * @param depth  the current directory level (starting directory = 0)
     * @param files the files (possibly filtered) in the directory, may be {@code null}
     * @return the filtered list of files
     * @throws IOException if an I/O Error occurs
     * @since 2.0
     */
    @SuppressWarnings("unused") // Possibly thrown from subclasses.
    protected File[] filterDirectoryContents(final File directory, final int depth, final File... files) throws
            IOException {
        return files;
    }

    /**
     * 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}.
     * </p>
     *
     * @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(final File startDirectory, final Collection<T> results,
                       final CancelException cancel) throws IOException {
        // re-throw exception - overridable by subclass
        throw cancel;
    }

    /**
     * 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>
     * <p>
     * This implementation does nothing and returns true.
     * </p>
     *
     * @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
     */
    @SuppressWarnings("unused") // Possibly thrown from subclasses.
    protected boolean handleDirectory(final File directory, final int depth, final Collection<T> results) throws
            IOException {
        // do nothing - overridable by subclass
        return true;  // process directory
    }

    /**
     * Overridable callback method invoked at the end of processing each directory.
     * <p>
     * This implementation does nothing.
     * </p>
     *
     * @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
     */
    @SuppressWarnings("unused") // Possibly thrown from subclasses.
    protected void handleDirectoryEnd(final File directory, final int depth, final Collection<T> results) throws
            IOException {
        // do nothing - overridable by subclass
    }

    /**
     * Overridable callback method invoked at the start of processing each directory.
     * <p>
     * This implementation does nothing.
     * </p>
     *
     * @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
     */
    @SuppressWarnings("unused") // Possibly thrown from subclasses.
    protected void handleDirectoryStart(final File directory, final int depth, final Collection<T> results) throws
            IOException {
        // do nothing - overridable by subclass
    }

    /**
     * Overridable callback method invoked at the end of processing.
     * <p>
     * This implementation does nothing.
     * </p>
     *
     * @param results  the collection of result objects, may be updated
     * @throws IOException if an I/O Error occurs
     */
    @SuppressWarnings("unused") // Possibly thrown from subclasses.
    protected void handleEnd(final Collection<T> results) throws IOException {
        // do nothing - overridable by subclass
    }

    /**
     * Overridable callback method invoked for each (non-directory) file.
     * <p>
     * This implementation does nothing.
     * </p>
     *
     * @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
     */
    @SuppressWarnings("unused") // Possibly thrown from subclasses.
    protected void handleFile(final File file, final int depth, final Collection<T> results) throws IOException {
        // do nothing - overridable by subclass
    }

    /**
     * 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()} method available from another
     * thread. The design pattern for the subclass should be as follows:
     * </p>
     * <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>
     * <p>
     * This implementation returns false.
     * </p>
     *
     * @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
     */
    @SuppressWarnings("unused") // Possibly thrown from subclasses.
    protected boolean handleIsCancelled(
            final File file, final int depth, final Collection<T> results) throws IOException {
        // do nothing - overridable by subclass
        return false;  // not cancelled
    }

    /**
     * Overridable callback method invoked for each restricted directory.
     * <p>
     * This implementation does nothing.
     * </p>
     *
     * @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
     */
    @SuppressWarnings("unused") // Possibly thrown from subclasses.
    protected void handleRestricted(final File directory, final int depth, final Collection<T> results) throws
            IOException {
        // do nothing - overridable by subclass
    }

    /**
     * Overridable callback method invoked at the start of processing.
     * <p>
     * This implementation does nothing.
     * </p>
     *
     * @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
     */
    @SuppressWarnings("unused") // Possibly thrown from subclasses.
    protected void handleStart(final File startDirectory, final Collection<T> results) throws IOException {
        // do nothing - overridable by subclass
    }

    /**
     * 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>
     * <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}.
     * </p>
     *
     * @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(final File startDirectory, final Collection<T> results) throws IOException {
        Objects.requireNonNull(startDirectory, "startDirectory");
        try {
            handleStart(startDirectory, results);
            walk(startDirectory, 0, results);
            handleEnd(results);
        } catch (final 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(final File directory, final int depth, final Collection<T> results) throws IOException {
        checkIfCancelled(directory, depth, results);
        if (handleDirectory(directory, depth, results)) {
            handleDirectoryStart(directory, depth, results);
            final int childDepth = depth + 1;
            if (depthLimit < 0 || childDepth <= depthLimit) {
                checkIfCancelled(directory, depth, results);
                File[] childFiles = directory.listFiles(filter);
                childFiles = filterDirectoryContents(directory, depth, childFiles);
                if (childFiles == null) {
                    handleRestricted(directory, childDepth, results);
                } else {
                    for (final File childFile : childFiles) {
                        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);
    }
}