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

io.helidon.build.cache.plugin.GoOfflineMojo Maven / Gradle / Ivy

/*
 * Copyright (c) 2024 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.cache.plugin;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Deque;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;

import io.helidon.build.common.Lists;
import io.helidon.build.common.Strings;
import io.helidon.build.common.logging.Log;
import io.helidon.build.common.logging.LogLevel;
import io.helidon.build.common.maven.MavenModel;
import io.helidon.build.common.maven.plugin.MavenArtifact;
import io.helidon.build.common.maven.plugin.MavenFilters;
import io.helidon.build.common.maven.plugin.PlexusLoggerHolder;

import org.apache.maven.execution.MavenSession;
import org.apache.maven.model.BuildBase;
import org.apache.maven.model.Dependency;
import org.apache.maven.model.DependencyManagement;
import org.apache.maven.model.Plugin;
import org.apache.maven.model.PluginManagement;
import org.apache.maven.model.Profile;
import org.apache.maven.model.Repository;
import org.apache.maven.model.building.ModelBuildingRequest;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.DefaultProjectBuildingRequest;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.ProjectBuilder;
import org.apache.maven.project.ProjectBuildingException;
import org.apache.maven.project.ProjectBuildingRequest;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.repository.RepositoryPolicy;
import org.eclipse.aether.resolution.ArtifactRequest;
import org.eclipse.aether.resolution.ArtifactResolutionException;
import org.eclipse.aether.resolution.ArtifactResult;

import static java.util.stream.Collectors.toMap;

/**
 * A goal to aggressively cache all Maven dependencies.
 * Only supports JDK >= 17.
 */
@Mojo(name = "go-offline", threadSafe = true, requiresDirectInvocation = true, aggregator = true)
public class GoOfflineMojo extends AbstractMojo {

    @Component
    @SuppressWarnings("unused")
    private PlexusLoggerHolder plexusLogHolder;

    /**
     * The entry point to Aether.
     */
    @Component
    private RepositorySystem repoSystem;

    /**
     * Maven Project Builder component.
     */
    @Component
    private ProjectBuilder projectBuilder;

    @Parameter(defaultValue = "${session}", readonly = true, required = true)
    private MavenSession session;

    /**
     * The current repository/network configuration of Maven.
     */
    @Parameter(defaultValue = "${repositorySystemSession}", readonly = true)
    private RepositorySystemSession repoSession;

    /**
     * The project remote repositories to use.
     */
    @Parameter(defaultValue = "${project.remoteProjectRepositories}", readonly = true)
    private List remoteRepos;

    /**
     * Pom identity.
     * List of relative paths that must exist for a directory to be resolved as a Maven module.
     */
    @Parameter(property = "cache.offline.pomScanningIdentity", defaultValue = "pom.xml")
    private List pomScanningIdentity = List.of();

    /**
     * Pom scanning includes.
     * List of glob expressions used as an include filter for directories that may contain {@code pom.xml} files.
     */
    @Parameter(property = "cache.offline.pomScanningIncludes", defaultValue = "**/*")
    private List pomScanningIncludes = List.of();

    /**
     * Pom scanning excludes.
     * List of glob expressions used as an exclude filter for directories that may contain {@code pom.xml} files.
     */
    @Parameter(property = "cache.offline.pomScanningExcludes", defaultValue = "**/target/**,**/src/**")
    private List pomScanningExcludes = List.of();

    /**
     * Pom include patterns.
     * List of include filters (format is {@code groupId:artifactId:packaging} with wildcard support)
     * of scanned {@code pom.xml} files.
     */
    @Parameter(property = "cache.offline.pomIncludes", defaultValue = "*:*:*")
    private List pomIncludes = List.of();

    /**
     * Pom exclude patterns.
     * List of exclude filters (format is {@code groupId:artifactId:packaging} with wildcard support)
     * of scanned {@code pom.xml} files.
     */
    @Parameter(property = "cache.offline.pomExcludes")
    private List pomExcludes = List.of();

    /**
     * Specifies if {@code -SNAPSHOT} artifacts should be processed.
     */
    @Parameter(property = "cache.offline.includeSnapshots", defaultValue = "false")
    private boolean includeSnapshots;

    /**
     * Specifies if dependencies should be processed.
     */
    @Parameter(property = "cache.offline.includeDependencies", defaultValue = "true")
    private boolean includeDeps;

    /**
     * Specifies if dependency management should be processed.
     */
    @Parameter(property = "cache.offline.includeDependencyManagement", defaultValue = "false")
    private boolean includeDepsMgmt;

    /**
     * Specifies if plugins should be processed.
     */
    @Parameter(property = "cache.offline.includePlugins", defaultValue = "true")
    private boolean includePlugins;

    /**
     * Specifies if plugin management should be processed.
     */
    @Parameter(property = "cache.offline.includePluginManagement", defaultValue = "false")
    private boolean includePluginMgmt;

    /**
     * Specifies if the resolution should traverse.
     */
    @Parameter(property = "cache.offline.traverse", defaultValue = "true")
    private boolean traverse;

    /**
     * Profile include patterns.
     * List of include filters (format is {@code groupId:artifactId:packaging} with wildcard support).
     */
    @Parameter(property = "cache.offline.profileIncludes", defaultValue = "*")
    private List profileIncludes = List.of();

    /**
     * Profile exclude patterns.
     * List of exclude filters (format is {@code groupId:artifactId:packaging} with wildcard support).
     */
    @Parameter(property = "cache.offline.profileExcludes")
    private List profileExcludes = List.of();

    /**
     * Transitive scope include patterns.
     * List of include filters (format is {@code groupId:artifactId:packaging} with wildcard support).
     */
    @Parameter(property = "cache.offline.scopeIncludes", defaultValue = "*")
    private List scopeIncludes = List.of();

    /**
     * Transitive scope exclude patterns.
     * List of exclude filters (format is {@code groupId:artifactId:packaging} with wildcard support).
     */
    @Parameter(property = "cache.offline.scopeExcludes", defaultValue = "test")
    private List scopeExcludes = List.of();

    /**
     * Specifies if optional transitive dependencies should be processed.
     */
    @Parameter(property = "cache.offline.includeOptional", defaultValue = "true")
    private boolean includeOptional;

    /**
     * Skip this goal execution.
     */
    @Parameter(property = "cache.offline.skip", defaultValue = "false")
    private boolean skip;

    /**
     * Specifies if the build will fail if there are errors during execution or not.
     */
    @Parameter(property = "cache.offline.failOnError", defaultValue = "false")
    private boolean failOnError;

    private Predicate pomFilter;
    private Predicate pomIdentityFilter;
    private Predicate pomScanningFilter;
    private Predicate profileFilter;
    private Predicate scopeFilter;
    private Map> workspace;
    private final Set resolved = new HashSet<>();

    @Override
    public void execute() {
        if (skip) {
            Log.info("processing is skipped.");
            return;
        }

        // init filters
        pomFilter = MavenFilters.pomFilter(pomIncludes, pomExcludes);
        pomIdentityFilter = MavenFilters.dirFilter(pomScanningIdentity);
        pomScanningFilter = MavenFilters.pathFilter(pomScanningIncludes, pomScanningExcludes, Path.of(""));
        profileFilter = MavenFilters.stringFilter(profileIncludes, profileExcludes);
        scopeFilter = MavenFilters.stringFilter(scopeIncludes, scopeExcludes);

        // scan workspace manually
        workspace = scanWorkspace();

        // derive projects from workspace
        List allProjects = collectProjects();

        // expand projects into artifacts to traverse (via effective pom)
        List initial = allProjects.stream()
                .map(it -> collect(it, false, includeDeps, includeDepsMgmt, includePlugins, includePluginMgmt))
                .flatMap(Collection::stream)
                .distinct()
                .toList();

        // mark workspace artifacts as pre-resolved
        resolved.addAll(workspace.keySet());

        // depth-first traversal
        Deque stack = new ArrayDeque<>(initial);
        while (!stack.isEmpty()) {
            ResolvableArtifacts e = stack.pop();
            List requests = Lists.filter(e.artifacts, this::filterArtifact);
            if (requests.isEmpty()) {
                continue;
            }
            List result = resolve(requests, e.repos);
            resolved.addAll(requests);
            if (traverse) {
                for (MavenArtifact artifact : result) {
                    if (!filterArtifact(artifact)) {
                        continue;
                    }
                    MavenArtifact pom = resolve(artifact.pom(), e.repos);
                    if (pom != null) {
                        Log.debug("Resolving for traversal: %s, requests: %s", artifact, requests);
                        MavenProject project = effectivePom(pom.file());
                        if (project != null) {
                            Log.info("Traversing %s", pom);
                            List downstream = collect(project, true, includeDeps, false, false, false);
                            downstream.forEach(stack::push);
                        }
                    }
                }
            }
        }
    }

