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

io.helidon.build.util.FileUtils Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2019, 2022 Oracle and/or its affiliates.
 *
 * Licensed 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 io.helidon.build.util;

import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileAttribute;
import java.nio.file.attribute.FileTime;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiPredicate;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static java.nio.file.StandardCopyOption.COPY_ATTRIBUTES;
import static java.util.Objects.requireNonNull;

/**
 * File utilities.
 */
public final class FileUtils {

    /**
     * The working directory.
     */
    public static final Path WORKING_DIR = requiredDirectoryFromProperty("user.dir", false);

    /**
     * The user home directory.
     */
    public static final Path USER_HOME_DIR = requiredDirectoryFromProperty("user.home", false);

    private static final String JAVA_BINARY_NAME = Constants.OS.javaExecutable();
    private static final String JAVA_HOME_VAR = "JAVA_HOME";
    private static final String PATH_VAR = "PATH";
    private static final String BIN_DIR_NAME = "bin";

    /**
     * Returns a directory path from the given system property name, creating it if required.
     *
     * @param systemPropertyName The property name.
     * @param createIfRequired {@code true} If the directory should be created if it does not exist.
     * @return The directory.
     */
    public static Path requiredDirectoryFromProperty(String systemPropertyName, boolean createIfRequired) {
        final String path = Requirements.requireNonNull(System.getProperty(systemPropertyName),
                "Required system property %s not set", systemPropertyName);
        return requiredDirectory(path, createIfRequired);
    }

    /**
     * Returns a directory path from the given path, creating it if required.
     *
     * @param path The path.
     * @param createIfRequired {@code true} If the directory should be created if it does not exist.
     * @return The directory.
     */
    public static Path requiredDirectory(String path, boolean createIfRequired) {
        final Path dir = Path.of(requireNonNull(path, "valid path required"));
        return createIfRequired ? ensureDirectory(dir) : assertDir(dir);
    }

    /**
     * Returns the relative path from the working directory for the given path, if possible.
     *
     * @param path The path.
     * @return The relative path or the original if it is not within the working directory.
     */
    public static Path fromWorking(Path path) {
        try {
            Path relativePath = WORKING_DIR.relativize(path);
            if (relativePath.getName(0).toString().equals("..")) {
                return path;
            } else {
                return relativePath;
            }
        } catch (IllegalArgumentException e) {
            return path;
        }
    }

