/*
* 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.sshd.common.util;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* Class for scanning a directory for files/directories which match certain
* criteria.
* <p/>
* These criteria consist of selectors and patterns which have been specified.
* With the selectors you can select which files you want to have included.
* Files which are not selected are excluded. With patterns you can include
* or exclude files based on their filename.
* <p/>
* The idea is simple. A given directory is recursively scanned for all files
* and directories. Each file/directory is matched against a set of selectors,
* including special support for matching against filenames with include and
* and exclude patterns. Only files/directories which match at least one
* pattern of the include pattern list or other file selector, and don't match
* any pattern of the exclude pattern list or fail to match against a required
* selector will be placed in the list of files/directories found.
* <p/>
* When no list of include patterns is supplied, "**" will be used, which
* means that everything will be matched. When no list of exclude patterns is
* supplied, an empty list is used, such that nothing will be excluded. When
* no selectors are supplied, none are applied.
* <p/>
* The filename pattern matching is done as follows:
* The name to be matched is split up in path segments. A path segment is the
* name of a directory or file, which is bounded by
* <code>File.separator</code> ('/' under UNIX, '\' under Windows).
* For example, "abc/def/ghi/xyz.java" is split up in the segments "abc",
* "def","ghi" and "xyz.java".
* The same is done for the pattern against which should be matched.
* <p/>
* The segments of the name and the pattern are then matched against each
* other. When '**' is used for a path segment in the pattern, it matches
* zero or more path segments of the name.
* <p/>
* There is a special case regarding the use of <code>File.separator</code>s
* at the beginning of the pattern and the string to match:<br>
* When a pattern starts with a <code>File.separator</code>, the string
* to match must also start with a <code>File.separator</code>.
* When a pattern does not start with a <code>File.separator</code>, the
* string to match may not start with a <code>File.separator</code>.
* When one of these rules is not obeyed, the string will not
* match.
* <p/>
* When a name path segment is matched against a pattern path segment, the
* following special characters can be used:<br>
* '*' matches zero or more characters<br>
* '?' matches one character.
* <p/>
* Examples:
* <p/>
* "**\*.class" matches all .class files/dirs in a directory tree.
* <p/>
* "test\a??.java" matches all files/dirs which start with an 'a', then two
* more characters and then ".java", in a directory called test.
* <p/>
* "**" matches everything in a directory tree.
* <p/>
* "**\test\**\XYZ*" matches all files/dirs which start with "XYZ" and where
* there is a parent directory called test (e.g. "abc\test\def\ghi\XYZ123").
* <p/>
* Case sensitivity may be turned off if necessary. By default, it is
* turned on.
* <p/>
* Example of usage:
* <pre>
* String[] includes = {"**\\*.class"};
* String[] excludes = {"modules\\*\\**"};
* ds.setIncludes(includes);
* ds.setExcludes(excludes);
* ds.setBasedir(new File("test"));
* ds.setCaseSensitive(true);
* ds.scan();
* <p/>
* System.out.println("FILES:");
* String[] files = ds.getIncludedFiles();
* for (int i = 0; i < files.length; i++) {
* System.out.println(files[i]);
* }
* </pre>
* This will scan a directory called test for .class files, but excludes all
* files in all proper subdirectories of a directory called "modules"
*
* @author Arnout J. Kuiper
* <a href="mailto:ajkuiper@wxs.nl">ajkuiper@wxs.nl</a>
* @author Magesh Umasankar
* @author <a href="mailto:bruce@callenish.com">Bruce Atherton</a>
* @author <a href="mailto:levylambert@tiscali-dsl.de">Antoine Levy-Lambert</a>
*/
public class DirectoryScanner {
/**
* The base directory to be scanned.
*/
protected File basedir;
/**
* The patterns for the files to be included.
*/
protected String[] includes;
/**
* The files which matched at least one include and no excludes
* and were selected.
*/
protected List<String> filesIncluded;
/**
* Whether or not the file system should be treated as a case sensitive
* one.
*/
protected boolean isCaseSensitive = true;
public DirectoryScanner() {
}
public DirectoryScanner(String basedir, String... includes) {
setBasedir(basedir);
setIncludes(includes);
}
/**
* Sets the base directory to be scanned. This is the directory which is
* scanned recursively. All '/' and '\' characters are replaced by
* <code>File.separatorChar</code>, so the separator used need not match
* <code>File.separatorChar</code>.
*
* @param basedir The base directory to scan.
* Must not be <code>null</code>.
*/
public void setBasedir(String basedir) {
setBasedir(new File(basedir.replace('/', File.separatorChar).replace(
'\\', File.separatorChar)));
}
/**
* Sets the base directory to be scanned. This is the directory which is
* scanned recursively.
*
* @param basedir The base directory for scanning.
* Should not be <code>null</code>.
*/
public void setBasedir(File basedir) {
this.basedir = basedir;
}
/**
* Returns the base directory to be scanned.
* This is the directory which is scanned recursively.
*
* @return the base directory to be scanned
*/
public File getBasedir() {
return basedir;
}
/**
* Sets the list of include patterns to use. All '/' and '\' characters
* are replaced by <code>File.separatorChar</code>, so the separator used
* need not match <code>File.separatorChar</code>.
* <p/>
* When a pattern ends with a '/' or '\', "**" is appended.
*
* @param includes A list of include patterns.
* May be <code>null</code>, indicating that all files
* should be included. If a non-<code>null</code>
* list is given, all elements must be
* non-<code>null</code>.
*/
public void setIncludes(String[] includes) {
if (includes == null) {
this.includes = null;
} else {
this.includes = new String[includes.length];
for (int i = 0; i < includes.length; i++) {
this.includes[i] = normalizePattern(includes[i]);
}
}
}
/**
* Scans the base directory for files which match at least one include
* pattern and don't match any exclude patterns. If there are selectors
* then the files must pass muster there, as well.
*
* @throws IllegalStateException if the base directory was set
* incorrectly (i.e. if it is <code>null</code>, doesn't exist,
* or isn't a directory).
*/
public String[] scan() throws IllegalStateException {
if (basedir == null) {
throw new IllegalStateException("No basedir set");
}
if (!basedir.exists()) {
throw new IllegalStateException("basedir " + basedir
+ " does not exist");
}
if (!basedir.isDirectory()) {
throw new IllegalStateException("basedir " + basedir
+ " is not a directory");
}
if (includes == null || includes.length == 0) {
throw new IllegalStateException("No includes set ");
}
filesIncluded = new ArrayList();
scandir(basedir, "");
return getIncludedFiles();
}
/**
* Scans the given directory for files and directories. Found files and
* directories are placed in their respective collections, based on the
* matching of includes, excludes, and the selectors. When a directory
* is found, it is scanned recursively.
*
* @param dir The directory to scan. Must not be <code>null</code>.
* @param vpath The path relative to the base directory (needed to
* prevent problems with an absolute path when using
* dir). Must not be <code>null</code>.
* @throws IOException
*/
protected void scandir(File dir, String vpath) {
String[] newfiles = dir.list();
if (newfiles == null) {
newfiles = new String[0];
}
for (int i = 0; i < newfiles.length; i++) {
String name = vpath + newfiles[i];
File file = new File(dir, newfiles[i]);
if (file.isDirectory()) {
if (isIncluded(name)) {
filesIncluded.add(name);
scandir(file, name + File.separator);
} else if (couldHoldIncluded(name)) {
scandir(file, name + File.separator);
}
} else if (file.isFile()) {
if (isIncluded(name)) {
filesIncluded.add(name);
}
}
}
}
/**
* Returns the names of the files which matched at least one of the
* include patterns and none of the exclude patterns.
* The names are relative to the base directory.
*
* @return the names of the files which matched at least one of the
* include patterns and none of the exclude patterns.
*/
public String[] getIncludedFiles() {
String[] files = new String[filesIncluded.size()];
filesIncluded.toArray(files);
return files;
}
/**
* Tests whether or not a name matches against at least one include
* pattern.
*
* @param name The name to match. Must not be <code>null</code>.
* @return <code>true</code> when the name matches against at least one
* include pattern, or <code>false</code> otherwise.
*/
protected boolean isIncluded(String name) {
for (int i = 0; i < includes.length; i++) {
if (SelectorUtils.matchPath(includes[i], name, isCaseSensitive)) {
return true;
}
}
return false;
}
/**
* Tests whether or not a name matches the start of at least one include
* pattern.
*
* @param name The name to match. Must not be <code>null</code>.
* @return <code>true</code> when the name matches against the start of at
* least one include pattern, or <code>false</code> otherwise.
*/
protected boolean couldHoldIncluded(String name) {
for (int i = 0; i < includes.length; i++) {
if (SelectorUtils.matchPatternStart(includes[i], name, isCaseSensitive)) {
return true;
}
}
return false;
}
/**
* Normalizes the pattern, e.g. converts forward and backward slashes to the platform-specific file separator.
*
* @param pattern The pattern to normalize, must not be <code>null</code>.
* @return The normalized pattern, never <code>null</code>.
*/
private String normalizePattern(String pattern) {
pattern = pattern.trim();
if (pattern.startsWith(SelectorUtils.REGEX_HANDLER_PREFIX)) {
if (File.separatorChar == '\\') {
pattern = replace(pattern, "/", "\\\\", -1);
} else {
pattern = replace(pattern, "\\\\", "/", -1);
}
} else {
pattern = pattern.replace(File.separatorChar == '/' ? '\\' : '/', File.separatorChar);
if (pattern.endsWith(File.separator)) {
pattern += "**";
}
}
return pattern;
}
/**
* <p>Replace a String with another String inside a larger String,
* for the first <code>max</code> values of the search String.</p>
* <p/>
* <p>A <code>null</code> reference passed to this method is a no-op.</p>
*
* @param text text to search and replace in
* @param repl String to search for
* @param with String to replace with
* @param max maximum number of values to replace, or <code>-1</code> if no maximum
* @return the text with any replacements processed
*/
public static String replace(String text, String repl, String with, int max) {
if ((text == null) || (repl == null) || (with == null) || (repl.length() == 0)) {
return text;
}
StringBuffer buf = new StringBuffer(text.length());
int start = 0, end = 0;
while ((end = text.indexOf(repl, start)) != -1) {
buf.append(text.substring(start, end)).append(with);
start = end + repl.length();
if (--max == 0) {
break;
}
}
buf.append(text.substring(start));
return buf.toString();
}
}