io.sundr.maven.GenerateBomMojo.java Source code

Java tutorial

Introduction

Here is the source code for io.sundr.maven.GenerateBomMojo.java

Source

/*
 * Copyright 2015 The original authors.
 *
 *    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.sundr.maven;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;

import io.sundr.codegen.generator.CodeGeneratorBuilder;
import io.sundr.maven.filter.Filters;

import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.DefaultArtifact;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.artifact.resolver.ArtifactResolutionRequest;
import org.apache.maven.artifact.resolver.ArtifactResolutionResult;
import org.apache.maven.artifact.resolver.ArtifactResolver;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.lifecycle.internal.LifecycleModuleBuilder;
import org.apache.maven.lifecycle.internal.LifecycleTaskSegmentCalculator;
import org.apache.maven.lifecycle.internal.ProjectIndex;
import org.apache.maven.lifecycle.internal.ReactorBuildStatus;
import org.apache.maven.lifecycle.internal.ReactorContext;
import org.apache.maven.lifecycle.internal.TaskSegment;
import org.apache.maven.model.Build;
import org.apache.maven.model.Dependency;
import org.apache.maven.model.DependencyManagement;
import org.apache.maven.model.Model;
import org.apache.maven.model.Plugin;
import org.apache.maven.model.PluginManagement;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.repository.RemoteRepository;

@Mojo(name = "generate-bom", inheritByDefault = false, defaultPhase = LifecyclePhase.VALIDATE)
public class GenerateBomMojo extends AbstractSundrioMojo {

    private static final Set<String> GENERATED_ARTIFACT_IDS = Collections.synchronizedSet(new HashSet<String>());

    @Component
    private ArtifactResolver artifactResolver;

    @Component
    private LifecycleModuleBuilder builder;

    @Component
    private LifecycleTaskSegmentCalculator segmentCalculator;

    @Component
    private RepositorySystem aetherSystem;

    @Parameter(defaultValue = "${repositorySystemSession}", readonly = true, required = true)
    private RepositorySystemSession aetherSession;

    @Parameter(defaultValue = "${project.remoteProjectRepositories}", readonly = true, required = true)
    private List<RemoteRepository> aetherRemoteRepositories;

    /**
     * Location of the localRepository repository.
     */
    @Parameter(defaultValue = "${localRepository}", readonly = true, required = true)
    private ArtifactRepository localRepository;

    /**
     * List of Remote Repositories used by the resolver
     */
    @Parameter(defaultValue = "${project.remoteArtifactRepositories}", readonly = true, required = true)
    protected List<ArtifactRepository> remoteRepositories;

    @Parameter
    private BomConfig[] boms;

    @Parameter(defaultValue = "${reactorProjects}", required = true, readonly = false)
    private List<MavenProject> reactorProjects;

    @Parameter(defaultValue = "${maven.version}")
    private String mavenVersion;

    @Parameter(defaultValue = "${bom.template.resource}")
    private String bomTemplateResource = "templates/bom.xml.vm";

    @Parameter(defaultValue = "${bom.template.url}")
    private URL bomTemplateUrl;

    @Override
    public void execute() throws MojoExecutionException, MojoFailureException {
        if (getProject().isExecutionRoot() && !getProject().getModules().isEmpty()) {
            List<MavenProject> updated = new LinkedList<MavenProject>();

            Map<BomConfig, MavenProject> generated = new HashMap<BomConfig, MavenProject>();
            updated.add(getProject());
            if (boms == null || boms.length == 0) {
                String artifactId = getProject().getArtifactId() + "-bom";
                if (GENERATED_ARTIFACT_IDS.add(artifactId)) {
                    BomConfig cfg = new BomConfig(artifactId, getProject().getName() + " Bom", " Generated bom");
                    MavenProject bomProject = generateBom(cfg);
                    generated.put(cfg, bomProject);
                    updated.add(bomProject);
                }
            } else {
                for (BomConfig cfg : boms) {
                    if (GENERATED_ARTIFACT_IDS.add(cfg.getArtifactId())) {
                        MavenProject bomProject = generateBom(cfg);
                        generated.put(cfg, bomProject);
                        updated.add(bomProject);
                    }
                }
            }

            updated.addAll(getAllButCurrent());
            for (Map.Entry<BomConfig, MavenProject> entry : generated.entrySet()) {
                build(getSession().clone(), entry.getValue(), updated, entry.getKey().getGoals());
            }
        }
    }

    private MavenProject generateBom(BomConfig config) throws MojoFailureException, MojoExecutionException {
        File outputDir = new File(getProject().getBuild().getOutputDirectory());
        File bomDir = new File(outputDir, config.getArtifactId());
        File generatedBom = new File(bomDir, BOM_NAME);

        if (!bomDir.exists() && !bomDir.mkdirs()) {
            throw new MojoFailureException("Failed to create output dir for bom:" + bomDir.getAbsolutePath());
        }
        preProccessConfig(config);
        FileWriter writer = null;
        try {
            writer = new FileWriter(generatedBom);
            // Imported dependencies may have important additional information (eg. exclusions)
            // Taking both the artifacts and their related dependencies
            Map<Artifact, Dependency> dependencies = new LinkedHashMap<Artifact, Dependency>();
            Set<Artifact> plugins = new LinkedHashSet<Artifact>();

            Set<Artifact> ownModules = Filters.filter(getReactorArtifacts(), Filters.createModulesFilter(config));

            //We add first project management and unwrapped boms. (we don't resolve those).
            Set<Artifact> dependencyManagementArtifacts = config.isInheritDependencyManagement()
                    ? getProjectDependencyManagement()
                    : new LinkedHashSet<Artifact>();
            Set<Artifact> pluginManagementArtifacts = config.isInheritPluginManagement()
                    ? getProjectPluginManagement()
                    : new LinkedHashSet<Artifact>();

            Set<Artifact> allDependencies = new LinkedHashSet<Artifact>(dependencyManagementArtifacts);
            allDependencies.addAll(getDependencies(getProjectDependencies())); //We resolve transitives here....

            //Populate dependencies
            dependencies.putAll(toDependencyMap(ownModules));
            dependencies.putAll(toDependencyMap(
                    Filters.filter(allDependencies, Filters.createDependencyFilter(getSession(), config))));

            //Populate dependencies with imported boms.
            ExternalBomResolver bomResolver = new ExternalBomResolver(getSession(), getArtifactHandler(),
                    aetherSystem, aetherSession, aetherRemoteRepositories, getLog());
            Map<Artifact, Dependency> externalDependencies = bomResolver.resolve(config);
            dependencies.putAll(externalDependencies);

            //Populate plugins
            plugins.addAll(Filters.filter(ownModules, Filters.MAVEN_PLUGIN_FILTER));
            plugins.addAll(
                    Filters.filter(pluginManagementArtifacts, Filters.createPluginFilter(getSession(), config)));

            //Checking version mismatches.
            MavenProject projectToGenerate = toGenerate(getProject(), config, dependencies.values(), plugins);
            verifyBomDependencies(config, projectToGenerate);

            getLog().info("Generating BOM: " + config.getArtifactId());
            new CodeGeneratorBuilder<Model>().withWriter(writer).withModel(projectToGenerate.getModel())
                    .withTemplateResource(bomTemplateResource).withTemplateUrl(bomTemplateUrl).build().generate();

            return toBuild(getProject(), config);

        } catch (Exception e) {
            throw new MojoFailureException("Failed to generate bom.", e);
        } finally {
            try {
                if (writer != null) {
                    writer.close();
                }
            } catch (IOException e) {
                throw new MojoExecutionException("Failed to close the generated bom writer", e);
            }
        }
    }

    private void verifyBomDependencies(BomConfig config, MavenProject project) throws MojoFailureException {
        if (!config.isCheckMismatches()) {
            return;
        }

        Map<String, Set<String>> versions = new TreeMap<String, Set<String>>();
        Set<String> mismatches = new TreeSet<String>();
        if (project.getDependencyManagement() != null
                && project.getDependencyManagement().getDependencies() != null) {
            for (Dependency dependency : project.getDependencyManagement().getDependencies()) {
                String key = dependencyKey(dependency);
                String version = dependency.getVersion();
                if (!versions.containsKey(key)) {
                    versions.put(key, new TreeSet<String>());
                }
                for (String otherVersion : versions.get(key)) {
                    if (!version.equals(otherVersion)) {
                        mismatches.add(key);
                        break;
                    }
                }
                versions.get(key).add(version);
            }
        }

        if (mismatches.size() > 0) {
            StringBuilder message = new StringBuilder();
            message.append("The BOM " + config.getArtifactId()
                    + " contains multiple versions of the following dependencies:\n");
            for (String key : mismatches) {
                message.append(" - " + key + " versions " + versions.get(key) + "\n");
            }

            if (config.isFailOnMismatch()) {
                throw new MojoFailureException(message.toString());
            } else {
                getLog().warn(message.toString());
            }
        }
    }

    private Set<Artifact> getReactorArtifacts() {
        Set<Artifact> reactorArtifacts = new LinkedHashSet<Artifact>();
        for (MavenProject project : reactorProjects) {
            reactorArtifacts.add(project.getArtifact());
        }
        return reactorArtifacts;
    }

    /**
     * Returns the model of the {@link org.apache.maven.project.MavenProject} to generate.
     * This is a trimmed down version and contains just the stuff that need to go into the bom.
     *
     * @param project The source {@link org.apache.maven.project.MavenProject}.
     * @param config  The {@link io.sundr.maven.BomConfig}.
     * @return The build {@link org.apache.maven.project.MavenProject}.
     */
    private static MavenProject toGenerate(MavenProject project, BomConfig config,
            Collection<Dependency> dependencies, Set<Artifact> plugins) {
        MavenProject toGenerate = project.clone();
        toGenerate.setGroupId(project.getGroupId());
        toGenerate.setArtifactId(config.getArtifactId());
        toGenerate.setVersion(project.getVersion());
        toGenerate.setPackaging("pom");
        toGenerate.setName(config.getName());
        toGenerate.setDescription(config.getDescription());

        toGenerate.setUrl(project.getUrl());
        toGenerate.setLicenses(project.getLicenses());
        toGenerate.setScm(project.getScm());
        toGenerate.setDevelopers(project.getDevelopers());

        toGenerate.getModel().setDependencyManagement(new DependencyManagement());
        for (Dependency dependency : dependencies) {
            toGenerate.getDependencyManagement().addDependency(dependency);
        }

        toGenerate.getModel().setBuild(new Build());
        if (!plugins.isEmpty()) {
            toGenerate.getModel().setBuild(new Build());
            toGenerate.getModel().getBuild().setPluginManagement(new PluginManagement());
            for (Artifact artifact : plugins) {
                toGenerate.getPluginManagement().addPlugin(toPlugin(artifact));
            }
        }

        return toGenerate;
    }

    /**
     * Returns the generated {@link org.apache.maven.project.MavenProject} to build.
     * This version of the project contains all the stuff needed for building (parents, profiles, properties etc).
     *
     * @param project The source {@link org.apache.maven.project.MavenProject}.
     * @param config  The {@link io.sundr.maven.BomConfig}.
     * @return The build {@link org.apache.maven.project.MavenProject}.
     */
    private static MavenProject toBuild(MavenProject project, BomConfig config) {
        File outputDir = new File(project.getBuild().getOutputDirectory());
        File bomDir = new File(outputDir, config.getArtifactId());
        File generatedBom = new File(bomDir, BOM_NAME);

        MavenProject toBuild = project.clone();
        //we want to avoid recursive "generate-bom".
        toBuild.setExecutionRoot(false);
        toBuild.setFile(generatedBom);
        toBuild.getModel().setPomFile(generatedBom);
        toBuild.setModelVersion(project.getModelVersion());

        toBuild.setArtifact(new DefaultArtifact(project.getGroupId(), config.getArtifactId(), project.getVersion(),
                project.getArtifact().getScope(), project.getArtifact().getType(),
                project.getArtifact().getClassifier(), project.getArtifact().getArtifactHandler()));

        toBuild.setParent(project.getParent());
        toBuild.getModel().setParent(project.getModel().getParent());

        toBuild.setGroupId(project.getGroupId());
        toBuild.setArtifactId(config.getArtifactId());
        toBuild.setVersion(project.getVersion());
        toBuild.setPackaging("pom");
        toBuild.setName(config.getName());
        toBuild.setDescription(config.getDescription());

        toBuild.setUrl(project.getUrl());
        toBuild.setLicenses(project.getLicenses());
        toBuild.setScm(project.getScm());
        toBuild.setDevelopers(project.getDevelopers());
        toBuild.setDistributionManagement(project.getDistributionManagement());
        toBuild.getModel().setProfiles(project.getModel().getProfiles());

        //We want to avoid having the generated stuff wiped.
        toBuild.getProperties().put("clean.skip", "true");
        toBuild.getModel().getBuild().setDirectory(bomDir.getAbsolutePath());
        toBuild.getModel().getBuild().setOutputDirectory(new File(bomDir, "target").getAbsolutePath());
        for (String key : config.getProperties().stringPropertyNames()) {
            toBuild.getProperties().put(key, config.getProperties().getProperty(key));
        }
        return toBuild;
    }

    private List<MavenProject> getAllButCurrent() {
        List<MavenProject> result = new LinkedList<MavenProject>(getSession().getProjects());
        result.remove(getSession().getCurrentProject());
        return result;
    }

    private void build(MavenSession session, MavenProject project, List<MavenProject> allProjects, GoalSet goals)
            throws MojoExecutionException {
        session.setProjects(allProjects);
        ProjectIndex projectIndex = new ProjectIndex(session.getProjects());
        try {
            ReactorBuildStatus reactorBuildStatus = new ReactorBuildStatus(
                    new BomDependencyGraph(session.getProjects()));
            ReactorContext reactorContext = new ReactorContextFactory(new MavenVersion(mavenVersion)).create(
                    session.getResult(), projectIndex, Thread.currentThread().getContextClassLoader(),
                    reactorBuildStatus, builder);
            List<TaskSegment> segments = segmentCalculator.calculateTaskSegments(session);
            for (TaskSegment segment : segments) {
                builder.buildProject(session, reactorContext, project, filterSegment(segment, goals));
            }
        } catch (Throwable t) {
            throw new MojoExecutionException("Error building generated bom:" + project.getArtifactId(), t);
        }
    }

    /**
     * Returns all the session/reactor artifacts topologically sorted.
     *
     * @return
     */
    private Set<Artifact> getSessionArtifacts() {
        Set<Artifact> result = new LinkedHashSet<Artifact>();
        for (MavenProject p : getSession().getProjectDependencyGraph().getSortedProjects()) {
            result.add(p.getArtifact());
        }
        return result;
    }

    /**
     * Returns all dependency artifacts in all modules, excluding all reactor artifacts (including attached).
     *
     * @return
     */
    private Set<Artifact> getProjectDependencies() {
        Set<Artifact> result = new LinkedHashSet<Artifact>();
        for (MavenProject p : getSession().getProjectDependencyGraph().getSortedProjects()) {
            for (Dependency dependency : p.getDependencies()) {
                result.add(toArtifact(dependency));
            }
        }
        return result;
    }

    /**
     * Returns all dependencies defined in dependency management of the root pom.
     *
     * @return
     */
    private Set<Artifact> getProjectDependencyManagement() {
        Set<Artifact> result = new LinkedHashSet<Artifact>();
        DependencyManagement dependencyManagement = getProject().getDependencyManagement();
        if (dependencyManagement != null) {
            for (Dependency dependency : dependencyManagement.getDependencies()) {
                result.add(toArtifact(dependency));
            }
        }
        return result;
    }

    /**
     * Returns all dependencies defined in dependency management of the root pom.
     *
     * @return
     */
    private Set<Artifact> getProjectPluginManagement() {
        Set<Artifact> result = new LinkedHashSet<Artifact>();
        PluginManagement pluginManagement = getProject().getPluginManagement();
        if (pluginManagement != null) {
            for (Plugin plugin : pluginManagement.getPlugins()) {
                result.add(toArtifact(plugin));
            }
        }
        return result;
    }

    /**
     * Collects dependencies, including transitives.
     * Project dependencies retain their scope, while test only dependencies (including transitives) will have test scope.
     * @param projectDependencies
     * @return
     */
    private Set<Artifact> getDependencies(final Set<Artifact> projectDependencies) {
        Set<Artifact> result = new LinkedHashSet<Artifact>(projectDependencies);
        Set<Artifact> testDependencies = dependenciesWithScope(projectDependencies, Artifact.SCOPE_TEST);
        Set<Artifact> nonTestDependencies = allBut(projectDependencies, testDependencies);

        Set<Artifact> testTransitives = resolve(testDependencies);
        Set<Artifact> nonTestTransitives = resolve(nonTestDependencies);

        Set<Artifact> testOnlyDependencies = allBut(testTransitives, nonTestTransitives);

        for (Artifact testOnly : testOnlyDependencies) {
            result.add(new DefaultArtifact(testOnly.getGroupId(), testOnly.getArtifactId(), testOnly.getVersion(),
                    Artifact.SCOPE_TEST, testOnly.getType(), testOnly.getClassifier(),
                    testOnly.getArtifactHandler()));
        }

        result.addAll(resolve(projectDependencies));
        return result;
    }

    private Set<Artifact> resolve(final Set<Artifact> dependencies) {
        ArtifactResolutionRequest request = new ArtifactResolutionRequest();
        request.setArtifact(getProject().getArtifact());
        request.setArtifactDependencies(dependencies);
        request.setLocalRepository(localRepository);
        request.setRemoteRepositories(remoteRepositories);
        request.setManagedVersionMap(getProject().getManagedVersionMap());
        request.setResolveTransitively(true);
        ArtifactResolutionResult result = artifactResolver.resolve(request);
        return result.getArtifacts();
    }

    private Artifact toArtifact(Dependency dependency) {
        return new DefaultArtifact(dependency.getGroupId(), dependency.getArtifactId(), dependency.getVersion(),
                dependency.getScope(), dependency.getType(), dependency.getClassifier(), getArtifactHandler());
    }

    private Artifact toArtifact(Plugin plugin) {
        return new DefaultArtifact(plugin.getGroupId(), plugin.getArtifactId(), plugin.getVersion(), null,
                Constants.MAVEN_PLUGIN_TYPE, null, getArtifactHandler());
    }

    private static Map<Artifact, Dependency> toDependencyMap(Collection<Artifact> artifacts) {
        Map<Artifact, Dependency> dependencyMap = new LinkedHashMap<Artifact, Dependency>();
        if (artifacts != null) {
            for (Artifact artifact : artifacts) {
                Dependency dependency = toDependency(artifact);
                dependencyMap.put(artifact, dependency);
            }
        }
        return dependencyMap;
    }

    private static Dependency toDependency(Artifact artifact) {
        Dependency dependency = new Dependency();
        dependency.setGroupId(artifact.getGroupId());
        dependency.setArtifactId(artifact.getArtifactId());
        dependency.setVersion(artifact.getVersion());
        dependency.setType(artifact.getType());
        dependency.setScope(Artifact.SCOPE_COMPILE.equals(artifact.getScope()) ? null : artifact.getScope());
        dependency.setClassifier(artifact.getClassifier());
        dependency.setOptional(artifact.isOptional());
        return dependency;
    }

    private static Plugin toPlugin(Artifact artifact) {
        Plugin plugin = new Plugin();
        plugin.setGroupId(artifact.getGroupId());
        plugin.setArtifactId(artifact.getArtifactId());
        plugin.setVersion(artifact.getVersion());
        return plugin;
    }

    private static TaskSegment filterSegment(TaskSegment segment, GoalSet goals) {
        List<Object> filtered = new ArrayList<Object>();

        Set<String> includes = goals.getIncludes();
        Set<String> excludes = goals.getExcludes();

        for (Object obj : segment.getTasks()) {
            String name = Reflections.readAnyField(obj, "pluginGoal", "lifecyclePhase");

            if (!excludes.contains(name) && (includes.contains(name) || includes.isEmpty())) {
                filtered.add(obj);
            }
        }
        return new TaskSegment(segment.isAggregating(), filtered.toArray());
    }

    private static void preProccessConfig(BomConfig config) {
        if (config.getModules().getIncludes().isEmpty()) {
            config.getModules().getIncludes().add("*:*");
        }
    }

    private static Set<Artifact> dependenciesWithScope(final Set<Artifact> dependencies, String scope) {
        Set<Artifact> result = new LinkedHashSet<Artifact>();
        for (Artifact artifact : dependencies) {
            if (scope.equals(artifact.getScope())) {
                result.add(artifact);
            }
        }
        return result;
    }

    private static Set<Artifact> allBut(Set<Artifact> source, Set<Artifact> exclusions) {
        Set<Artifact> result = new LinkedHashSet<Artifact>(source);
        result.removeAll(exclusions);
        return result;
    }

    private static String dependencyKey(Dependency dependency) {
        return dependency.getGroupId() + ":" + dependency.getArtifactId() + ":" + dependency.getType() + ":"
                + dependency.getClassifier();
    }

}