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

io.helidon.build.maven.enforcer.FileFinder Maven / Gradle / Ivy

/*
 * Copyright (c) 2021 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.maven.enforcer;

import java.io.IOException;
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.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import io.helidon.build.util.Log;

/**
 * Configuration of discovery of files to check.
 */
public class FileFinder {
    private static final DateTimeFormatter YEAR_FORMATTER = DateTimeFormatter.ofPattern("yyyy");

    private final String currentYear = YEAR_FORMATTER.format(ZonedDateTime.now());
    private final Path repositoryRoot;
    private final boolean useGit;
    private final boolean honorGitIgnore;

    private FileFinder(Builder builder) {
        useGit = builder.useGit;
        honorGitIgnore = builder.honorGitIgnore;
        repositoryRoot = builder.repositoryRoot;
    }

    /**
     * Builder to set up file config.
     *
     * @return a new builder
     */
    public static Builder builder() {
        return new Builder();
    }

    /**
     * Get files to check based on this file config.
     *
     * @param basePath path to use
     * @return found files
     */
    public FoundFiles findFiles(Path basePath) {
        // find repository root if not configured
        Path gitRepoDir = (repositoryRoot == null ? GitCommands.repositoryRoot(basePath) : repositoryRoot);

        List excludes = new ArrayList<>();
        if (useGit && honorGitIgnore) {
            addGitIgnore(gitRepoDir, excludes);
        }

        Set foundFiles;
        Set locallyModified;

        if (useGit) {
            locallyModified = GitCommands.locallyModified(gitRepoDir, basePath, currentYear);
            foundFiles = new HashSet<>(locallyModified);

            foundFiles.addAll(GitCommands.gitTracked(gitRepoDir, basePath));
        } else {
            foundFiles = findAllFiles(gitRepoDir, basePath);
            locallyModified = foundFiles;
        }

        List fileRequests = foundFiles.stream()
                .filter(file -> isValid(file, excludes))
                .collect(Collectors.toList());

        Set filteredLocallyModified = exclude(excludes, locallyModified);
        fileRequests.sort(FileRequest::compareTo);

        return FoundFiles.create(gitRepoDir, fileRequests, filteredLocallyModified, useGit);
    }

    @Override
    public String toString() {
        return "FileConfig{"
                + "repositoryRoot=" + repositoryRoot
                + ", useGit=" + useGit
                + ", honorGitIgnore=" + honorGitIgnore
                + '}';
    }

    private void addGitIgnore(Path gitRepoDir, List excludes) {
        Path gitIgnore = gitRepoDir.resolve(".gitignore");

        excludes.addAll(FileMatcher.create(".git/"));

        List lines = FileSystem.toLines(gitIgnore)
                .stream()
                .filter(it -> !it.startsWith("#"))
                .filter(it -> !it.isBlank())
                .collect(Collectors.toList());

        for (String line : lines) {
            if (line.contains("*")) {
                if (line.startsWith("*.")) {
                    excludes.addAll(FileMatcher.create(line.substring(1)));
                } else {
                    if (line.startsWith("*")) {
                        excludes.add(new NameEndExclude(line.substring(1)));
                    } else if (line.endsWith("*")) {
                        excludes.add(new NameStartExclude(line.substring(line.length() - 1)));
                    } else {
                        Log.warn("$(YELLOW .gitignore) matches not supported: " + line);
                    }
                }
            } else {
                excludes.addAll(FileMatcher.create(line));
            }
        }
    }

    private Set findAllFiles(Path gitRepoDir, Path basePath) {
        Set result = new HashSet<>();

        try {
            Files.walkFileTree(basePath, new SimpleFileVisitor<>() {
                @Override
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
                    result.add(FileRequest.create(gitRepoDir, gitRepoDir.relativize(file).toString(), lastModifiedYear(file)));
                    return FileVisitResult.CONTINUE;
                }
            });
        } catch (IOException e) {
            throw new EnforcerException("Failed to list files", e);
        }

        return result;
    }

    private String lastModifiedYear(Path file) {
        try {
            return YEAR_FORMATTER.format(ZonedDateTime.ofInstant(Files.getLastModifiedTime(file).toInstant(), ZoneId.of("GMT")));
        } catch (IOException e) {
            throw new EnforcerException("Failed to parse last modified year from local file: " + file, e);
        }
    }

    private Set exclude(List excludes, Set locallyModified) {
        return locallyModified.stream()
                .filter(it -> {
                    for (FileMatcher exclude : excludes) {
                        if (exclude.matches(it)) {
                            return false;
                        }
                    }
                    return true;
                })
                .map(FileRequest::relativePath)
                .collect(Collectors.toSet());
    }

    private boolean isValid(FileRequest file,
                            List excludes) {

        // file may have been deleted from GIT (or locally)
        if (!Files.exists(file.path())) {
            Log.debug("File " + file.relativePath() + " does not exist, ignoring.");
            return false;
        }

        for (FileMatcher exclude : excludes) {
            if (exclude.matches(file)) {
                Log.debug("Excluding " + file.relativePath());
                return false;
            }
        }

        return true;
    }

    /**
     * {@code FileConfig} builder static inner class.
     */
    public static final class Builder {
        private Path repositoryRoot;
        private boolean useGit = true;
        private boolean honorGitIgnore = true;

        private Builder() {
        }

        /**
         * Sets the {@code useGit} and returns a reference to this Builder so that the methods can be chained together.
         * @param useGit the {@code useGit} to set
         * @return a reference to this Builder
         */
        public Builder useGit(boolean useGit) {
            this.useGit = useGit;
            return this;
        }

        /**
         * Sets the {@code honorGitIgnore} and returns a reference to this Builder so that the methods can be chained together.
         * @param honorGitIgnore the {@code honorGitIgnore} to set
         * @return a reference to this Builder
         */
        public Builder honorGitIgnore(boolean honorGitIgnore) {
            this.honorGitIgnore = honorGitIgnore;
            return this;
        }

        /**
         * Sets the root of the repository, if it cannot be determined using git.
         *
         * @param repositoryRoot root of repo
         * @return updated builder
         */
        public Builder repositoryRoot(Path repositoryRoot) {
            this.repositoryRoot = repositoryRoot;
            return this;
        }

        /**
         * Returns a {@code FileConfig} built from the parameters previously set.
         *
         * @return a {@code FileConfig} built with parameters of this {@code FileConfig.Builder}
         */
        public FileFinder build() {
            return new FileFinder(this);
        }
    }

    private static final class NameEndExclude implements FileMatcher {
        private final String end;

        private NameEndExclude(String end) {
            this.end = end;
        }

        @Override
        public boolean matches(FileRequest file) {
            return file.fileName().endsWith(end);
        }
    }

    private static final class NameStartExclude implements FileMatcher {
        private final String start;

        private NameStartExclude(String start) {
            this.start = start;
        }

        @Override
        public boolean matches(FileRequest file) {
            return file.fileName().startsWith(start);
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy