This is an automated email from the ASF dual-hosted git repository. slachiewicz pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/maven-clean-plugin.git
commit d1ecf6e32cb638c498f3e8ffaa8e7921ad990424 Author: Martin Desruisseaux <[email protected]> AuthorDate: Tue Jul 8 16:11:05 2025 +0200 Replace `PathSelector` by a modified copy of the class in Maven core. The intend is to prepare the replacement by a shared implementation. --- .../org/apache/maven/plugins/clean/Cleaner.java | 11 +- .../org/apache/maven/plugins/clean/Fileset.java | 7 +- .../apache/maven/plugins/clean/PathSelector.java | 508 +++++++++++++++------ 3 files changed, 379 insertions(+), 147 deletions(-) diff --git a/src/main/java/org/apache/maven/plugins/clean/Cleaner.java b/src/main/java/org/apache/maven/plugins/clean/Cleaner.java index 6dd973f..2ad8276 100644 --- a/src/main/java/org/apache/maven/plugins/clean/Cleaner.java +++ b/src/main/java/org/apache/maven/plugins/clean/Cleaner.java @@ -32,6 +32,7 @@ import java.nio.file.attribute.DosFileAttributeView; import java.nio.file.attribute.PosixFileAttributeView; import java.nio.file.attribute.PosixFilePermission; import java.util.ArrayDeque; +import java.util.Arrays; import java.util.BitSet; import java.util.Deque; import java.util.EnumSet; @@ -105,6 +106,10 @@ final class Cleaner implements FileVisitor<Path> { @Nonnull private final String fastMode; + /** + * Combination of includes and excludes path matchers. + * A {@code null} value means to include everything. + */ @Nullable private PathSelector selector; @@ -205,7 +210,11 @@ final class Cleaner implements FileVisitor<Path> { * @throws IOException if a file/directory could not be deleted and {@code failOnError} is {@code true} */ public void delete(@Nonnull Fileset fileset) throws IOException { - selector = new PathSelector(fileset); + selector = new PathSelector( + fileset.getDirectory(), + Arrays.asList(fileset.getIncludes()), + Arrays.asList(fileset.getExcludes()), + fileset.isUseDefaultExcludes()); if (selector.isEmpty()) { selector = null; } diff --git a/src/main/java/org/apache/maven/plugins/clean/Fileset.java b/src/main/java/org/apache/maven/plugins/clean/Fileset.java index 2842ec6..de497f8 100644 --- a/src/main/java/org/apache/maven/plugins/clean/Fileset.java +++ b/src/main/java/org/apache/maven/plugins/clean/Fileset.java @@ -41,6 +41,11 @@ public class Fileset { private boolean useDefaultExcludes; + /** + * Creates an initially empty file set. + */ + public Fileset() {} + /** * {@return the base directory}. */ @@ -96,7 +101,7 @@ public class Fileset { * Appends the elements of the given array in the given buffer. * This is a helper method for {@link #toString()} implementations. * - * @param buffer the buffer where to add the elements + * @param buffer the buffer to add the elements to * @param label label identifying the array of elements to add * @param patterns the elements to append, or {@code null} if none */ diff --git a/src/main/java/org/apache/maven/plugins/clean/PathSelector.java b/src/main/java/org/apache/maven/plugins/clean/PathSelector.java index 0a80ba1..2d81082 100644 --- a/src/main/java/org/apache/maven/plugins/clean/PathSelector.java +++ b/src/main/java/org/apache/maven/plugins/clean/PathSelector.java @@ -22,19 +22,26 @@ import java.io.File; import java.nio.file.FileSystem; import java.nio.file.Path; import java.nio.file.PathMatcher; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; import java.util.Set; /** - * Determines whether a path is selected for deletion. + * Determines whether a path is selected according to include/exclude patterns. * The pathnames used for method parameters will be relative to some base directory - * and use {@code '/'} as separator, regardless the hosting operating system. + * and use {@code '/'} as separator, regardless of the hosting operating system. * - * <h4>Syntax</h4> - * If a pattern contains the {@code ':'} character, then that pattern is given verbatim to - * {@link FileSystem#getPathMatcher(String)}, which will interpret the part before {@code ':'} - * as the syntax (usually {@code "glob"} or {@code "regex"}). If a pattern does not contain the - * {@code ':'} character, then the syntax defaults to a reproduction of the Maven 3 behavior. + * <h2>Syntax</h2> + * If a pattern contains the {@code ':'} character and the prefix before is longer than 1 character, + * then that pattern is given verbatim to {@link FileSystem#getPathMatcher(String)}, which interprets + * the part before {@code ':'} as the syntax (usually {@code "glob"} or {@code "regex"}). + * If a pattern does not contain the {@code ':'} character, or if the prefix is one character long + * (interpreted as a Windows drive), then the syntax defaults to a reproduction of the Maven 3 behavior. * This is implemented as the {@code "glob"} syntax with the following modifications: * * <ul> @@ -43,13 +50,17 @@ import java.util.Set; * <li>Trailing {@code "/"} is completed as {@code "/**"}.</li> * <li>The {@code "**"} wildcard means "0 or more directories" instead of "1 or more directories". * This is implemented by adding variants of the pattern without the {@code "**"} wildcard.</li> + * <li>Bracket characters [ ] and { } are escaped.</li> + * <li>On Unix only, the escape character {@code '\\'} is itself escaped.</li> * </ul> * - * If above changes are not desired, put an explicit {@code "glob:"} prefix before the patterns. - * Note that putting such prefix is recommended anyway for better performances. + * If above changes are not desired, put an explicit {@code "glob:"} prefix before the pattern. + * Note that putting such a prefix is recommended anyway for better performances. * * @author Benjamin Bentmann * @author Martin Desruisseaux + * + * @see java.nio.file.FileSystem#getPathMatcher(String) */ final class PathSelector implements PathMatcher { /** @@ -58,93 +69,126 @@ final class PathSelector implements PathMatcher { * <p><b>Source:</b> this list is copied from {@code plexus-utils-4.0.2} (released in * September 23, 2024), class {@code org.codehaus.plexus.util.AbstractScanner}.</p> */ - private static final String[] DEFAULT_EXCLUDES = { - // Miscellaneous typical temporary files - "**/*~", - "**/#*#", - "**/.#*", - "**/%*%", - "**/._*", - - // CVS - "**/CVS", - "**/CVS/**", - "**/.cvsignore", - - // RCS - "**/RCS", - "**/RCS/**", - - // SCCS - "**/SCCS", - "**/SCCS/**", - - // Visual SourceSafe - "**/vssver.scc", - - // MKS - "**/project.pj", - - // Subversion - "**/.svn", - "**/.svn/**", - - // Arch - "**/.arch-ids", - "**/.arch-ids/**", - - // Bazaar - "**/.bzr", - "**/.bzr/**", - - // SurroundSCM - "**/.MySCMServerInfo", - - // Mac - "**/.DS_Store", - - // Serena Dimensions Version 10 - "**/.metadata", - "**/.metadata/**", - - // Mercurial - "**/.hg", - "**/.hg/**", - - // git - "**/.git", - "**/.git/**", - "**/.gitignore", - - // BitKeeper - "**/BitKeeper", - "**/BitKeeper/**", - "**/ChangeSet", - "**/ChangeSet/**", - - // darcs - "**/_darcs", - "**/_darcs/**", - "**/.darcsrepo", - "**/.darcsrepo/**", - "**/-darcs-backup*", - "**/.darcs-temp-mail" - }; + private static final List<String> DEFAULT_EXCLUDES = List.of( + // Miscellaneous typical temporary files + "**/*~", + "**/#*#", + "**/.#*", + "**/%*%", + "**/._*", + + // CVS + "**/CVS", + "**/CVS/**", + "**/.cvsignore", + + // RCS + "**/RCS", + "**/RCS/**", + + // SCCS + "**/SCCS", + "**/SCCS/**", + + // Visual SourceSafe + "**/vssver.scc", + + // MKS + "**/project.pj", + + // Subversion + "**/.svn", + "**/.svn/**", + + // Arch + "**/.arch-ids", + "**/.arch-ids/**", + + // Bazaar + "**/.bzr", + "**/.bzr/**", + + // SurroundSCM + "**/.MySCMServerInfo", + + // Mac + "**/.DS_Store", + + // Serena Dimensions Version 10 + "**/.metadata", + "**/.metadata/**", + + // Mercurial + "**/.hg", + "**/.hg/**", + + // git + "**/.git", + "**/.git/**", + "**/.gitignore", + + // BitKeeper + "**/BitKeeper", + "**/BitKeeper/**", + "**/ChangeSet", + "**/ChangeSet/**", + + // darcs + "**/_darcs", + "**/_darcs/**", + "**/.darcsrepo", + "**/.darcsrepo/**", + "**/-darcs-backup*", + "**/.darcs-temp-mail"); + + /** + * Maximum number of characters of the prefix before {@code ':'} for handling as a Maven syntax. + */ + private static final int MAVEN_SYNTAX_THRESHOLD = 1; /** - * String representation of the normalized include filters. - * This is kept only for {@link #toString()} implementation. + * The default syntax to use if none was specified. Note that when this default syntax is applied, + * the user-provided pattern get some changes as documented in class Javadoc. + */ + private static final String DEFAULT_SYNTAX = "glob:"; + + /** + * Characters having a special meaning in the glob syntax. + * + * @see FileSystem#getPathMatcher(String) + */ + private static final String SPECIAL_CHARACTERS = "*?[]{}\\"; + + /** + * A path matcher which accepts all files. + * + * @see #simplify() + */ + private static final PathMatcher INCLUDES_ALL = (path) -> true; + + /** + * String representations of the normalized include filters. + * Each pattern shall be prefixed by its syntax, which is {@value #DEFAULT_SYNTAX} by default. + * An empty array means to include all files. + * + * @see #toString() */ private final String[] includePatterns; /** - * String representation of the normalized exclude filters. - * This is kept only for {@link #toString()} implementation. + * String representations of the normalized exclude filters. + * Each pattern shall be prefixed by its syntax. If no syntax is specified, + * the default is a Maven 3 syntax similar, but not identical, to {@value #DEFAULT_SYNTAX}. + * This array may be longer or shorter than the user-supplied excludes, depending on whether + * default excludes have been added and whether some unnecessary excludes have been omitted. + * + * @see #toString() */ private final String[] excludePatterns; /** * The matcher for includes. The length of this array is equal to {@link #includePatterns} array length. + * An empty array means to include all files. */ private final PathMatcher[] includes; @@ -157,12 +201,13 @@ final class PathSelector implements PathMatcher { * The matcher for all directories to include. This array includes the parents of all those directories, * because they need to be accepted before we can walk to the sub-directories. * This is an optimization for skipping whole directories when possible. + * An empty array means to include all directories. */ private final PathMatcher[] dirIncludes; /** * The matcher for directories to exclude. This array does <em>not</em> include the parent directories, - * since they may contain other sub-trees that need to be included. + * because they may contain other sub-trees that need to be included. * This is an optimization for skipping whole directories when possible. */ private final PathMatcher[] dirExcludes; @@ -173,41 +218,184 @@ final class PathSelector implements PathMatcher { private final Path baseDirectory; /** - * Creates a new selector from the given file seT. + * Whether paths must be relativized before to be given to a matcher. If {@code true}, then every paths + * will be made relative to {@link #baseDirectory} for allowing patterns like {@code "foo/bar/*.java"} + * to work. As a slight optimization, we can skip this step if all patterns start with {@code "**"}. + */ + private final boolean needRelativize; + + /** + * Creates a new selector from the given includes and excludes. * - * @param fs the user-specified configuration + * @param directory the base directory of the files to filter + * @param includes the patterns of the files to include, or null or empty for including all files + * @param excludes the patterns of the files to exclude, or null or empty for no exclusion + * @param useDefaultExcludes whether to augment the excludes with a default set of <abbr>SCM</abbr> patterns */ - PathSelector(Fileset fs) { - includePatterns = normalizePatterns(fs.getIncludes(), false); - excludePatterns = normalizePatterns(addDefaultExcludes(fs.getExcludes(), fs.isUseDefaultExcludes()), true); - baseDirectory = fs.getDirectory(); - FileSystem system = baseDirectory.getFileSystem(); - includes = matchers(system, includePatterns); - excludes = matchers(system, excludePatterns); - dirIncludes = matchers(system, directoryPatterns(includePatterns, false)); - dirExcludes = matchers(system, directoryPatterns(excludePatterns, true)); + PathSelector(Path directory, Collection<String> includes, Collection<String> excludes, boolean useDefaultExcludes) { + includePatterns = normalizePatterns(includes, false); + excludePatterns = normalizePatterns(effectiveExcludes(excludes, includePatterns, useDefaultExcludes), true); + baseDirectory = directory; + FileSystem fs = directory.getFileSystem(); + this.includes = matchers(fs, includePatterns); + this.excludes = matchers(fs, excludePatterns); + dirIncludes = matchers(fs, directoryPatterns(includePatterns, false)); + dirExcludes = matchers(fs, directoryPatterns(excludePatterns, true)); + needRelativize = needRelativize(includePatterns) || needRelativize(excludePatterns); } /** - * Returns the given array of excludes, optionally expanded with a default set of excludes. + * Returns the given array of excludes, optionally expanded with a default set of excludes, + * then with unnecessary excludes omitted. An unnecessary exclude is an exclude which will never + * match a file because there is no include which would accept a file that could match the exclude. + * For example, if the only include is {@code "*.java"}, then the <code>"**/project.pj"</code>, + * <code>"**/.DS_Store"</code> and other excludes will never match a file and can be omitted. + * Because the list of {@linkplain #DEFAULT_EXCLUDES default excludes} contains many elements, + * removing unnecessary excludes can reduce a lot the number of matches tested on each source file. + * + * <h4>Implementation note</h4> + * The removal of unnecessary excludes is done on a best effort basis. The current implementation + * compares only the prefixes and suffixes of each pattern, keeping the pattern in case of doubt. + * This is not bad, but it does not remove all unnecessary patterns. It would be possible to do + * better in the future if benchmarking suggests that it would be worth the effort. * - * @param excludes the user-specified excludes. + * @param excludes the user-specified excludes, potentially not yet converted to glob syntax + * @param includes the include patterns converted to glob syntax * @param useDefaultExcludes whether to expand user exclude with the set of default excludes - * @return the potentially expanded set of excludes to use + * @return the potentially expanded or reduced set of excludes to use */ - private static String[] addDefaultExcludes(final String[] excludes, final boolean useDefaultExcludes) { - if (!useDefaultExcludes) { + private static Collection<String> effectiveExcludes( + Collection<String> excludes, final String[] includes, final boolean useDefaultExcludes) { + if (excludes == null || excludes.isEmpty()) { + if (useDefaultExcludes) { + excludes = new ArrayList<>(DEFAULT_EXCLUDES); + } else { + return List.of(); + } + } else { + excludes = new ArrayList<>(excludes); + excludes.removeIf(Objects::isNull); + if (useDefaultExcludes) { + excludes.addAll(DEFAULT_EXCLUDES); + } + } + if (includes.length == 0) { return excludes; } - String[] defaults = DEFAULT_EXCLUDES; - if (excludes == null || excludes.length == 0) { - return defaults; - } else { - String[] patterns = new String[excludes.length + defaults.length]; - System.arraycopy(excludes, 0, patterns, 0, excludes.length); - System.arraycopy(defaults, 0, patterns, excludes.length, defaults.length); - return patterns; + /* + * Get the prefixes and suffixes of all includes, stopping at the first special character. + * Redundant prefixes and suffixes are omitted. + */ + var prefixes = new String[includes.length]; + var suffixes = new String[includes.length]; + for (int i = 0; i < includes.length; i++) { + String include = includes[i]; + if (!include.startsWith(DEFAULT_SYNTAX)) { + return excludes; // Do not filter if at least one pattern is too complicated. + } + include = include.substring(DEFAULT_SYNTAX.length()); + prefixes[i] = prefixOrSuffix(include, false); + suffixes[i] = prefixOrSuffix(include, true); + } + prefixes = sortByLength(prefixes, false); + suffixes = sortByLength(suffixes, true); + /* + * Keep only the exclude which start with one of the prefixes and end with one of the suffixes. + * Note that a prefix or suffix may be the empty string, which match everything. + */ + final Iterator<String> it = excludes.iterator(); + nextExclude: + while (it.hasNext()) { + final String exclude = it.next(); + final int s = exclude.indexOf(':'); + if (s <= MAVEN_SYNTAX_THRESHOLD || exclude.startsWith(DEFAULT_SYNTAX)) { + if (cannotMatch(exclude, prefixes, false) || cannotMatch(exclude, suffixes, true)) { + it.remove(); + } + } + } + return excludes; + } + + /** + * Returns the maximal amount of ordinary characters at the beginning or end of the given pattern. + * The prefix or suffix stops at the first {@linkplain #SPECIAL_CHARACTERS special character}. + * + * @param include the pattern for which to get a prefix or suffix without special character + * @param suffix {@code false} if a prefix is desired, or {@code true} if a suffix is desired + */ + private static String prefixOrSuffix(final String include, boolean suffix) { + int s = suffix ? -1 : include.length(); + for (int i = SPECIAL_CHARACTERS.length(); --i >= 0; ) { + char c = SPECIAL_CHARACTERS.charAt(i); + if (suffix) { + s = Math.max(s, include.lastIndexOf(c)); + } else { + int p = include.indexOf(c); + if (p >= 0 && p < s) { + s = p; + } + } } + return suffix ? include.substring(s + 1) : include.substring(0, s); + } + + /** + * Returns {@code true} if the given exclude cannot match any include patterns. + * In case of doubt, returns {@code false}. + * + * @param exclude the exclude pattern to test + * @param fragments the prefixes or suffixes (fragments without special characters) of the includes + * @param suffix {@code false} if the specified fragments are prefixes, {@code true} if they are suffixes + * @return {@code true} if it is certain that the exclude pattern cannot match, or {@code false} in case of doubt + */ + private static boolean cannotMatch(String exclude, final String[] fragments, final boolean suffix) { + exclude = prefixOrSuffix(exclude, suffix); + for (String fragment : fragments) { + int fg = fragment.length(); + int ex = exclude.length(); + int length = Math.min(fg, ex); + if (suffix) { + fg -= length; + ex -= length; + } else { + fg = 0; + ex = 0; + } + if (exclude.regionMatches(ex, fragment, fg, length)) { + return false; + } + } + return true; + } + + /** + * Sorts the given patterns by their length. The main intent is to have the empty string first, + * while will cause the loops testing for prefixes and suffixes to stop almost immediately. + * Short prefixes or suffixes are also more likely to be matched. + * + * @param fragments the fragments to sort in-place + * @param suffix {@code false} if the specified fragments are prefixes, {@code true} if they are suffixes + * @return the given array, or a smaller array if some fragments were discarded because redundant + */ + private static String[] sortByLength(final String[] fragments, final boolean suffix) { + Arrays.sort(fragments, (s1, s2) -> s1.length() - s2.length()); + int count = 0; + /* + * Simplify the array of prefixes or suffixes by removing all redundant elements. + * An element is redundant if there is a shorter prefix or suffix with the same characters. + */ + nextBase: + for (String fragment : fragments) { + for (int i = count; --i >= 0; ) { + String base = fragments[i]; + if (suffix ? fragment.endsWith(base) : fragment.startsWith(base)) { + continue nextBase; // Skip this fragment + } + } + fragments[count++] = fragment; + } + return (fragments.length == count) ? fragments : Arrays.copyOf(fragments, count); } /** @@ -218,16 +406,15 @@ final class PathSelector implements PathMatcher { * @param excludes whether the patterns are exclude patterns * @return normalized patterns without null, empty or duplicated patterns */ - private static String[] normalizePatterns(final String[] patterns, final boolean excludes) { - if (patterns == null) { + private static String[] normalizePatterns(final Collection<String> patterns, final boolean excludes) { + if (patterns == null || patterns.isEmpty()) { return new String[0]; } // TODO: use `LinkedHashSet.newLinkedHashSet(int)` instead with JDK19. - final var normalized = new LinkedHashSet<String>(patterns.length); + final var normalized = new LinkedHashSet<String>(patterns.size()); for (String pattern : patterns) { if (pattern != null && !pattern.isEmpty()) { - final boolean useMavenSyntax = pattern.indexOf(':') < 0; - if (useMavenSyntax) { + if (pattern.indexOf(':') <= MAVEN_SYNTAX_THRESHOLD) { pattern = pattern.replace(File.separatorChar, '/'); if (pattern.endsWith("/")) { pattern += "**"; @@ -240,15 +427,20 @@ final class PathSelector implements PathMatcher { pattern = pattern.substring(3); } pattern = pattern.replace("/**/**/", "/**/"); - } - normalized.add(pattern); - /* - * If the pattern starts or ends with "**", Java GLOB expects a directory level at - * that location while Maven seems to consider that "**" can mean "no directory". - * Add another pattern for reproducing this effect. - */ - if (useMavenSyntax) { + pattern = pattern.replace("\\", "\\\\") + .replace("[", "\\[") + .replace("]", "\\]") + .replace("{", "\\{") + .replace("}", "\\}"); + normalized.add(DEFAULT_SYNTAX + pattern); + /* + * If the pattern starts or ends with "**", Java GLOB expects a directory level at + * that location while Maven seems to consider that "**" can mean "no directory". + * Add another pattern for reproducing this effect. + */ addPatternsWithOneDirRemoved(normalized, pattern, 0); + } else { + normalized.add(pattern); } } } @@ -261,7 +453,7 @@ final class PathSelector implements PathMatcher { * Tests suggest that we need an explicit GLOB pattern with no {@code "**"} for matching an absence of directory. * * @param patterns where to add the derived patterns - * @param pattern the pattern for which to add derived forms + * @param pattern the pattern for which to add derived forms, without the "glob:" syntax prefix * @param end should be 0 (reserved for recursive invocations of this method) */ private static void addPatternsWithOneDirRemoved(final Set<String> patterns, final String pattern, int end) { @@ -277,13 +469,11 @@ final class PathSelector implements PathMatcher { end++; // Ommit the leading slash if there is nothing before it. } } - if (start > 0) { - if (pattern.charAt(--start) != '/') { - continue; - } + if (start > 0 && pattern.charAt(--start) != '/') { + continue; } String reduced = pattern.substring(0, start) + pattern.substring(end); - patterns.add(reduced); + patterns.add(DEFAULT_SYNTAX + reduced); addPatternsWithOneDirRemoved(patterns, reduced, start); } } @@ -292,7 +482,7 @@ final class PathSelector implements PathMatcher { * Applies some heuristic rules for simplifying the set of patterns, * then returns the patterns as an array. * - * @param patterns the patterns to simplify and return asarray + * @param patterns the patterns to simplify and return as an array * @param excludes whether the patterns are exclude patterns * @return the set content as an array, after simplification */ @@ -322,13 +512,13 @@ final class PathSelector implements PathMatcher { // TODO: use `LinkedHashSet.newLinkedHashSet(int)` instead with JDK19. final var directories = new LinkedHashSet<String>(patterns.length); for (String pattern : patterns) { - int s = pattern.indexOf(':'); - if (s < 0 || pattern.startsWith("glob:")) { + if (pattern.startsWith(DEFAULT_SYNTAX)) { if (excludes) { if (pattern.endsWith("/**")) { directories.add(pattern.substring(0, pattern.length() - 3)); } } else { + int s = pattern.indexOf(':'); if (pattern.regionMatches(++s, "**/", 0, 3)) { s = pattern.indexOf('/', s + 3); if (s < 0) { @@ -342,18 +532,29 @@ final class PathSelector implements PathMatcher { return simplify(directories, excludes); } + /** + * Returns {@code true} if at least one pattern requires path to be relativized before to be matched. + * + * @param patterns include or exclude patterns + * @return whether at least one pattern require relativization + */ + private static boolean needRelativize(String[] patterns) { + for (String pattern : patterns) { + if (!pattern.startsWith(DEFAULT_SYNTAX + "**/")) { + return true; + } + } + return false; + } + /** * Creates the path matchers for the given patterns. - * If no syntax is specified, the default is {@code glob}. + * The syntax (usually {@value #DEFAULT_SYNTAX}) must be specified for each pattern. */ private static PathMatcher[] matchers(final FileSystem fs, final String[] patterns) { final var matchers = new PathMatcher[patterns.length]; for (int i = 0; i < patterns.length; i++) { - String pattern = patterns[i]; - if (pattern.indexOf(':') < 0) { - pattern = "glob:" + pattern; - } - matchers[i] = fs.getPathMatcher(pattern); + matchers[i] = fs.getPathMatcher(patterns[i]); } return matchers; } @@ -362,26 +563,44 @@ final class PathSelector implements PathMatcher { * {@return whether there is no include or exclude filters}. * In such case, this {@code PathSelector} instance should be ignored. */ - boolean isEmpty() { + public boolean isEmpty() { return (includes.length | excludes.length) == 0; } /** - * Determines whether a path is selected for deletion. + * {@return a potentially simpler matcher equivalent to this matcher}. + */ + @SuppressWarnings("checkstyle:MissingSwitchDefault") + public PathMatcher simplify() { + if (!needRelativize && excludes.length == 0) { + switch (includes.length) { + case 0: + return INCLUDES_ALL; + case 1: + return includes[0]; + } + } + return this; + } + + /** + * Determines whether a path is selected. * This is true if the given file matches an include pattern and no exclude pattern. * - * @param pathname The pathname to test, must not be {@code null} - * @return {@code true} if the given path is selected for deletion, {@code false} otherwise + * @param path the pathname to test, must not be {@code null} + * @return {@code true} if the given path is selected, {@code false} otherwise */ @Override public boolean matches(Path path) { - path = baseDirectory.relativize(path); + if (needRelativize) { + path = baseDirectory.relativize(path); + } return (includes.length == 0 || isMatched(path, includes)) && (excludes.length == 0 || !isMatched(path, excludes)); } /** - * {@return whether the given file matches according one of the given matchers}. + * {@return whether the given file matches according to one of the given matchers}. */ private static boolean isMatched(Path path, PathMatcher[] matchers) { for (PathMatcher matcher : matchers) { @@ -410,7 +629,6 @@ final class PathSelector implements PathMatcher { /** * {@return a string representation for logging purposes}. - * This string representation is reported logged when {@link Cleaner} is executed. */ @Override public String toString() {