    private boolean filterArtifact(MavenArtifact artifact) {
        return !resolved.contains(artifact) && artifact.version() != null
               && (includeSnapshots || !artifact.version().endsWith("-SNAPSHOT"));
    }

    private boolean filterDependency(Dependency dep, MavenProject project, String profileId) {
        String scope = dep.getScope() == null ? scope(project, dep, profileId) : dep.getScope();
        String optional = dep.getOptional() == null ? optional(project, dep, profileId) : dep.getOptional();
        return scopeFilter.test(scope == null ? "" : scope)
               && (!Boolean.parseBoolean(optional) || includeOptional);
    }

    private List collectProjects() {
        return workspace.values().stream()
                .flatMap(Collection::stream)
                .map(p -> Optional.ofNullable(effectivePom(p)))
                .filter(Optional::isPresent)
                .map(Optional::get)
                .toList();
    }

    private Map> scanWorkspace() {
        try (Stream stream = Files.walk(Path.of(""))) {
            return stream
                    .filter(pomIdentityFilter)
                    .filter(p -> pomScanningFilter.test(p))
                    .map(it -> {
                        Path file = it.resolve("pom.xml");
                        Log.debug("Reading model: %s", file);
                        return Map.entry(MavenModel.read(file), Lists.of(file));
                    })
                    .filter(it -> pomFilter.test(it.getKey()))
                    .collect(toMap(it -> new MavenArtifact(it.getKey()), Map.Entry::getValue, Lists::addAll));
        } catch (IOException ex) {
            throw new UncheckedIOException(ex);
        }
    }

    private MavenArtifact resolve(MavenArtifact artifact, Set repositories) {
        try {
            Log.debug("Resolving artifact: %s, repositories: %s", artifact, repositories);
            ArtifactRequest request = new ArtifactRequest();
            request.setArtifact(artifact.toAetherArtifact());
            request.setRepositories(new ArrayList<>(repositories));
            ArtifactResult result = repoSystem.resolveArtifact(repoSession, request);
            return new MavenArtifact(result.getArtifact());
        } catch (ArtifactResolutionException ex) {
            if (failOnError) {
                throw new RuntimeException(ex);
            }
            Log.log(LogLevel.DEBUG, ex, "Unable to resolve artifact: %s, repositories: %s", artifact, repositories);
            return null;
        }
    }

    private List resolve(List artifacts, Set repositories) {
        try {
            Log.debug("Resolving artifacts: %s, repositories: %s", artifacts, repositories);
            List repos = new ArrayList<>(repositories);
            List requests = Lists.map(artifacts, it -> {
                ArtifactRequest request = new ArtifactRequest();
                request.setArtifact(it.toAetherArtifact());
                request.setRepositories(repos);
                return request;
            });
            List results = repoSystem.resolveArtifacts(repoSession, requests);
            return Lists.map(results, it -> new MavenArtifact(it.getArtifact()));
        } catch (ArtifactResolutionException ex) {
            if (failOnError) {
                throw new RuntimeException(ex);
            }
            Log.log(LogLevel.DEBUG, ex, "Unable to resolve artifacts: %s, repositories: %s", artifacts, repositories);
            return List.of();
        }
    }

    private MavenProject effectivePom(Path pomFile) {
        try {
            Log.debug("resolving effective pom " + pomFile);
            ProjectBuildingRequest orig = session.getProjectBuildingRequest();
            ProjectBuildingRequest pbr = new DefaultProjectBuildingRequest(orig);
            pbr.setRemoteRepositories(orig.getRemoteRepositories());
            pbr.setPluginArtifactRepositories(orig.getPluginArtifactRepositories());
            pbr.setProject(null);
            pbr.setValidationLevel(ModelBuildingRequest.VALIDATION_LEVEL_MINIMAL);
            pbr.setResolveDependencies(false);
            return projectBuilder.build(pomFile.toFile(), pbr).getProject();
        } catch (ProjectBuildingException ex) {
            if (failOnError) {
                throw new RuntimeException(ex);
            }
            Log.log(LogLevel.DEBUG, ex, "Unable to build effective model for: %s", pomFile);
            return null;
        }
    }

    private List collect(MavenProject project,
                                              boolean traversing,
                                              boolean includeDeps,
                                              boolean includeDepsMgmt,
                                              boolean includePlugins,
                                              boolean includePluginMgmt) {

        List artifacts = new ArrayList<>();

        Set repos = repos(project.getRepositories(), Set.of());
        Set pluginRepos = repos(project.getPluginRepositories(), Set.of());

        if (includeDeps) {
            List deps = project.getDependencies();
            if (traversing) {
                deps = Lists.filter(deps, it -> filterDependency(it, project, null));
            }
            artifacts.add(collectDeps(project, deps, repos, null));
        }
        if (includeDepsMgmt) {
            artifacts.add(collectDepMgmt(project, project.getDependencyManagement(), repos, null));
        }
        if (includePlugins) {
            artifacts.add(collectPlugins(project, project.getBuildPlugins(), pluginRepos, null));
        }
        if (includePluginMgmt) {
            artifacts.add(collectPluginMgmt(project, project.getPluginManagement(), pluginRepos, null));
        }

        MavenProject currentProject = project;
        while (currentProject != null) {
            List activeProfileIds = Lists.map(currentProject.getActiveProfiles(), Profile::getId);
            for (Profile profile : currentProject.getModel().getProfiles()) {
                String profileId = profile.getId();
                if (activeProfileIds.contains(profileId) && profileFilter.test(profileId)) {
                    continue;
                }

                Set profileRepos = repos(profile.getRepositories(), repos);
                Set profilePluginRepos = repos(profile.getPluginRepositories(), pluginRepos);

                if (includeDeps) {
                    List deps = profile.getDependencies();
                    if (traversing) {
                        deps = Lists.filter(deps, it -> filterDependency(it, project, profileId));
                    }
                    artifacts.add(collectDeps(currentProject, deps, profileRepos, profileId));
                }
                if (includeDepsMgmt) {
                    DependencyManagement mgmt = profile.getDependencyManagement();
                    artifacts.add(collectDepMgmt(currentProject, mgmt, profileRepos, profileId));
                }

                BuildBase profileBuild = profile.getBuild();
                if (includePlugins) {
                    List plugins = profileBuild != null ? profileBuild.getPlugins() : List.of();
                    artifacts.add(collectPlugins(currentProject, plugins, profilePluginRepos, profileId));
                }
                if (includePluginMgmt) {
                    PluginManagement mgmt = profileBuild != null ? profileBuild.getPluginManagement() : null;
                    artifacts.add(collectPluginMgmt(currentProject, mgmt, profilePluginRepos, profileId));
                }
            }
            currentProject = currentProject.getParent();
        }
        return Lists.filter(artifacts, it -> !it.artifacts.isEmpty());
    }

    private ResolvableArtifacts collectDeps(MavenProject project,
                                            List deps,
                                            Set repos,
                                            String profileId) {

        List artifacts = new ArrayList<>();
        List filtered = Lists.filter(deps, it -> filterDependency(it, project, profileId));
        filtered.forEach(it -> artifacts.add(new MavenArtifact(it, () -> version(project, it, profileId))));
        return new ResolvableArtifacts(artifacts, repos);
    }

    private ResolvableArtifacts collectDepMgmt(MavenProject project,
                                               DependencyManagement mgmt,
                                               Set repos,
                                               String profileId) {

        List artifacts = new ArrayList<>();
        if (mgmt != null) {
            mgmt.getDependencies().forEach(it -> artifacts.add(new MavenArtifact(it, () -> version(project, it, profileId))));
        }
        return new ResolvableArtifacts(artifacts, repos);
    }

    private ResolvableArtifacts collectPlugins(MavenProject project,
                                               List plugins,
                                               Set repos,
                                               String profileId) {

        List artifacts = new ArrayList<>();
        plugins.forEach(it -> artifacts.add(new MavenArtifact(it, () -> version(project, it, profileId))));
        return new ResolvableArtifacts(artifacts, repos);
    }

    private ResolvableArtifacts collectPluginMgmt(MavenProject project,
                                                  PluginManagement mgmt,
                                                  Set repos,
                                                  String profileId) {

        List artifacts = new ArrayList<>();
        if (mgmt != null) {
            mgmt.getPlugins().forEach(it -> artifacts.add(new MavenArtifact(it, () -> version(project, it, profileId))));
        }
        return new ResolvableArtifacts(artifacts, repos);
    }

    private String optional(MavenProject project, Dependency d, String profileId) {
        return effectiveValue(project, d, profileId, this::optional, MavenProject::getDependencyManagement,
                Profile::getDependencyManagement);
    }

    private String scope(MavenProject project, Dependency d, String profileId) {
        return effectiveValue(project, d, profileId, this::scope, MavenProject::getDependencyManagement,
                Profile::getDependencyManagement);
    }

    private String version(MavenProject project, Dependency d, String profileId) {
        return effectiveValue(project, d, profileId, this::version, MavenProject::getDependencyManagement,
                Profile::getDependencyManagement);
    }

    private String version(MavenProject project, Plugin p, String profileId) {
        return effectiveValue(project, p, profileId, this::version, MavenProject::getPluginManagement,
                it -> it.getBuild() != null ? it.getBuild().getPluginManagement() : null);
    }

    private  R effectiveValue(MavenProject project,
                                       U u,
                                       String profileId,
                                       BiFunction f0,
                                       Function f1,
                                       Function f2) {
        R value = f0.apply(f1.apply(project), u);
        if (value == null && profileId != null) {
            MavenProject currentProject = project;
            while (value == null && currentProject != null) {
                for (Profile profile : currentProject.getModel().getProfiles()) {
                    if (profile.getId().equals(profileId)) {
                        value = f0.apply(f2.apply(profile), u);
                        break;
                    }
                }
                currentProject = currentProject.getParent();
            }
        }
        return value;
    }

    private String optional(DependencyManagement mgmt, Dependency d) {
        if (mgmt != null) {
            return mgmt.getDependencies().stream()
                    .filter(it -> it.getGroupId().equals(d.getGroupId()) && it.getArtifactId().equals(d.getArtifactId()))
                    .findFirst()
                    .map(Dependency::getOptional)
                    .orElse(null);
        }
        return null;
    }

    private String scope(DependencyManagement mgmt, Dependency d) {
        if (mgmt != null) {
            return mgmt.getDependencies().stream()
                    .filter(it -> it.getGroupId().equals(d.getGroupId()) && it.getArtifactId().equals(d.getArtifactId()))
                    .findFirst()
                    .map(Dependency::getScope)
                    .orElse(null);
        }
        return null;
    }

    private String version(DependencyManagement mgmt, Dependency d) {
        if (mgmt != null) {
            return mgmt.getDependencies().stream()
                    .filter(it -> it.getGroupId().equals(d.getGroupId()) && it.getArtifactId().equals(d.getArtifactId()))
                    .findFirst()
                    .map(Dependency::getVersion)
                    .orElse(null);
        }
        return null;
    }

    private String version(PluginManagement mgmt, Plugin p) {
        if (mgmt != null) {
            return mgmt.getPlugins().stream()
                    .filter(it -> it.getGroupId().equals(p.getGroupId()) && it.getArtifactId().equals(p.getArtifactId()))
                    .findFirst()
                    .map(Plugin::getVersion)
                    .orElse(null);
        }
        return null;
    }

    private RemoteRepository repository(Repository repo) {
        RemoteRepository.Builder builder = new RemoteRepository.Builder(repo.getId(), repo.getLayout(), repo.getUrl());
        org.apache.maven.model.RepositoryPolicy releasePolicy = repo.getReleases();
        if ((releasePolicy == null || !releasePolicy.isEnabled()) && !includeSnapshots) {
            // discard the repo if snapshot only and includeSnapshots is false
            return null;
        }
        if (releasePolicy != null) {
            String updatePolicy = releasePolicy.getUpdatePolicy();
            String checksumPolicy = releasePolicy.getChecksumPolicy();
            builder.setReleasePolicy(new RepositoryPolicy(
                    releasePolicy.isEnabled(),
                    Strings.isValid(updatePolicy) ? updatePolicy : "always",
                    Strings.isValid(checksumPolicy) ? checksumPolicy : "ignore"));
        }
        org.apache.maven.model.RepositoryPolicy snapshotPolicy = repo.getSnapshots();
        if (snapshotPolicy != null) {
            String updatePolicy = snapshotPolicy.getUpdatePolicy();
            String checksumPolicy = snapshotPolicy.getChecksumPolicy();
            builder.setSnapshotPolicy(new RepositoryPolicy(
                    snapshotPolicy.isEnabled() && includeSnapshots,
                    Strings.isValid(updatePolicy) ? updatePolicy : "always",
                    Strings.isValid(checksumPolicy) ? checksumPolicy : "ignore"));
        }
        return builder.build();
    }

    private Set repos(List repositories, Set initial) {
        Set result = new HashSet<>(initial);
        for (Repository repo : repositories) {
            RemoteRepository remote = repository(repo);
            if (remote != null) {
                result.add(remote);
            }
        }
        return result;
    }

    private record ResolvableArtifacts(List artifacts, Set repos) {
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy