blob: 3cb04e1980684241b754e9953875400b31ab102a [file] [log] [blame]
package org.apache.maven.archetype.common.util;
/*
* 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.
*/
import org.codehaus.plexus.util.SelectorUtils;
import org.codehaus.plexus.util.StringUtils;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
/**
* <p>Class for scanning a directory for files/directories which match certain criteria.</p>
*
* <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>
*
* <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>
*
* <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>
*
* <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>
*
* <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>
*
* <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>
*
* <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>
*
* <p>Examples:</p>
*
* <p>"**\*.class" matches all .class files/dirs in a directory tree.</p>
*
* <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>
*
* <p>"**" matches everything in a directory tree.</p>
*
* <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>
*
* <p>Case sensitivity may be turned off if necessary. By default, it is turned on.</p>
*
* <p>Example of usage:</p>
*
* <pre>
* String[] includes = {"**\\*.class"};
* String[] excludes = {"modules\\*\\**"};
* ds.setIncludes(includes);
* ds.setExcludes(excludes);
* ds.setBasedir(new File("test"));
* ds.setCaseSensitive(true);
* ds.scan();
*
* System.out.println("FILES:");
* String[] files = ds.getIncludedFiles();
* for (int i = 0; i &lt; files.length; i++) {
* System.out.println(files[i]);
* }
* </pre>
*
* <p>This will scan a directory called test for .class files, but excludes all files in all proper
* subdirectories of a directory called "modules"</p>
*
* <p>This class was stealed from rg.coudehaus.plexus.util.DirectoryScanner and adapted to search
* from a List&lt;String&gt;</p>
*
* @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 ListScanner
{
/**
* Patterns which should be excluded by default.
*
* @see #addDefaultExcludes()
*/
public static final String[] DEFAULTEXCLUDES =
{ // Miscellaneous typical temporary files
"**/*~", "**/#*#", "**/.#*", "**/%*%", "**/._*",
// CVS
"**/CVS", "**/CVS/**", "**/.cvsignore",
// SCCS
"**/SCCS", "**/SCCS/**",
// Visual SourceSafe
"**/vssver.scc",
// Subversion
"**/.svn", "**/.svn/**",
// Arch
"**/.arch-ids", "**/.arch-ids/**",
// Bazaar
"**/.bzr", "**/.bzr/**",
// GIT
"**/.git", "**/.git/**",
// Mercurial
"**/.hg", "**/.hg/**",
// SurroundSCM
"**/.MySCMServerInfo",
// Mac
"**/.DS_Store"
};
/** The base directory to be scanned. */
protected String basedir;
/** Whether or not everything tested so far has been included. */
protected boolean everythingIncluded = true;
/** The patterns for the files to be excluded. */
protected String[] excludes;
/** The patterns for the files to be included. */
protected String[] includes;
/** Whether or not the file system should be treated as a case sensitive one. */
protected boolean isCaseSensitive = true;
/** Sole constructor. */
public ListScanner()
{
}
public static String getDefaultExcludes()
{
return StringUtils.join( DEFAULTEXCLUDES, "," );
}
/**
* Tests whether or not a string matches against a pattern. The pattern may contain two special
* characters:<br>
* '*' means zero or more characters<br>
* '?' means one and only one character
*
* @param pattern The pattern to match against. Must not be <code>null</code>.
* @param str The string which must be matched against the pattern. Must not be <code>
* null</code>.
* @return <code>true</code> if the string matches against the pattern, or <code>false</code>
* otherwise.
*/
public static boolean match( String pattern, String str )
{
// default matches the SelectorUtils default
return match( pattern, str, true );
}
/**
* Tests whether or not a string matches against a pattern. The pattern may contain two special
* characters:<br>
* '*' means zero or more characters<br>
* '?' means one and only one character
*
* @param pattern The pattern to match against. Must not be <code>null</code>.
* @param str The string which must be matched against the pattern. Must not be
* <code>null</code>.
* @param isCaseSensitive Whether or not matching should be performed case sensitively.
* @return <code>true</code> if the string matches against the pattern, or <code>false</code>
* otherwise.
*/
protected static boolean match( String pattern, String str, boolean isCaseSensitive )
{
return SelectorUtils.match( pattern, str, isCaseSensitive );
}
/**
* Tests whether or not a given path matches a given pattern.
*
* @param pattern The pattern to match against. Must not be <code>null</code>.
* @param str The path to match, as a String. Must not be <code>null</code>.
* @return <code>true</code> if the pattern matches against the string, or <code>false</code>
* otherwise.
*/
protected static boolean matchPath( String pattern, String str )
{
// default matches the SelectorUtils default
return matchPath( pattern, str, true );
}
/**
* Tests whether or not a given path matches a given pattern.
*
* @param pattern The pattern to match against. Must not be <code>null</code>.
* @param str The path to match, as a String. Must not be <code>null</code>.
* @param isCaseSensitive Whether or not matching should be performed case sensitively.
* @return <code>true</code> if the pattern matches against the string, or <code>false</code>
* otherwise.
*/
protected static boolean matchPath( String pattern, String str, boolean isCaseSensitive )
{
return SelectorUtils.matchPath( PathUtils.convertPathForOS( pattern ), PathUtils.convertPathForOS( str ),
isCaseSensitive );
}
/**
* <p>Tests whether or not a given path matches the start of a given pattern up to the first "**".<p>
*
* <p>This is not a general purpose test and should only be used if you can live with false
* positives. For example, <code>pattern=**\a</code> and <code>str=b</code> will yield <code>
* true</code>.</p>
*
* @param pattern The pattern to match against. Must not be <code>null</code>.
* @param str The path to match, as a String. Must not be <code>null</code>.
* @return whether or not a given path matches the start of a given pattern up to the first
* "**".
*/
protected static boolean matchPatternStart( String pattern, String str )
{
// default matches SelectorUtils default
return matchPatternStart( pattern, str, true );
}
/**
* <p>Tests whether or not a given path matches the start of a given pattern up to the first "**".</p>
*
* <p>This is not a general purpose test and should only be used if you can live with false
* positives. For example, <code>pattern=**\a</code> and <code>str=b</code> will yield <code>
* true</code>.</p>
*
* @param pattern The pattern to match against. Must not be <code>null</code>.
* @param str The path to match, as a String. Must not be <code>null</code>.
* @param isCaseSensitive Whether or not matching should be performed case sensitively.
* @return whether or not a given path matches the start of a given pattern up to the first
* "**".
*/
protected static boolean matchPatternStart( String pattern, String str, boolean isCaseSensitive )
{
return SelectorUtils.matchPatternStart( PathUtils.convertPathForOS( pattern ),
PathUtils.convertPathForOS( str ), isCaseSensitive );
}
/** Adds default exclusions to the current exclusions set. */
public void addDefaultExcludes()
{
int excludesLength = ( excludes == null ) ? 0 : excludes.length;
String[] newExcludes;
newExcludes = new String[excludesLength + DEFAULTEXCLUDES.length];
if ( excludesLength > 0 )
{
System.arraycopy( excludes, 0, newExcludes, 0, excludesLength );
}
for ( int i = 0; i < DEFAULTEXCLUDES.length; i++ )
{
newExcludes[i + excludesLength] =
DEFAULTEXCLUDES[i].replace( '/', File.separatorChar ).replace( '\\', File.separatorChar );
}
excludes = newExcludes;
}
/**
* Returns the base directory to be scanned. This is the directory which is scanned recursively.
*
* @return the base directory to be scanned
*/
public String getBasedir()
{
return basedir;
}
/**
* Sets the base directory to be scanned. This is the directory which is scanned recursively.
* This directory is normalized for multiple os's (all / and \\ are replaced with
* File.separatorChar
*
* @param basedir The base directory for scanning. Should not be <code>null</code>.
*/
public void setBasedir( String basedir )
{
this.basedir = basedir;
}
/**
* Sets whether or not the file system should be regarded as case sensitive.
*
* @param isCaseSensitive whether or not the file system should be regarded as a case
* sensitive one
*/
public void setCaseSensitive( boolean isCaseSensitive )
{
this.isCaseSensitive = isCaseSensitive;
}
/**
* <p>Sets the list of exclude 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>
*
* <p>When a pattern ends with a '/' or '\', "**" is appended.</p>
*
* @param excludesList A list of exclude patterns. May be <code>null</code>, indicating that no
* files should be excluded. If a non-<code>null</code> list is given, all
* elements must be non-<code>null</code>.
*/
public void setExcludes( List<String> excludesList )
{
String[] excludes = excludesList.toArray( new String[excludesList.size()] );
if ( excludes == null )
{
this.excludes = null;
}
else
{
setExcludes( excludes );
}
}
public void setExcludes( String excludes )
{
if ( excludes == null )
{
this.excludes = null;
}
else
{
setExcludes( StringUtils.split( excludes, "," ) );
}
}
/**
* <p>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>
*
* <p>When a pattern ends with a '/' or '\', "**" is appended.</p>
*
* @param includesList 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( List<String> includesList )
{
String[] includes = includesList.toArray( new String[includesList.size()] );
if ( includes == null )
{
this.includes = null;
}
else
{
setIncludes( includes );
}
}
public void setIncludes( String includes )
{
if ( includes == null )
{
this.includes = null;
}
else
{
setIncludes( StringUtils.split( includes, "," ) );
}
}
/**
* 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 List<String> scan( List<String> files )
throws
IllegalStateException
{
// System.err.println("Scanning \nbasedir="+basedir +
// " \nincludes=" + java.util.Arrays.toString(includes) +
// " \nexcludes=" + java.util.Arrays.toString(excludes) +
// " \non files="+files);
if ( basedir == null )
{
throw new IllegalStateException( "No basedir set" );
}
if ( includes == null )
{
// No includes supplied, so set it to 'matches all'
includes = new String[1];
includes[0] = "**";
}
if ( excludes == null )
{
excludes = new String[0];
}
List<String> result = new ArrayList<>();
for ( String fileName : files )
{
if ( isIncluded( fileName ) && !isExcluded( fileName ) )
{
result.add( fileName );
}
}
// System.err.println("Result "+result+"\n\n\n");
return result;
}
/**
* Tests whether or not a name matches against at least one exclude 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 exclude pattern, or
* <code>false</code> otherwise.
*/
protected boolean isExcluded( String name )
{
return matchesPatterns( name, excludes );
}
/**
* 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 )
{
return matchesPatterns( name, includes );
}
/**
* 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>.
* @param patterns The list of patterns to match.
* @return <code>true</code> when the name matches against at least one include pattern, or
* <code>false</code> otherwise.
*/
protected boolean matchesPatterns( String name, String[] patterns )
{
// avoid extra object creation in the loop
String path = null;
String baseDir = getBasedir();
if ( baseDir.length() > 0 )
{
baseDir = baseDir.concat( File.separator );
}
for ( int i = 0; i < patterns.length; i++ )
{
path = PathUtils.convertPathForOS( baseDir + patterns[i] );
// System.err.println("path="+path);
if ( matchPath( path, name, isCaseSensitive ) )
{
return true;
}
}
return false;
}
private void setExcludes( String[] excludes )
{
this.excludes = setPatterns( excludes );
}
private void setIncludes( String[] includes )
{
this.includes = setPatterns( includes );
}
private String[] setPatterns( String[] patterns )
{
String[] result = null;
if ( ( patterns != null ) && ( patterns.length > 0 ) )
{
result = new String[patterns.length];
for ( int i = 0; i < patterns.length; i++ )
{
String pattern = patterns[i].trim();
// don't normalize the pattern here, we internalize the normalization
// just normalize for comparison purposes
if ( PathUtils.convertPathForOS( pattern ).endsWith( File.separator ) )
{
pattern += "**";
}
result[i] = pattern;
}
}
return result;
}
}