    /**
     * Ensure that the given path is an existing directory, creating it if required.
     *
     * @param path The path.
     * @param attrs The attributes.
     * @return The normalized, absolute directory path.
     */
    public static Path ensureDirectory(Path path, FileAttribute... attrs) {
        if (Files.exists(requireNonNull(path))) {
            return assertDir(path);
        } else {
            try {
                return Files.createDirectories(path, attrs);
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
        }
    }

    /**
     * Copies the source directory to the destination.
     *
     * @param source The source directory.
     * @param destination The destination directory. Must not exist.
     * @return The absolute, normalized destination directory.
     * @throws IllegalArgumentException If the destination exists.
     */
    @SuppressWarnings("CaughtExceptionImmediatelyRethrown")
    public static Path copyDirectory(Path source, Path destination) {
        assertDoesNotExist(destination);
        try (Stream stream = Files.walk(source)) {
            stream.forEach(src -> {
                try {
                    final Path dst = destination.resolve(source.relativize(src));
                    if (Files.isDirectory(src)) {
                        Files.createDirectory(dst);
                    } else {
                        Files.copy(src, dst, COPY_ATTRIBUTES);
                    }
                } catch (IOException e) {
                    throw new UncheckedIOException(e);
                }
            });
        } catch (UncheckedIOException e) {
            throw e;
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
        return destination.toAbsolutePath().normalize();
    }

    /**
     * List all files in the given directory that match the given filter. Does not recurse.
     *
     * @param directory The directory.
     * @param fileNameFilter The filter.
     * @return The normalized, absolute file paths.
     */
    public static List listFiles(Path directory, Predicate fileNameFilter) {
        return listFiles(directory, fileNameFilter, 1);
    }

    /**
     * List all files in the given directory that match the given filter. Does not recurse.
     *
     * @param directory The directory.
     * @param pathFilter The filter.
     * @return The normalized, absolute file paths.
     */
    public static List listFiles(Path directory, BiPredicate pathFilter) {
        return listFiles(directory, pathFilter, 1);
    }

    /**
     * List all files in the given directory that match the given filter, recursively if maxDepth {@code > 1}.
     *
     * @param directory The directory.
     * @param fileNameFilter The filter.
     * @param maxDepth The maximum recursion depth.
     * @return The normalized, absolute file paths.
     */
    public static List listFiles(Path directory, Predicate fileNameFilter, int maxDepth) {
        return listFiles(directory, (path, attrs) -> fileNameFilter.test(path.getFileName().toString()), maxDepth);
    }

    /**
     * List all files in the given directory that match the given filter, recursively if maxDepth {@code > 1}.
     *
     * @param directory The directory.
     * @param pathFilter The filter.
     * @param maxDepth The maximum recursion depth.
     * @return The normalized, absolute file paths.
     */
    public static List listFiles(Path directory, BiPredicate pathFilter, int maxDepth) {
        try {
            return Files.find(assertDir(directory), maxDepth, pathFilter).collect(Collectors.toList());
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    /**
     * List all files and directories in the given directory. Does not recurse.
     *
     * @param directory The directory.
     * @return The normalized, absolute file paths.
     */
    public static List list(Path directory) {
        return list(directory, 1);
    }

    /**
     * List all files and directories in the given directory, recursively if maxDepth {@code > 1}.
     *
     * @param directory The directory.
     * @param maxDepth The maximum recursion depth.
     * @return The normalized, absolute file paths.
     */
    public static List list(Path directory, final int maxDepth) {
        try {
            return Files.find(assertDir(directory), maxDepth, (path, attrs) -> true)
                        .collect(Collectors.toList());
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    /**
     * Assert that the given path exists and is a directory.
     *
     * @param directory The directory.
     * @return The normalized, absolute directory path.
     * @throws IllegalArgumentException If the path does not exist or is not a directory.
     */
    public static Path assertDir(Path directory) {
        final Path result = assertExists(directory);
        if (Files.isDirectory(result)) {
            return result;
        } else {
            throw new IllegalArgumentException(directory + " is not a directory");
        }
    }

    /**
     * Assert that the given path exists and is a file.
     *
     * @param file The file.
     * @return The normalized, absolute file path.
     * @throws IllegalArgumentException If the path does not exist or is not a file.
     */
    public static Path assertFile(Path file) {
        final Path result = assertExists(file);
        if (Files.isRegularFile(result)) {
            return result;
        } else {
            throw new IllegalArgumentException(file + " is not a file");
        }
    }

    /**
     * Assert that the given path exists.
     *
     * @param path The path.
     * @return The normalized, absolute path.
     * @throws IllegalArgumentException If the path does not exist.
     */
    public static Path assertExists(Path path) {
        if (Files.exists(requireNonNull(path))) {
            return path.toAbsolutePath().normalize();
        } else {
            throw new IllegalArgumentException(path + " does not exist");
        }
    }

    /**
     * Assert that the given path does not exist.
     *
     * @param path The path.
     * @return The normalized, absolute path.
     * @throws IllegalArgumentException If the path exists.
     */
    public static Path assertDoesNotExist(Path path) {
        if (Files.exists(requireNonNull(path))) {
            throw new IllegalArgumentException(path + " exists");
        } else {
            return path.toAbsolutePath().normalize();
        }
    }

    /**
     * Deletes the given file or directory if it exists.
     *
     * @param fileOrDirectory The file or directory.
     * @return The file or directory.
     * @throws IOException If an error occurs.
     */
    public static Path delete(Path fileOrDirectory) throws IOException {
        if (Files.exists(fileOrDirectory)) {
            if (Files.isRegularFile(fileOrDirectory)) {
                Files.delete(fileOrDirectory);
            } else {
                deleteDirectory(fileOrDirectory);
            }
        }
        return fileOrDirectory;
    }

    /**
     * Deletes the given directory if it exists.
     *
     * @param directory The directory.
     * @return The directory.
     * @throws IOException If an error occurs.
     */
    public static Path deleteDirectory(Path directory) throws IOException {
        if (Files.exists(directory)) {
            if (Files.isDirectory(directory)) {
                try (Stream stream = Files.walk(directory)) {
                    stream.sorted(Comparator.reverseOrder())
                          .forEach(file -> {
                              try {
                                  Files.delete(file);
                              } catch (IOException e) {
                                  throw new UncheckedIOException(e);
                              }
                          });
                }
            } else {
                throw new IllegalArgumentException(directory + " is not a directory");
            }
        }
        return directory;
    }

    /**
     * Deletes the content of the given directory, if any.
     *
     * @param directory The directory.
     * @return The directory.
     * @throws IOException If an error occurs.
     */
    public static Path deleteDirectoryContent(Path directory) throws IOException {
        if (Files.exists(directory)) {
            if (Files.isDirectory(directory)) {
                try (Stream stream = Files.walk(directory)) {
                    stream.sorted(Comparator.reverseOrder())
                          .filter(file -> !file.equals(directory))
                          .forEach(file -> {
                              try {
                                  Files.delete(file);
                              } catch (IOException e) {
                                  throw new UncheckedIOException(e);
                              }
                          });
                }
            } else {
                throw new IllegalArgumentException(directory + " is not a directory");
            }
        }
        return directory;
    }

    /**
     * Returns the total size of all files in the given path, including subdirectories.
     *
     * @param path The path. May be a file or directory.
     * @return The size, in bytes.
     * @throws UncheckedIOException If an error occurs.
     */
    public static long sizeOf(Path path) {
        try {
            if (Files.isRegularFile(path)) {
                return Files.size(path);
            } else {
                final AtomicLong size = new AtomicLong();
                Files.walkFileTree(path, new FileVisitor<>() {
                    @Override
                    public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
                        return FileVisitResult.CONTINUE;
                    }

                    @Override
                    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
                        size.addAndGet(attrs.size());
                        return FileVisitResult.CONTINUE;
                    }

                    @Override
                    public FileVisitResult visitFileFailed(Path file, IOException exc) {
                        return FileVisitResult.CONTINUE;
                    }

                    @Override
                    public FileVisitResult postVisitDirectory(Path dir, IOException exc) {
                        return FileVisitResult.CONTINUE;
                    }
                });
                return size.get();
            }
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    /**
     * Returns the last modified time of the given file, in seconds.
     *
     * @param file The file.
     * @return The last modified time.
     */
    public static long lastModifiedSeconds(Path file) {
        return lastModifiedTime(file).to(TimeUnit.SECONDS);
    }

    /**
     * Returns the last modified time of the given file, in millis.
     *
     * @param file The file.
     * @return The last modified time.
     */
    public static long lastModifiedMillis(Path file) {
        return lastModifiedTime(file).to(TimeUnit.MILLISECONDS);
    }

    /**
     * Returns the last modified time of the given file.
     *
     * @param file The file.
     * @return The last modified time.
     */
    public static FileTime lastModifiedTime(Path file) {
        try {
            return Files.getLastModifiedTime(file);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    /**
     * Tests whether or not the given file has a modified time that is newer than the base time.
     *
     * @param file The file.
     * @param baseTime The base time. May be {@code null}.
     * @return {@code true} if base time is {@code null} or change time is newer.
     */
    public static Optional newerThan(Path file, FileTime baseTime) {
        final FileTime modTime = lastModifiedTime(file);
        if (newerThan(modTime, baseTime)) {
            return Optional.of(modTime);
        } else {
            return Optional.empty();
        }
    }

    /**
     * Tests whether or not the given file has a modified time that is older than the base time.
     *
     * @param file The file.
     * @param baseTime The base time. May be {@code null}.
     * @return {@code true} if base time is {@code null} or change time is older.
     */
    public static Optional olderThan(Path file, FileTime baseTime) {
        final FileTime modTime = lastModifiedTime(file);
        if (olderThan(modTime, baseTime)) {
            return Optional.of(modTime);
        } else {
            return Optional.empty();
        }
    }

    /**
     * Tests whether or not the given change time is newer than a base time.
     *
     * @param changeTime The time.
     * @param baseTime The base time. May be {@code null}.
     * @return {@code true} if base time is {@code null} or change time is newer.
     */
    public static boolean newerThan(FileTime changeTime, FileTime baseTime) {
        return baseTime == null || changeTime.compareTo(baseTime) > 0;
    }

    /**
     * Tests whether or not the given change time is older than a base time.
     *
     * @param changeTime The time.
     * @param baseTime The base time. May be {@code null}.
     * @return {@code true} if base time is {@code null} or change time is older.
     */
    public static boolean olderThan(FileTime changeTime, FileTime baseTime) {
        return baseTime == null || changeTime.compareTo(baseTime) < 0;
    }

    /**
     * Returns the file name of the given file, as a string.
     *
     * @param file The file.
     * @return The name.
     */
    public static String fileName(Path file) {
        return requireNonNull(file.getFileName()).toString();
    }

    /**
     * Find an executable in the {@code PATH} environment variable, if present.
     *
     * @param executableName The executable name.
     * @return The path.
     */
    public static Optional findExecutableInPath(String executableName) {
        return Arrays.stream(requireNonNull(System.getenv(PATH_VAR)).split(File.pathSeparator))
                     .map(Paths::get)
                     .map(path -> path.resolve(executableName))
                     .filter(path -> (!Constants.OS.isPosix() && Files.exists(path)) || Files.isExecutable(path))
                     .findFirst();
    }

    /**
     * Returns the path to the java executable, searching the {@code PATH} var first, then checking {@code JAVA_HOME}.
     *
     * @return The path.
     */
    public static Optional javaExecutable() {
        final Optional path = javaExecutableInPath();
        if (path.isPresent()) {
            return path;
        } else {
            return javaExecutableInJavaHome();
        }
    }

    /**
     * Returns the path to the java executable, searching the {@code PATH} var first, then checking {@code JAVA_HOME}.
     *
     * @return The path.
     * @throws IllegalStateException if not found.
     */
    public static Path assertJavaExecutable() {
        return javaExecutable().orElseThrow(() -> new IllegalStateException(JAVA_BINARY_NAME + " not found. Please add it to "
                                                                            + "your PATH or set the JAVA_HOME or variable."));
    }

    /**
     * Returns the path to the java executable using the {@code PATH} var.
     *
     * @return The path.
     */
    public static Optional javaExecutableInPath() {
        return findExecutableInPath(JAVA_BINARY_NAME);
    }

    /**
     * Returns the path to the java executable using the {@code JAVA_HOME} var if present and valid.
     *
     * @return The path.
     */
    public static Optional javaExecutableInJavaHome() {
        final String javaHomePath = System.getenv(JAVA_HOME_VAR);
        if (javaHomePath != null) {
            final Path javaHome = Paths.get(javaHomePath);
            final Path binary = javaHome.resolve(BIN_DIR_NAME).resolve(JAVA_BINARY_NAME);
            if (!Constants.OS.isPosix() || Files.isExecutable(binary)) {
                return Optional.of(binary);
            } else {
                throw new IllegalStateException(JAVA_BINARY_NAME + " not found in JAVA_HOME path: " + javaHomePath);
            }
        }
        return Optional.empty();
    }

    /**
     * Create a {@link Path} path under the given parent directory that does not already exist.
     * Appends {@code -$i} to the given name until a non-existing entry is found.
     *
     * @param directory parent directory where to create the new directory
     * @param name      the name of the entry to create
     * @param suffix    the suffix to append after {@code -$i}
     * @return Path
     */
    public static Path unique(Path directory, String name, String suffix) {
        Path path = directory.resolve(name + suffix);
        int i = 1;
        while (Files.exists(path)) {
            path = directory.resolve(name + "-" + i + suffix);
            i++;
        }
        return path;
    }

    /**
     * Create a {@link Path} path under the given parent directory that does not already exist.
     * Appends {@code -$i} to the given name until a non-existing entry is found.
     *
     * @param directory parent directory where to create the new directory
     * @param name      the name of the entry to create
     * @return Path
     */
    public static Path unique(Path directory, String name) {
        return unique(directory, name, "");
    }

    /**
     * Creates the given file (with no content) if it does not already exist.
     *
     * @param file The file.
     * @return The file.
     */
    public static Path ensureFile(Path file) {
        if (!Files.exists(file)) {
            try {
                Files.createFile(file);
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
        }
        return file;
    }

    /**
     * Ensure that the given file exists, and update the modified time if it does.
     *
     * @param file The file.
     * @return The file.
     */
    public static Path touch(Path file) {
        if (Files.exists(file)) {
            final long currentTime = System.currentTimeMillis();
            final long lastModified = lastModifiedSeconds(file);
            final long lastModifiedPlusOneSecond = lastModified + 1000;
            final long newTime = Math.max(currentTime, lastModifiedPlusOneSecond);
            try {
                Files.setLastModifiedTime(file, FileTime.fromMillis(newTime));
                return file;
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
        } else {
            return ensureFile(file);
        }
    }

    /**
     * Change detection type.
     */
    public enum ChangeDetectionType {
        /**
         * Return the first newer modification time.
         */
        FIRST,
        /**
         * Return the latest newer modification time.
         */
        LATEST
    }

    /**
     * Checks whether any matching file in the given directory has a modified time more recent than the given time.
     *
     * @param directory The directory.
     * @param baseTime The time to check against. If {@code null}, uses {@code FileUtils.fromMillis(0)}.
     * @param dirFilter A filter for directories to visit.
     * @param fileFilter A filter for which files to check.
     * @param type The type.
     * @return The time, if changed.
     */
    public static Optional changedSince(Path directory,
                                                  FileTime baseTime,
                                                  Predicate dirFilter,
                                                  Predicate fileFilter,
                                                  ChangeDetectionType type) {

        final FileTime base = baseTime == null ? FileTime.fromMillis(0) : baseTime;
        final AtomicReference checkTime = new AtomicReference<>(base);
        final AtomicReference changeTime = new AtomicReference<>();
        final boolean checkAllFiles = type == ChangeDetectionType.LATEST;
        Log.debug("Checking if project has files newer than last check time %s", checkTime.get());
        try {
            Files.walkFileTree(directory, new FileVisitor<>() {
                @Override
                public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
                    return dirFilter.test(dir) ? FileVisitResult.CONTINUE : FileVisitResult.SKIP_SUBTREE;
                }

                @Override
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
                    if (fileFilter.test(file)) {
                        final FileTime fileTime = lastModifiedTime(file);
                        if (fileTime.compareTo(checkTime.get()) > 0) {
                            Log.debug("%s @ %s is newer than last check time %s", file, fileTime, checkTime.get());
                            changeTime.set(fileTime);
                            if (checkAllFiles) {
                                checkTime.set(fileTime);
                            } else {
                                return FileVisitResult.TERMINATE;
                            }
                        }
                    }
                    return FileVisitResult.CONTINUE;
                }

                @Override
                public FileVisitResult visitFileFailed(Path file, IOException exc) {
                    changeTime.set(null);
                    return FileVisitResult.TERMINATE;
                }

                @Override
                public FileVisitResult postVisitDirectory(Path dir, IOException exc) {
                    return FileVisitResult.CONTINUE;
                }
            });

            return Optional.ofNullable(changeTime.get());

        } catch (Exception e) {
            Log.warn(e.getMessage());
        }

        return Optional.of(FileTime.fromMillis(System.currentTimeMillis())); // Force it if we get here
    }

    private FileUtils() {
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy