/*
* $Id$
*
* Copyright (C) 2003-2015 JNode.org
*
* This library is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published
* by the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
* License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this library; If not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package org.jnode.command.util;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import java.util.Stack;
import java.util.List;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import org.jnode.shell.PathnamePattern;
/**
* <p>
* <code>AbstractDirectoryWalker</code> - walk through a directory hierarchy
* recursively
* </p>
* <code>AbstractDirectoryWalker</code> will start at a given starting depth
* relatively to the given directory and walk recursively through the directory
* hierarchy until stopping depth is reached. <br>
* On its way, it will call "handleFile()" and "handleDir()" for every file and
* directory, that is not filtered out by any of the filters set for this
* DirectoryWalker.
*
* @author Alexander Kerner
* @author chris boertien
*/
public abstract class AbstractDirectoryWalker {
private static class FileObject {
final File file;
final Long depth;
FileObject(File file, Long depth) {
this.file = file;
this.depth = depth;
}
}
/**
* A FileFilter that filters based on matching a pathname glob pattern
*/
public static class PathnamePatternFilter implements FileFilter {
private Matcher matcher;
private boolean exclude;
/**
* Create the filter with the given pattern and orientation
*
* @param pattern the pathname glob pattern to match with
* @param exclude if true, reverse the sense of matching and
* exclude files that match.
*/
public PathnamePatternFilter(String pattern, boolean exclude) {
this.exclude = exclude;
this.matcher = PathnamePattern.compilePosixShellPattern(pattern, 0).matcher("");
}
@Override
public boolean accept(File file) {
return matcher.reset(file.getName()).matches() ^ exclude;
}
}
/**
* A FileFilter that filters based on matching a regular expression.
*/
public static class RegexPatternFilter implements FileFilter {
private Matcher matcher;
private boolean exclude;
/**
* Create the filter with the given pattern and orientation
*
* @param pattern the regular expression to match with
* @param exclude if true, reverse the sense of matching and
* exclude files that match the pattern.
*/
public RegexPatternFilter(String pattern, boolean exclude) {
this.exclude = exclude;
this.matcher = Pattern.compile(pattern).matcher("");
}
@Override
public boolean accept(File file) {
return matcher.reset(file.getName()).matches() ^ exclude;
}
}
/**
* A FileFilter that filters based on the file modification time.
*/
public static class ModTimeFilter implements FileFilter {
private long modTime;
private boolean newer;
/**
* Create the filter with the given mod time and direction
*
* @param time the time point to filter on
* @param newer if true, accept if the file mtime is > time, false
* accepts if the file mtime is <= to time.
*/
public ModTimeFilter(long time, boolean newer) {
this.modTime = time;
this.newer = newer;
}
@Override
public boolean accept(File file) {
return file.lastModified() == modTime || ((file.lastModified() < modTime) ^ newer);
}
}
/**
* A FileFilter that filters based on the file size.
*/
public static class SizeFilter implements FileFilter {
private long size;
private boolean greater;
/**
* Create the filter with the given size and direction.
*
* @param size the size point to filter on
* @param greater if true, accept if the file length is > size, false
* accepts if the file length is <= to size.
*/
public SizeFilter(long size, boolean greater) {
this.size = size;
this.greater = greater;
}
@Override
public boolean accept(File file) {
return file.length() == size || ((file.length() <= size) ^ greater);
}
}
private final Stack<FileObject> stack = new Stack<FileObject>();
private final Set<FileFilter> filters = new HashSet<FileFilter>();
private final Set<FileFilter> dirFilters = new HashSet<FileFilter>();
private volatile Long maxDepth = null;
private volatile Long minDepth = null;
private volatile boolean cancelled = false;
/**
* Walk the directory hierarchies of the given directories.
*
* Before walking begins on each of the given directories, the
* extending class has a chance to do some initialization through
* {@link #handleStartingDir(File)}. Once walking has commenced, each file
* will be checked against the current set of constraints, and
* passed to the extending class for further processing if accepted.
* When walking is complete for that branch, the {@code lastAction}
* method is called, and the walker moves on to the next directory,
* or returns if there are no more directories to walk.
*
* If an IOException propagates beyond the {@code walk} method, there
* is currently no way to resume walking. The following reasons may
* cause this to happen.
* <ul>
* <li>Any of the supplied directories are null, or not a directory.
* <li>A SecurityException was triggered, and the caller has not overriden
* the {@link #handleRestrictedFile(File)} method.
* </ul>
*
* @param dirs array of {@link java.io.File} to walk through.
* @throws IOException if any IO error occurs.
* @throws NullPointerException if dirs is null, or contains no directories
*/
public synchronized void walk(final File... dirs) throws IOException {
if (dirs == null || dirs.length == 0) {
throw new NullPointerException("Directory to walk from must not be null");
}
for (File dir : dirs) {
// perhaps this shouldn't fail like this, as it may
// be possible that this was simply due to a race condition
// with another process that has deleted the directory already
if (dir == null || !dir.isDirectory())
throw new IOException("No such directory " + dir);
/* See note in handleChilds()
dir = dir.getCanonicalPath();
*/
handleStartingDir(dir);
stack.push(new FileObject(dir, 0L));
while (!cancelled && !stack.isEmpty()) {
handle(stack.pop());
}
lastAction(cancelled);
// if this was canceled, we need to clear the stack
stack.clear();
}
}
public synchronized void walk(final List<File> dirs) throws IOException {
walk(dirs.toArray(new File[dirs.size() ]));
}
private void handle(final FileObject file) throws IOException {
if (minDepth != null && file.depth < minDepth) {
// out of boundaries
} else if (notFiltered(file.file)) {
handleFileOrDir(file);
} else {
// filtered out
}
try {
// Don't descend into directories beyond maxDepth
if (file.file.isDirectory() && (maxDepth == null || file.depth < maxDepth) && dirNotFiltered(file.file)) {
handleChildren(file);
}
} catch (SecurityException e) {
// Exception rises, when access to folder content was denied
handleRestrictedFile(file.file);
}
}
/**
* Add a directories contents to the stack.
*/
private void handleChildren(final FileObject file) throws IOException, SecurityException {
final Stack<File> stack = new Stack<File>();
final File[] content = file.file.listFiles();
if (content == null) {
// I/O Error or file
} else if (content.length == 0) {
// dir is empty
} else {
for (File f : content) {
/* I dont think is the right way to handle this. getCanonicalPath()
* does more than just trim symlinks, and symlinks aren't something
* we need to worry about. Even when we do we should have a lower
* level API to work with.
* - Chris
if (f.toString().equals(f.getCanonicalPath())) {
stack.push(f);
} else {
// dont follow symlinks
}
*/
stack.push(f);
}
while (!stack.isEmpty()) {
this.stack.push(new FileObject(stack.pop(), file.depth + 1));
}
}
}
/**
* Trigger the callbacks
*/
private void handleFileOrDir(final FileObject file) throws IOException {
if (file.file.isDirectory())
handleDir(file.file);
else if (file.file.isFile())
handleFile(file.file);
else {
handleSpecialFile(file.file);
}
}
/**
* Process a file through a set of file filters.
*
* This may be called from extending classes in order to bypass
* the regular walking procedure.
*
* As an example, if the caller simply wants to process specific
* files through the filter set, without setting up a full directory
* walk.
*
* @param file the file to check
* @return true if the file was accepted by all the filters, or if there
* were not filters.
*/
protected final boolean notFiltered(final File file) {
if (!filters.isEmpty())
for (FileFilter filter : filters)
if (!filter.accept(file))
return false;
return true;
}
/**
* Stop recursing if this is false.
*/
private boolean dirNotFiltered(File file) {
if (!dirFilters.isEmpty()) {
for (FileFilter filter : dirFilters) {
if (!filter.accept(file)) {
return false;
}
}
}
return true;
}
/**
* Stop walking the current directory hierarchy.
*
* This will not stop the walker altogether if there were multiple directories
* passed to {@code walk}. Instead, walking of the current directory hierarchy
* will stop, {@code lastAction(true)} is called, and the walker is reset with
* the next directory. If this it was the last directory, or there was only one
* then walk will return without error.
*/
public void stopWalking() {
cancelled = true;
}
/**
* The minimum depth level (exclusive) at which to begin handling files.
*
* The initial directory has a depth level of 0. Therefore if you set the
* minimum depth to 0, the initial directory will not handled, but its
* contents will.
*
* A negative value will be seen as null.
*
* @param min starting depth at which actual action performing is started.
*/
public void setMinDepth(Long min) {
if (min >= 0) {
minDepth = min;
}
}
/**
* The maximum depth level (inclusive) at which to stop handling files.
*
* When the walker reaches this level, it will not recurse any deeper
* into the file hierarchy. If the maximum depth is 0, the initial directory
* will be handled, but the walker will not query for its contents.
*
* A negative value will be seen as null.
*
* @param max ending depth at which actual action performing is stopped.
*/
public void setMaxDepth(Long max) {
if (max >= 0) {
maxDepth = max;
}
}
/**
* Add a FileFilter to this walker.
*
* Before the extended class is asked to handle a file or directory, it
* must be accepted by the set of filters supplied. If no filters are
* supplied, then every file and directory will be handled.
*
* @param filter a {@link FileFilter} to be added to this
* DirectoryWalker's FilterSet.
*/
public synchronized void addFilter(FileFilter filter) {
filters.add(filter);
}
/**
* Add a FileFilter to stop recursing of directories.
*
* Before recursing a directory, it must be accepted by the set of
* directory filters supplied. If no filters are supplied, then this
* will not prevent recursing of directories.
*
* @param filter {@link FileFilter} to be added
*/
public synchronized void addDirectoryFilter(FileFilter filter) {
dirFilters.add(filter);
}
/**
* Handle a file or directory that triggered a SecurityException.
* This method is called, when access to a file was denied.
* <p>
* The default implementation will raise an {@link IOException} instead of a
* {@link SecurityException}. May be overridden by extending classes to
* do something else.
*
* Because this method throws an IOException that will propagate beyond the
* walk method, if an application wishes to continue walking after encountering
* a SecurityException while accessing a file or directory, then it must override
* this and provide an implementation that does not throw an exception.
*
* @param file {@code File} object, to which access was restricted.
* @throws IOException in default implementation.
*/
protected void handleRestrictedFile(final File file) throws IOException {
throw new IOException("Permission denied for " + file);
}
/**
* Handle the initial directory of a tree.
*
* This method is called, when walking is about to start. It gets called
* for each directory that was initially supplied to the walk method.
*
* By default, it does nothing. May be overridden by extending classes to do
* something else.
*
* Override this to do some initialization before starting to walk each of the
* given directory roots.
*
* @param file {@code File} object, that represents starting dir.
* @throws IOException if IO error occurs.
*/
protected void handleStartingDir(final File file) throws IOException {
// do nothing by default
}
/**
* This method is called, when walking has finished.
* By default, it does nothing. May be overridden by extending classes to do something else.
* @param wasCancelled true, if directory walking was aborted.
*/
protected void lastAction(boolean wasCancelled){
// do nothing by default
}
/**
* Handle a directory.
*
* Override this to perform some operation on this directory.
*
* @param file {@code File} object, that represents current directory.
* @throws IOException if IO error occurs.
*/
public abstract void handleDir(final File file) throws IOException;
/**
* Handle a file.
*
* Override this to perform some operation on this file.
*
* @param file {@code File} object, that represents current file.
* @throws IOException if IO error occurs.
*/
public abstract void handleFile(final File file) throws IOException;
/**
* Handle a special file.
*
* Override this to perform some operation on this special file.
*
* @param file {@code File} object that represents a special file.
* @throws IOException if an IO error occurs.
*/
public void handleSpecialFile(File file) throws IOException {
// do nothing by default
}
}