All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.maven.plugins.clean.Cleaner Maven / Gradle / Ivy

Go to download

The Maven Clean Plugin is a plugin that removes files generated at build-time in a project's directory.

There is a newer version: 4.0.0-beta-1
Show newest version
/*
 * 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.io.IOException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
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.Deque;

import org.apache.maven.execution.ExecutionListener;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.plugin.logging.Log;
import org.codehaus.plexus.util.Os;
import org.eclipse.aether.SessionData;

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
 */
class Cleaner {

    private static final boolean ON_WINDOWS = Os.isFamily(Os.FAMILY_WINDOWS);

    private static final String LAST_DIRECTORY_TO_DELETE = Cleaner.class.getName() + ".lastDirectoryToDelete";

    /**
     * The maven session.  This is typically non-null in a real run, but it can be during unit tests.
     */
    private final MavenSession session;

    private final Logger logDebug;

    private final Logger logInfo;

    private final Logger logVerbose;

    private final Logger logWarn;

    private final File fastDir;

    private final String fastMode;

    /**
     * Creates a new cleaner.
     * @param log The logger to use, may be null to disable logging.
     * @param verbose Whether to perform verbose logging.
     * @param fastMode The fast deletion mode
     */
    Cleaner(MavenSession session, final Log log, boolean verbose, File fastDir, String fastMode) {
        logDebug = (log == null || !log.isDebugEnabled()) ? null : log::debug;

        logInfo = (log == null || !log.isInfoEnabled()) ? null : log::info;

        logWarn = (log == null || !log.isWarnEnabled()) ? null : log::warn;

        logVerbose = verbose ? logInfo : logDebug;

        this.session = session;
        this.fastDir = fastDir;
        this.fastMode = fastMode;
    }

    /**
     * Deletes the specified directories and its contents.
     *
     * @param basedir The directory to delete, must not be null. 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.
     */
    public void delete(
            File basedir, Selector selector, boolean followSymlinks, boolean failOnError, boolean retryOnError)
            throws IOException {
        if (!basedir.isDirectory()) {
            if (!basedir.exists()) {
                if (logDebug != null) {
                    logDebug.log("Skipping non-existing directory " + basedir);
                }
                return;
            }
            throw new IOException("Invalid base directory " + basedir);
        }

        if (logInfo != null) {
            logInfo.log("Deleting " + basedir + (selector != null ? " (" + selector + ")" : ""));
        }

        File file = followSymlinks ? basedir : basedir.getCanonicalFile();

        if (selector == null && !followSymlinks && fastDir != null && session != null) {
            // If anything wrong happens, we'll just use the usual deletion mechanism
            if (fastDelete(file)) {
                return;
            }
        }

        delete(file, "", selector, followSymlinks, failOnError, retryOnError);
    }

    private boolean fastDelete(File baseDirFile) {
        Path baseDir = baseDirFile.toPath();
        Path fastDir = this.fastDir.toPath();
        // Handle the case where we use ${maven.multiModuleProjectDirectory}/target/.clean for example
        if (fastDir.toAbsolutePath().startsWith(baseDir.toAbsolutePath())) {
            try {
                String prefix = baseDir.getFileName().toString() + ".";
                Path tmpDir = Files.createTempDirectory(baseDir.getParent(), prefix);
                try {
                    Files.move(baseDir, tmpDir, StandardCopyOption.REPLACE_EXISTING);
                    if (session != null) {
                        session.getRepositorySession().getData().set(LAST_DIRECTORY_TO_DELETE, baseDir.toFile());
                    }
                    baseDir = tmpDir;
                } catch (IOException e) {
                    Files.delete(tmpDir);
                    throw e;
                }
            } catch (IOException e) {
                if (logDebug != null) {
                    // TODO: this Logger interface cannot log exceptions and needs refactoring
                    logDebug.log("Unable to fast delete directory: " + e);
                }
                return false;
            }
        }
        // Create fastDir and the needed parents if needed
        try {
            if (!Files.isDirectory(fastDir)) {
                Files.createDirectories(fastDir);
            }
        } catch (IOException e) {
            if (logDebug != null) {
                // TODO: this Logger interface cannot log exceptions and needs refactoring
                logDebug.log("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());
            // Note that by specifying the ATOMIC_MOVE, we expect an exception to be thrown
            // if the path leads to a directory on another mountpoint.  If this is the case
            // or any other exception occurs, an exception will be thrown in which case
            // the method will return false and the usual deletion will be performed.
            Files.move(baseDir, dstDir, StandardCopyOption.ATOMIC_MOVE);
            BackgroundCleaner.delete(this, tmpDir.toFile(), fastMode);
            return true;
        } catch (IOException e) {
            if (logDebug != null) {
                // TODO: this Logger interface cannot log exceptions and needs refactoring
                logDebug.log("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.
     */
    private Result delete(
            File file,
            String pathname,
            Selector selector,
            boolean followSymlinks,
            boolean failOnError,
            boolean retryOnError)
            throws IOException {
        Result result = new Result();

        boolean isDirectory = file.isDirectory();

        if (isDirectory) {
            if (selector == null || selector.couldHoldSelected(pathname)) {
                if (followSymlinks || !isSymbolicLink(file.toPath())) {
                    File canonical = followSymlinks ? file : file.getCanonicalFile();
                    String[] filenames = canonical.list();
                    if (filenames != null) {
                        String prefix = pathname.length() > 0 ? pathname + File.separatorChar : "";
                        for (int i = filenames.length - 1; i >= 0; i--) {
                            String filename = filenames[i];
                            File child = new File(canonical, filename);
                            result.update(delete(
                                    child, prefix + filename, selector, followSymlinks, failOnError, retryOnError));
                        }
                    }
                } else if (logDebug != null) {
                    logDebug.log("Not recursing into symlink " + file);
                }
            } else if (logDebug != null) {
                logDebug.log("Not recursing into directory without included files " + file);
            }
        }

        if (!result.excluded && (selector == null || selector.isSelected(pathname))) {
            if (logVerbose != null) {
                if (isDirectory) {
                    logVerbose.log("Deleting directory " + file);
                } else if (file.exists()) {
                    logVerbose.log("Deleting file " + file);
                } else {
                    logVerbose.log("Deleting dangling symlink " + file);
                }
            }
            result.failures += delete(file, failOnError, retryOnError);
        } else {
            result.excluded = true;
        }

        return result;
    }

    private boolean isSymbolicLink(Path path) throws IOException {
        BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
        return attrs.isSymbolicLink()
                // MCLEAN-93: NTFS junctions have isDirectory() and isOther() attributes set
                || (attrs.isDirectory() && attrs.isOther());
    }

    /**
     * Deletes the specified file, directory. If the path denotes a symlink, only the link is removed, its target is
     * left untouched.
     *
     * @param file The file/directory to delete, must not be null.
     * @param failOnError Whether to abort with an exception in case the file/directory could not be deleted.
     * @param retryOnError Whether to undertake additional delete attempts in case 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.
     */
    private int delete(File file, boolean failOnError, boolean retryOnError) throws IOException {
        if (!file.delete()) {
            boolean deleted = false;

            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 i = 0; !deleted && i < delays.length; i++) {
                    try {
                        Thread.sleep(delays[i]);
                    } catch (InterruptedException e) {
                        // ignore
                    }
                    deleted = file.delete() || !file.exists();
                }
            } else {
                deleted = !file.exists();
            }

            if (!deleted) {
                if (failOnError) {
                    throw new IOException("Failed to delete " + file);
                } else {
                    if (logWarn != null) {
                        logWarn.log("Failed to delete " + file);
                    }
                    return 1;
                }
            }
        }

        return 0;
    }

    private static class Result {

        private int failures;

        private boolean excluded;

        public void update(Result result) {
            failures += result.failures;
            excluded |= result.excluded;
        }
    }

    private interface Logger {

        void log(CharSequence message);
    }

    private static class BackgroundCleaner extends Thread {

        private static BackgroundCleaner instance;

        private final Deque filesToDelete = new ArrayDeque<>();

        private final Cleaner cleaner;

        private final String fastMode;

        private static final int NEW = 0;
        private static final int RUNNING = 1;
        private static final int STOPPED = 2;

        private int status = NEW;

        public static void delete(Cleaner cleaner, File dir, String fastMode) {
            synchronized (BackgroundCleaner.class) {
                if (instance == null || !instance.doDelete(dir)) {
                    instance = new BackgroundCleaner(cleaner, dir, fastMode);
                }
            }
        }

        static void sessionEnd() {
            synchronized (BackgroundCleaner.class) {
                if (instance != null) {
                    instance.doSessionEnd();
                }
            }
        }

        private BackgroundCleaner(Cleaner cleaner, File dir, String fastMode) {
            super("mvn-background-cleaner");
            this.cleaner = cleaner;
            this.fastMode = fastMode;
            init(cleaner.fastDir, dir);
        }

        public void run() {
            while (true) {
                File basedir = pollNext();
                if (basedir == null) {
                    break;
                }
                try {
                    cleaner.delete(basedir, "", null, false, false, true);
                } catch (IOException e) {
                    // do not display errors
                }
            }
        }

        synchronized void init(File fastDir, File dir) {
            if (fastDir.isDirectory()) {
                File[] children = fastDir.listFiles();
                if (children != null && children.length > 0) {
                    for (File child : children) {
                        doDelete(child);
                    }
                }
            }
            doDelete(dir);
        }

        synchronized File pollNext() {
            File basedir = filesToDelete.poll();
            if (basedir == null) {
                if (cleaner.session != null) {
                    SessionData data = cleaner.session.getRepositorySession().getData();
                    File lastDir = (File) data.get(LAST_DIRECTORY_TO_DELETE);
                    if (lastDir != null) {
                        data.set(LAST_DIRECTORY_TO_DELETE, null);
                        return lastDir;
                    }
                }
                status = STOPPED;
                notifyAll();
            }
            return basedir;
        }

        synchronized boolean doDelete(File dir) {
            if (status == STOPPED) {
                return false;
            }
            filesToDelete.add(dir);
            if (status == NEW && FAST_MODE_BACKGROUND.equals(fastMode)) {
                status = RUNNING;
                notifyAll();
                start();
            }
            wrapExecutionListener();
            return true;
        }

        /**
         * If this has not been done already, we wrap the ExecutionListener inside a proxy
         * which simply delegates call to the previous listener.  When the session ends, it will
         * also call {@link BackgroundCleaner#sessionEnd()}.
         * There's no clean API to do that properly as this is a very unusual use case for a plugin
         * to outlive its main execution.
         */
        private void wrapExecutionListener() {
            ExecutionListener executionListener = cleaner.session.getRequest().getExecutionListener();
            if (executionListener == null
                    || !Proxy.isProxyClass(executionListener.getClass())
                    || !(Proxy.getInvocationHandler(executionListener) instanceof SpyInvocationHandler)) {
                ExecutionListener listener = (ExecutionListener) Proxy.newProxyInstance(
                        ExecutionListener.class.getClassLoader(),
                        new Class[] {ExecutionListener.class},
                        new SpyInvocationHandler(executionListener));
                cleaner.session.getRequest().setExecutionListener(listener);
            }
        }

        synchronized void doSessionEnd() {
            if (status != STOPPED) {
                if (status == NEW) {
                    start();
                }
                if (!FAST_MODE_DEFER.equals(fastMode)) {
                    try {
                        if (cleaner.logInfo != null) {
                            cleaner.logInfo.log("Waiting for background file deletion");
                        }
                        while (status != STOPPED) {
                            wait();
                        }
                    } catch (InterruptedException e) {
                        // ignore
                    }
                }
            }
        }
    }

    static class SpyInvocationHandler implements InvocationHandler {
        private final ExecutionListener delegate;

        SpyInvocationHandler(ExecutionListener delegate) {
            this.delegate = delegate;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            if ("sessionEnded".equals(method.getName())) {
                BackgroundCleaner.sessionEnd();
            }
            if (delegate != null) {
                return method.invoke(delegate, args);
            }
            return null;
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy