DefaultTempFileService.java

/*
 * 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.internal.impl;

import javax.inject.Named;
import javax.inject.Singleton;

import java.io.IOException;
import java.nio.file.FileVisitOption;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;

import org.apache.maven.api.Session;
import org.apache.maven.api.SessionData;
import org.apache.maven.api.services.TempFileService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static org.apache.maven.api.Constants.MAVEN_TEMPFILE_KEEP;

/**
 * Default TempFileService implementation.
 * Stores tracked paths in Session-scoped data and removes them after the build.
 */
@Named
@Singleton
public final class DefaultTempFileService implements TempFileService {

    private static final Logger LOGGER = LoggerFactory.getLogger(DefaultTempFileService.class);

    // unique, typed session key (uses factory; one narrow unchecked cast)
    @SuppressWarnings({"unchecked", "rawtypes"})
    private static final SessionData.Key<Set<Path>> TMP_KEY =
            (SessionData.Key) SessionData.key(Set.class, DefaultTempFileService.class);

    // supplier with concrete types (avoids inference noise)
    private static final Supplier<Set<Path>> TMP_SUPPLIER =
            () -> Collections.newSetFromMap(new ConcurrentHashMap<Path, Boolean>());

    @Override
    public Path createTempFile(final Session session, final String prefix, final String suffix) throws IOException {
        Objects.requireNonNull(session, "session");
        final Path file = Files.createTempFile(prefix, suffix);
        register(session, file);
        return file;
    }

    @Override
    public Path createTempFile(final Session session, final String prefix, final String suffix, final Path directory)
            throws IOException {
        Objects.requireNonNull(session, "session");
        Objects.requireNonNull(directory, "directory");
        final Path file = Files.createTempFile(directory, prefix, suffix);
        register(session, file);
        return file;
    }

    @Override
    public Path createTempDirectory(final Session session, final String prefix) throws IOException {
        Objects.requireNonNull(session, "session");
        final Path dir = Files.createTempDirectory(prefix);
        register(session, dir);
        return dir;
    }

    @Override
    public Path createTempDirectory(final Session session, final String prefix, final Path directory)
            throws IOException {
        Objects.requireNonNull(session, "session");
        Objects.requireNonNull(directory, "directory");
        final Path dir = Files.createTempDirectory(directory, prefix);
        register(session, dir);
        return dir;
    }

    @Override
    public void register(final Session session, final Path path) {
        Objects.requireNonNull(session, "session");
        Objects.requireNonNull(path, "path");
        final Set<Path> bucket = sessionPaths(session);
        bucket.add(path);
        LOGGER.debug("Temp path registered for cleanup: {}", path);
    }

    @Override
    public void cleanup(final Session session) throws IOException {
        Objects.requireNonNull(session, "session");

        if (Boolean.getBoolean(MAVEN_TEMPFILE_KEEP)) {
            LOGGER.info("Skipping temp cleanup due to -D{}=true", MAVEN_TEMPFILE_KEEP);
            return;
        }

        final Set<Path> bucket = sessionPaths(session);
        IOException first = null;

        for (final Path path : bucket) {
            try {
                deleteTree(path);
            } catch (final IOException e) {
                if (first == null) {
                    first = e;
                } else if (e != first) {
                    first.addSuppressed(e);
                }
                LOGGER.warn("Failed to delete temp path {}", path, e);
            }
        }
        bucket.clear();

        if (first != null) {
            throw first;
        }
    }

    // ---- internals ---------------------------------------------------------

    private Set<Path> sessionPaths(final Session session) {
        return session.getData().computeIfAbsent(TMP_KEY, TMP_SUPPLIER);
    }

    private static void deleteTree(final Path path) throws IOException {
        if (path == null || Files.notExists(path)) {
            return;
        }
        // Walk depth-first and delete files, then directories.
        // Best-effort: locked or inaccessible entries are logged and skipped
        // so the rest of the tree is still cleaned up.
        Files.walkFileTree(
                path, EnumSet.noneOf(FileVisitOption.class), Integer.MAX_VALUE, new SimpleFileVisitor<Path>() {
                    @Override
                    public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs)
                            throws IOException {
                        Files.deleteIfExists(file);
                        return FileVisitResult.CONTINUE;
                    }

                    @Override
                    public FileVisitResult visitFileFailed(final Path file, final IOException exc) {
                        LOGGER.debug("Could not access temp file for deletion: {}", file, exc);
                        return FileVisitResult.CONTINUE;
                    }

                    @Override
                    public FileVisitResult postVisitDirectory(final Path dir, final IOException exc) {
                        try {
                            Files.deleteIfExists(dir);
                        } catch (final IOException e) {
                            LOGGER.debug("Could not delete temp directory: {}", dir, e);
                        }
                        return FileVisitResult.CONTINUE;
                    }
                });
    }
}