true if verbose output is enabled, false otherwise.
*/
private boolean isVerbose() {
- return (verbose != null) ? verbose : getLog().isDebugEnabled();
+ return (verbose != null) ? verbose : logger.isDebugEnabled();
}
/**
@@ -307,8 +292,4 @@ private Path[] getDirectories() {
}
return directories;
}
-
- private Log getLog() {
- return logger;
- }
}
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 f19a9b5..0a399ac 100644
--- a/src/main/java/org/apache/maven/plugins/clean/Cleaner.java
+++ b/src/main/java/org/apache/maven/plugins/clean/Cleaner.java
@@ -20,15 +20,17 @@
import java.io.File;
import java.io.IOException;
+import java.nio.file.FileVisitOption;
+import java.nio.file.FileVisitResult;
+import java.nio.file.FileVisitor;
import java.nio.file.Files;
-import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayDeque;
+import java.util.BitSet;
import java.util.Deque;
-import java.util.function.BiConsumer;
-import java.util.function.Consumer;
+import java.util.EnumSet;
import java.util.stream.Stream;
import org.apache.maven.api.Event;
@@ -36,121 +38,175 @@
import org.apache.maven.api.Listener;
import org.apache.maven.api.Session;
import org.apache.maven.api.SessionData;
+import org.apache.maven.api.annotations.Nonnull;
+import org.apache.maven.api.annotations.Nullable;
import org.apache.maven.api.plugin.Log;
-import org.codehaus.plexus.util.Os;
-
-import static org.apache.maven.plugins.clean.CleanMojo.FAST_MODE_BACKGROUND;
-import static org.apache.maven.plugins.clean.CleanMojo.FAST_MODE_DEFER;
/**
* Cleans directories.
*
* @author Benjamin Bentmann
+ * @author Martin Desruisseaux
*/
-class Cleaner {
-
- private static final boolean ON_WINDOWS = Os.isFamily(Os.FAMILY_WINDOWS);
+final class Cleaner implements FileVisitornull to disable logging
+ * @param logger the logger to use
* @param verbose whether to perform verbose logging
* @param fastDir the explicit configured directory or to be deleted in fast mode
* @param fastMode the fast deletion mode
+ * @param followSymlinks whether to follow symlinks
+ * @param failOnError whether to abort with an exception in case a selected file/directory could not be deleted
+ * @param retryOnError whether to undertake additional delete attempts in case the first attempt failed
*/
- Cleaner(Session session, Log log, boolean verbose, Path fastDir, String fastMode) {
- logDebug = (log == null || !log.isDebugEnabled()) ? null : logger(log::debug, log::debug);
-
- logInfo = (log == null || !log.isInfoEnabled()) ? null : logger(log::info, log::info);
-
- logWarn = (log == null || !log.isWarnEnabled()) ? null : logger(log::warn, log::warn);
-
- logVerbose = verbose ? logInfo : logDebug;
-
+ @SuppressWarnings("checkstyle:ParameterNumber")
+ Cleaner(
+ @Nonnull Session session,
+ @Nonnull Log logger,
+ boolean verbose,
+ @Nonnull Path fastDir,
+ @Nonnull String fastMode,
+ boolean followSymlinks,
+ boolean failOnError,
+ boolean retryOnError) {
this.session = session;
+ this.logger = logger;
+ this.verbose = verbose;
this.fastDir = fastDir;
this.fastMode = fastMode;
+ this.followSymlinks = followSymlinks;
+ this.failOnError = failOnError;
+ this.retryOnError = retryOnError;
+ listDeletedFiles = verbose ? logger.isInfoEnabled() : logger.isDebugEnabled();
+ nonEmptyDirectoryLevels = new BitSet();
}
- private Logger logger(Consumernull. Non-existing directories will be silently
- * ignored
- * @param selector the selector used to determine what contents to delete, may be null to delete
- * everything
- * @param followSymlinks whether to follow symlinks
- * @param failOnError whether to abort with an exception in case a selected file/directory could not be deleted
- * @param retryOnError whether to undertake additional delete attempts in case the first attempt failed
- * @throws IOException if a file/directory could not be deleted and failOnError is true
+ * @param basedir the directory to delete, must not be {@code null}
+ * @throws IOException if a file/directory could not be deleted and {@code failOnError} is {@code true}
*/
- public void delete(
- Path basedir, Selector selector, boolean followSymlinks, boolean failOnError, boolean retryOnError)
- throws IOException {
+ public void delete(@Nonnull Path basedir) throws IOException {
if (!Files.isDirectory(basedir)) {
- if (!Files.exists(basedir)) {
- if (logDebug != null) {
- logDebug.log("Skipping non-existing directory " + basedir);
+ if (Files.notExists(basedir)) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Skipping non-existing directory " + basedir);
}
return;
}
throw new IOException("Invalid base directory " + basedir);
}
-
- if (logInfo != null) {
- logInfo.log("Deleting " + basedir + (selector != null ? " (" + selector + ")" : ""));
+ if (logger.isInfoEnabled()) {
+ logger.info("Deleting " + basedir + (selector != null ? " (" + selector + ")" : ""));
+ }
+ var options = EnumSet.noneOf(FileVisitOption.class);
+ if (followSymlinks) {
+ options.add(FileVisitOption.FOLLOW_LINKS);
+ basedir = getCanonicalPath(basedir, null);
}
-
- Path file = followSymlinks ? basedir : getCanonicalPath(basedir);
-
if (selector == null && !followSymlinks && fastDir != null && session != null) {
// If anything wrong happens, we'll just use the usual deletion mechanism
- if (fastDelete(file)) {
+ if (fastDelete(basedir)) {
return;
}
}
-
- delete(file, "", selector, followSymlinks, failOnError, retryOnError);
+ Files.walkFileTree(basedir, options, Integer.MAX_VALUE, this);
}
private boolean fastDelete(Path baseDir) {
- Path fastDir = this.fastDir;
// Handle the case where we use ${maven.multiModuleProjectDirectory}/target/.clean for example
if (fastDir.toAbsolutePath().startsWith(baseDir.toAbsolutePath())) {
try {
@@ -167,9 +223,7 @@ private boolean fastDelete(Path baseDir) {
throw e;
}
} catch (IOException e) {
- if (logDebug != null) {
- logDebug.log("Unable to fast delete directory", e);
- }
+ logger.debug("Unable to fast delete directory", e);
return false;
}
}
@@ -179,15 +233,12 @@ private boolean fastDelete(Path baseDir) {
Files.createDirectories(fastDir);
}
} catch (IOException e) {
- if (logDebug != null) {
- logDebug.log(
- "Unable to fast delete directory as the path " + fastDir
- + " does not point to a directory or cannot be created",
- e);
- }
+ logger.debug(
+ "Unable to fast delete directory as the path " + fastDir
+ + " does not point to a directory or cannot be created",
+ e);
return false;
}
-
try {
Path tmpDir = Files.createTempDirectory(fastDir, "");
Path dstDir = tmpDir.resolve(baseDir.getFileName());
@@ -199,175 +250,198 @@ private boolean fastDelete(Path baseDir) {
BackgroundCleaner.delete(this, tmpDir, fastMode);
return true;
} catch (IOException e) {
- if (logDebug != null) {
- logDebug.log("Unable to fast delete directory", e);
- }
+ logger.debug("Unable to fast delete directory", e);
return false;
}
}
/**
- * Deletes the specified file or directory.
- *
- * @param file the file/directory to delete, must not be null. If followSymlinks is
- * false, it is assumed that the parent file is canonical
- * @param pathname the relative pathname of the file, using {@link File#separatorChar}, must not be
- * null
- * @param selector the selector used to determine what contents to delete, may be null to delete
- * everything
- * @param followSymlinks whether to follow symlinks
- * @param failOnError whether to abort with an exception in case a selected file/directory could not be deleted
- * @param retryOnError whether to undertake additional delete attempts in case the first attempt failed
- * @return The result of the cleaning, never null
- * @throws IOException if a file/directory could not be deleted and failOnError is true
+ * Invoked for a directory before entries in the directory are visited.
+ * Determines if the given directory should be scanned for files to delete.
*/
- private Result delete(
- Path file,
- String pathname,
- Selector selector,
- boolean followSymlinks,
- boolean failOnError,
- boolean retryOnError)
- throws IOException {
- Result result = new Result();
-
- boolean isDirectory = Files.isDirectory(file);
-
- if (isDirectory) {
- if (selector == null || selector.couldHoldSelected(pathname)) {
- final boolean isSymlink = isSymbolicLink(file);
- Path canonical = followSymlinks ? file : getCanonicalPath(file);
- if (followSymlinks || !isSymlink) {
- String prefix = !pathname.isEmpty() ? pathname + File.separatorChar : "";
- try (Streamnull
- * @param failOnError whether to abort with an exception if the file/directory could not be deleted
- * @param retryOnError whether to undertake additional delete attempts if the first attempt failed
- * @return 0 if the file was deleted, 1 otherwise
- * @throws IOException if a file/directory could not be deleted and failOnError is true
+ * @param file the file/directory to delete, must not be {@code null}
+ * @return whether the file has been deleted
+ * @throws IOException if a file/directory could not be deleted and {@code failOnError} is {@code true}
*/
- private int delete(Path file, boolean failOnError, boolean retryOnError) throws IOException {
- IOException failure = delete(file);
- if (failure != null) {
-
+ @SuppressWarnings("SleepWhileInLoop")
+ private boolean tryDelete(final Path file) throws IOException {
+ try {
+ return Files.deleteIfExists(file);
+ } catch (IOException failure) {
if (retryOnError) {
if (ON_WINDOWS) {
// try to release any locks held by non-closed files
System.gc();
}
-
- final int[] delays = {50, 250, 750};
- for (int delay : delays) {
+ for (int delay : new int[] {50, 250, 750}) {
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
- throw new IOException(e);
+ failure.addSuppressed(e);
+ throw failure;
}
- failure = delete(file);
- if (failure == null) {
- break;
+ try {
+ return Files.deleteIfExists(file);
+ } catch (IOException again) {
+ again.addSuppressed(failure);
+ failure = again;
}
}
}
-
- if (Files.exists(file)) {
- if (failOnError) {
- throw new IOException("Failed to delete " + file, failure);
- } else {
- if (logWarn != null) {
- logWarn.log("Failed to delete " + file, failure);
- }
- return 1;
- }
+ if (logger.isWarnEnabled()) {
+ logger.warn("Failed to delete " + file, failure);
}
+ failureCount++;
+ if (failOnError) {
+ throw failure;
+ }
+ return false;
}
-
- return 0;
}
- private static IOException delete(Path file) {
- try {
- Files.deleteIfExists(file);
- } catch (IOException e) {
- return e;
+ /**
+ * Reports that a file, directory or symbolic link has been deleted. This method should be invoked only
+ * when {@link #tryDelete(Path)} returned {@code true} and {@link #listDeletedFiles} is {@code true}.
+ *
+ * If {@code attrs} is {@code null}, then the file is assumed a directory. This arbitrary rule + * is an implementation convenience specific to the context in which we invoke this method.
+ */ + private void logDelete(final Path file, final BasicFileAttributes attrs) { + String message; + if (attrs == null || attrs.isDirectory()) { + message = "Deleted directory " + file; + } else if (attrs.isRegularFile()) { + message = "Deleted file " + file; + } else if (attrs.isSymbolicLink()) { + message = "Deleted dangling symlink " + file; + } else { + message = "Deleted " + file; } - return null; - } - - private static class Result { - - private int failures; - - private boolean excluded; - - public void update(Result result) { - failures += result.failures; - excluded |= result.excluded; + if (verbose) { + logger.info(message); + } else { + logger.debug(message); } } - private interface Logger { - - void log(CharSequence message); - - void log(CharSequence message, Throwable t); - } - private static class BackgroundCleaner extends Thread { private static BackgroundCleaner instance; @@ -409,13 +483,14 @@ private BackgroundCleaner(Cleaner cleaner, Path dir, String fastMode) { @Override public void run() { - while (true) { - Path basedir = pollNext(); - if (basedir == null) { - break; - } + var options = EnumSet.noneOf(FileVisitOption.class); + if (cleaner.followSymlinks) { + options.add(FileVisitOption.FOLLOW_LINKS); + } + Path basedir; + while ((basedir = pollNext()) != null) { try { - cleaner.delete(basedir, "", null, false, false, true); + Files.walkFileTree(basedir, options, Integer.MAX_VALUE, cleaner); } catch (IOException e) { // do not display errors } @@ -457,7 +532,7 @@ synchronized boolean doDelete(Path dir) { return false; } filesToDelete.add(dir); - if (status == NEW && FAST_MODE_BACKGROUND.equals(fastMode)) { + if (status == NEW && CleanMojo.FAST_MODE_BACKGROUND.equals(fastMode)) { status = RUNNING; notifyAll(); start(); @@ -486,11 +561,9 @@ synchronized void doSessionEnd() { if (status == NEW) { start(); } - if (!FAST_MODE_DEFER.equals(fastMode)) { + if (!CleanMojo.FAST_MODE_DEFER.equals(fastMode)) { try { - if (cleaner.logInfo != null) { - cleaner.logInfo.log("Waiting for background file deletion"); - } + cleaner.logger.info("Waiting for background file deletion"); while (status != STOPPED) { wait(); } 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 90d15c5..2842ec6 100644 --- a/src/main/java/org/apache/maven/plugins/clean/Fileset.java +++ b/src/main/java/org/apache/maven/plugins/clean/Fileset.java @@ -19,11 +19,10 @@ package org.apache.maven.plugins.clean; import java.nio.file.Path; -import java.util.Arrays; /** * Customizes the string representation of - *org.apache.maven.shared.model.fileset.FileSet to return the
+ * {@code org.apache.maven.shared.model.fileset.FileSet} to return the
* included and excluded files from the file-set's directory. Specifically,
* "file-set: [directory] (included: [included files],
* excluded: [excluded files])"
@@ -43,53 +42,87 @@ public class Fileset {
private boolean useDefaultExcludes;
/**
- * @return {@link #directory}
+ * {@return the base directory}.
*/
public Path getDirectory() {
return directory;
}
/**
- * @return {@link #includes}
+ * {@return the patterns of the file to include, or an empty array if unspecified}.
*/
public String[] getIncludes() {
return (includes != null) ? includes : new String[0];
}
/**
- * @return {@link #excludes}
+ * {@return the patterns of the file to exclude, or an empty array if unspecified}.
*/
public String[] getExcludes() {
return (excludes != null) ? excludes : new String[0];
}
/**
- * @return {@link #followSymlinks}
+ * {@return whether the base directory is excluded from the fileset}.
+ * This is {@code false} by default. The base directory can be excluded
+ * explicitly if the exclude patterns contains an empty string.
+ */
+ public boolean isBaseDirectoryExcluded() {
+ if (excludes != null) {
+ for (String pattern : excludes) {
+ if (pattern == null || pattern.isEmpty()) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * {@return whether to follow symbolic links}.
*/
public boolean isFollowSymlinks() {
return followSymlinks;
}
/**
- * @return {@link #useDefaultExcludes}
+ * {@return whether to use a default set of excludes}.
*/
public boolean isUseDefaultExcludes() {
return useDefaultExcludes;
}
/**
- * Retrieves the included and excluded files from this file-set's directory.
- * Specifically, "file-set: [directory] (included:
- * [included files], excluded: [excluded files])"
+ * Appends the elements of the given array in the given buffer.
+ * This is a helper method for {@link #toString()} implementations.
*
- * @return The included and excluded files from this file-set's directory.
+ * @param buffer the buffer where to add the elements
+ * @param label label identifying the array of elements to add
+ * @param patterns the elements to append, or {@code null} if none
+ */
+ static void append(StringBuilder buffer, String label, String[] patterns) {
+ buffer.append(label).append(": [");
+ if (patterns != null) {
+ for (int i = 0; i < patterns.length; i++) {
+ if (i != 0) {
+ buffer.append(", ");
+ }
+ buffer.append(patterns[i]);
+ }
+ }
+ buffer.append(']');
+ }
+
+ /**
+ * {@return a string representation of the included and excluded files from this file-set's directory}.
* Specifically, "file-set: [directory] (included:
* [included files], excluded: [excluded files])"
- * @see java.lang.Object#toString()
*/
@Override
public String toString() {
- return "file set: " + getDirectory() + " (included: " + Arrays.asList(getIncludes()) + ", excluded: "
- + Arrays.asList(getExcludes()) + ")";
+ var buffer = new StringBuilder("file set: ").append(getDirectory());
+ append(buffer.append(" ("), "included", getIncludes());
+ append(buffer.append(", "), "excluded", getExcludes());
+ return buffer.append(')').toString();
}
}
diff --git a/src/main/java/org/apache/maven/plugins/clean/GlobSelector.java b/src/main/java/org/apache/maven/plugins/clean/GlobSelector.java
deleted file mode 100644
index 3d61071..0000000
--- a/src/main/java/org/apache/maven/plugins/clean/GlobSelector.java
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- * 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.maven.plugins.clean;
-
-import java.io.File;
-import java.util.Arrays;
-
-import org.codehaus.plexus.util.DirectoryScanner;
-import org.codehaus.plexus.util.SelectorUtils;
-
-/**
- * Selects paths based on Ant-like glob patterns.
- *
- * @author Benjamin Bentmann
- */
-class GlobSelector implements Selector {
-
- private final String[] includes;
-
- private final String[] excludes;
-
- private final String str;
-
- GlobSelector(String[] includes, String[] excludes, boolean useDefaultExcludes) {
- this.str = "includes = " + toString(includes) + ", excludes = " + toString(excludes);
- this.includes = normalizePatterns(includes);
- this.excludes = normalizePatterns(addDefaultExcludes(excludes, useDefaultExcludes));
- }
-
- private static String toString(String[] patterns) {
- return (patterns == null) ? "[]" : Arrays.asList(patterns).toString();
- }
-
- private static String[] addDefaultExcludes(String[] excludes, boolean useDefaultExcludes) {
- String[] defaults = DirectoryScanner.DEFAULTEXCLUDES;
- if (!useDefaultExcludes) {
- return excludes;
- } else 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;
- }
- }
-
- private static String[] normalizePatterns(String[] patterns) {
- String[] normalized;
-
- if (patterns != null) {
- normalized = new String[patterns.length];
- for (int i = patterns.length - 1; i >= 0; i--) {
- normalized[i] = normalizePattern(patterns[i]);
- }
- } else {
- normalized = new String[0];
- }
-
- return normalized;
- }
-
- private static String normalizePattern(String pattern) {
- if (pattern == null) {
- return "";
- }
-
- String normalized = pattern.replace((File.separatorChar == '/') ? '\\' : '/', File.separatorChar);
-
- if (normalized.endsWith(File.separator)) {
- normalized += "**";
- }
-
- return normalized;
- }
-
- @Override
- public boolean isSelected(String pathname) {
- return (includes.length <= 0 || isMatched(pathname, includes))
- && (excludes.length <= 0 || !isMatched(pathname, excludes));
- }
-
- private static boolean isMatched(String pathname, String[] patterns) {
- for (String pattern : patterns) {
- if (SelectorUtils.matchPath(pattern, pathname)) {
- return true;
- }
- }
- return false;
- }
-
- @Override
- public boolean couldHoldSelected(String pathname) {
- for (String include : includes) {
- if (SelectorUtils.matchPatternStart(include, pathname)) {
- return true;
- }
- }
- return includes.length <= 0;
- }
-
- @Override
- public String toString() {
- return str;
- }
-}
diff --git a/src/main/java/org/apache/maven/plugins/clean/Selector.java b/src/main/java/org/apache/maven/plugins/clean/Selector.java
index 93a0695..ee84048 100644
--- a/src/main/java/org/apache/maven/plugins/clean/Selector.java
+++ b/src/main/java/org/apache/maven/plugins/clean/Selector.java
@@ -18,28 +18,405 @@
*/
package org.apache.maven.plugins.clean;
+import java.io.File;
+import java.nio.file.FileSystem;
+import java.nio.file.Path;
+import java.nio.file.PathMatcher;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
/**
- * Determines whether a path is selected for deletion. The pathnames used for method parameters will be relative to some
- * base directory and use {@link java.io.File#separatorChar} as separator.
+ * Determines whether a path is selected for deletion.
+ * The pathnames used for method parameters will be relative to some base directory
+ * and use {@code '/'} as separator, regardless the hosting operating system.
+ *
+ * Source: this list is copied from {@code plexus-utils-4.0.2} (released in + * September 23, 2024), class {@code org.codehaus.plexus.util.AbstractScanner}.
+ */ + 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" + }; + + /** + * String representation of the normalized include filters. + * This is kept only for {@link #toString()} implementation. + */ + private final String[] includePatterns; + + /** + * String representation of the normalized exclude filters. + * This is kept only for {@link #toString()} implementation. + */ + private final String[] excludePatterns; + + /** + * The matcher for includes. The length of this array is equal to {@link #includePatterns} array length. + */ + private final PathMatcher[] includes; + + /** + * The matcher for excludes. The length of this array is equal to {@link #excludePatterns} array length. + */ + private final PathMatcher[] excludes; + + /** + * 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. + */ + private final PathMatcher[] dirIncludes; + + /** + * The matcher for directories to exclude. This array does not include the parent directories, + * since 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; + + /** + * The base directory. All files will be relativized to that directory before to be matched. + */ + private final Path baseDirectory; + + /** + * Creates a new selector from the given file seT. + * + * @param fs the user-specified configuration + */ + Selector(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)); + } + + /** + * Returns the given array of excludes, optionally expanded with a default set of excludes. + * + * @param excludes the user-specified excludes. + * @param useDefaultExcludes whether to expand user exclude with the set of default excludes + * @return the potentially expanded set of excludes to use + */ + private static String[] addDefaultExcludes(final String[] excludes, final boolean useDefaultExcludes) { + if (!useDefaultExcludes) { + 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; + } + } + + /** + * Returns the given array of patterns with path separator normalized to {@code '/'}. + * Null or empty patterns are ignored, and duplications are removed. + * + * @param patterns the patterns to normalize + * @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) { + return new String[0]; + } + // TODO: use `LinkedHashSet.newLinkedHashSet(int)` instead with JDK19. + final var normalized = new LinkedHashSetnull.
- * @return true if the given path is selected for deletion, false otherwise.
+ * @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
*/
- boolean isSelected(String pathname);
+ @Override
+ public boolean matches(Path path) {
+ 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}.
+ */
+ private static boolean isMatched(Path path, PathMatcher[] matchers) {
+ for (PathMatcher matcher : matchers) {
+ if (matcher.matches(path)) {
+ return true;
+ }
+ }
+ return false;
+ }
/**
* Determines whether a directory could contain selected paths.
*
- * @param pathname The directory pathname to test, must not be null.
- * @return true if the given directory might contain selected paths, false if the
- * directory will definitively not contain selected paths..
+ * @param directory the directory pathname to test, must not be {@code null}
+ * @return {@code true} if the given directory might contain selected paths, {@code false} if the
+ * directory will definitively not contain selected paths
+ */
+ public boolean couldHoldSelected(Path directory) {
+ if (baseDirectory.equals(directory)) {
+ return true;
+ }
+ directory = baseDirectory.relativize(directory);
+ return (dirIncludes.length == 0 || isMatched(directory, dirIncludes))
+ && (dirExcludes.length == 0 || !isMatched(directory, dirExcludes));
+ }
+
+ /**
+ * {@return a string representation for logging purposes}.
+ * This string representation is reported logged when {@link Cleaner} is executed.
*/
- boolean couldHoldSelected(String pathname);
+ @Override
+ public String toString() {
+ var buffer = new StringBuilder();
+ Fileset.append(buffer, "includes", includePatterns);
+ Fileset.append(buffer.append(", "), "excludes", excludePatterns);
+ return buffer.toString();
+ }
}
diff --git a/src/test/java/org/apache/maven/plugins/clean/CleanMojoTest.java b/src/test/java/org/apache/maven/plugins/clean/CleanMojoTest.java
index 02e8bf6..670133c 100644
--- a/src/test/java/org/apache/maven/plugins/clean/CleanMojoTest.java
+++ b/src/test/java/org/apache/maven/plugins/clean/CleanMojoTest.java
@@ -29,6 +29,7 @@
import java.nio.file.Paths;
import java.util.Collections;
+import org.apache.maven.api.plugin.Log;
import org.apache.maven.api.plugin.MojoException;
import org.apache.maven.api.plugin.testing.Basedir;
import org.apache.maven.api.plugin.testing.InjectMojo;
@@ -40,11 +41,11 @@
import static org.apache.maven.api.plugin.testing.MojoExtension.getBasedir;
import static org.apache.maven.api.plugin.testing.MojoExtension.setVariableValueToObject;
-import static org.codehaus.plexus.util.IOUtil.copy;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
/**
* Test the clean mojo.
@@ -52,6 +53,8 @@
@MojoTest
public class CleanMojoTest {
+ private final Log log = mock(Log.class);
+
/**
* Tests the simple removal of directories
*
@@ -211,8 +214,8 @@ public void testFollowLinksWithWindowsJunction() throws Exception {
.start();
process.waitFor();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
- copy(process.getInputStream(), baos);
- copy(process.getErrorStream(), baos);
+ process.getInputStream().transferTo(baos);
+ process.getErrorStream().transferTo(baos);
if (!Files.exists(link)) {
throw new IOException("Unable to create junction: " + baos);
}
@@ -241,7 +244,7 @@ interface LinkCreator {
}
private void testSymlink(LinkCreator linkCreator) throws Exception {
- Cleaner cleaner = new Cleaner(null, null, false, null, null);
+ Cleaner cleaner = new Cleaner(null, log, false, null, null, false, true, false);
Path testDir = Paths.get("target/test-classes/unit/test-dir").toAbsolutePath();
Path dirWithLnk = testDir.resolve("dir");
Path orgDir = testDir.resolve("org-dir");
@@ -254,7 +257,7 @@ private void testSymlink(LinkCreator linkCreator) throws Exception {
Files.write(file, Collections.singleton("Hello world"));
linkCreator.createLink(jctDir, orgDir);
// delete
- cleaner.delete(dirWithLnk, null, false, true, false);
+ cleaner.delete(dirWithLnk);
// verify
assertTrue(Files.exists(file));
assertFalse(Files.exists(jctDir));
@@ -267,7 +270,8 @@ private void testSymlink(LinkCreator linkCreator) throws Exception {
Files.write(file, Collections.singleton("Hello world"));
linkCreator.createLink(jctDir, orgDir);
// delete
- cleaner.delete(dirWithLnk, null, true, true, false);
+ cleaner = new Cleaner(null, log, false, null, null, true, true, false);
+ cleaner.delete(dirWithLnk);
// verify
assertFalse(Files.exists(file));
assertFalse(Files.exists(jctDir));
diff --git a/src/test/java/org/apache/maven/plugins/clean/CleanerTest.java b/src/test/java/org/apache/maven/plugins/clean/CleanerTest.java
index 798ee3a..c4e8567 100644
--- a/src/test/java/org/apache/maven/plugins/clean/CleanerTest.java
+++ b/src/test/java/org/apache/maven/plugins/clean/CleanerTest.java
@@ -18,9 +18,7 @@
*/
package org.apache.maven.plugins.clean;
-import java.io.IOException;
import java.nio.file.AccessDeniedException;
-import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
@@ -41,8 +39,8 @@
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.inOrder;
@@ -61,8 +59,8 @@ class CleanerTest {
void deleteSucceedsDeeply(@TempDir Path tempDir) throws Exception {
final Path basedir = createDirectory(tempDir.resolve("target")).toRealPath();
final Path file = createFile(basedir.resolve("file"));
- final Cleaner cleaner = new Cleaner(null, log, false, null, null);
- cleaner.delete(basedir, null, false, true, false);
+ final var cleaner = new Cleaner(null, log, false, null, null, false, true, false);
+ cleaner.delete(basedir);
assertFalse(exists(basedir));
assertFalse(exists(file));
}
@@ -76,14 +74,10 @@ void deleteFailsWithoutRetryWhenNoPermission(@TempDir Path tempDir) throws Excep
// Remove the executable flag to prevent directory listing, which will result in a DirectoryNotEmptyException.
final Set