/* * 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(); } }