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

org.basepom.inline.mojo.InlineMojo Maven / Gradle / Ivy

There is a newer version: 1.5.0
Show newest version
/*
 * 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 org.basepom.inline.mojo;

import static com.google.common.base.Preconditions.checkState;
import static java.lang.String.format;
import static java.nio.file.StandardCopyOption.ATOMIC_MOVE;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;

import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import java.util.stream.Collectors;
import javax.xml.stream.XMLStreamException;

import com.google.common.base.Functions;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.Iterables;
import com.google.common.io.CharStreams;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
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.plugins.annotations.ResolutionScope;
import org.apache.maven.project.DependencyResolutionException;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.MavenProjectHelper;
import org.apache.maven.project.ProjectBuilder;
import org.apache.maven.project.ProjectBuildingException;
import org.apache.maven.project.ProjectDependenciesResolver;
import org.basepom.inline.transformer.ClassPath;
import org.basepom.inline.transformer.ClassPathResource;
import org.basepom.inline.transformer.ClassPathTag;
import org.basepom.inline.transformer.JarTransformer;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.artifact.Artifact;
import org.eclipse.aether.graph.Dependency;
import org.eclipse.aether.util.artifact.JavaScopes;
import org.jdom2.JDOMException;

/**
 * Inlines one or more dependencies of the project, relocated the classes and writes a new artifact.
 */
@Mojo(name = "inline", defaultPhase = LifecyclePhase.PACKAGE,
        requiresProject = true, threadSafe = true,
        requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME,
        requiresDependencyCollection = ResolutionScope.COMPILE_PLUS_RUNTIME)
public final class InlineMojo extends AbstractMojo {

    private static final PluginLog LOG = new PluginLog(InlineMojo.class);

    @Parameter(defaultValue = "${project}", readonly = true, required = true)
    public MavenProject project;

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

    @Parameter(defaultValue = "${reactorProjects}", readonly = true, required = true)
    public List reactorProjects;

    @Component
    public ProjectBuilder mavenProjectBuilder;

    @Component
    public ProjectDependenciesResolver projectDependenciesResolver;

    @Component
    public RepositorySystem repositorySystem;

    @Component
    public MavenProjectHelper projectHelper;

    /**
     * The destination directory for the inlined artifact.
     */
    @Parameter(defaultValue = "${project.build.directory}")
    public File outputDirectory = null;

    /**
     * The POM file to use.
     */
    @Parameter(property = "inline.pomFile", defaultValue = "${project.file}")
    public File pomFile = null;

    /**
     * List of dependencies to inline.
     */
    @Parameter
    private List inlineDependencies = ImmutableList.of();

    // called by maven
    public void setInlineDependencies(List inlineDependencies) {
        this.inlineDependencies = ImmutableList.copyOf(inlineDependencies);
    }

    /**
     * List of things to include.
     */
    @Parameter
    private List includes = ImmutableList.of();

    // called by maven
    public void setIncludes(List includes) {
        this.includes = ImmutableList.copyOf(includes);
    }

    /**
     * List of things to exclude.
     */
    @Parameter
    private List excludes = ImmutableList.of();

    // called by maven
    public void setExcludes(List excludes) {
        this.excludes = ImmutableList.copyOf(excludes);
    }

    /**
     * Hide inlined classes from IDE autocompletion.
     */
    @Parameter(defaultValue = "true", property = "inline.hide-classes")
    public boolean hideClasses = true;

    /**
     * Skip the execution.
     */
    @Parameter(defaultValue = "false", property = "inline.skip")
    public boolean skip = false;

    /**
     * Silence all non-output and non-error messages.
     */
    @Parameter(defaultValue = "false", property = "inline.quiet")
    public boolean quiet = false;

    /**
     * Defines the package prefix for all relocated classes. This prefix must be a valid package name. All relocated classes are put under this prefix.
     */
    @Parameter(required = true, property = "inline.prefix")
    public String prefix = null;

    /**
     * If true, requires the dependencies to inline to be defined in scope 
provided
. This is good practice as it allows the unchanged pom to be used * with the inlined artifact. */ @Parameter(defaultValue = "true", property = "inline.requireProvided") public boolean requireProvided = true; /** * If true, require the dependencies to inline to be defined as
optional
. This is good practice as it allows the unchanged pom to be used with * the inlined artifact. */ @Parameter(defaultValue = "true", property = "inline.requireOptional") public boolean requireOptional = true; /** * Fail if an inline dependency is defined but the corresponding dependency is not actually found. */ @Parameter(defaultValue = "true", property = "inline.failOnNoMatch") public boolean failOnNoMatch = true; /** * The path to the output file for the inlined artifact. When this parameter is set, the created archive will neither replace the project's main artifact * nor will it be attached. Hence, this parameter causes the parameters {@link #inlinedArtifactAttached}, {@link #inlinedClassifierName} to be ignored when * used. */ @Parameter public File outputJarFile = null; /** * The path to the output file for the new POM file. When this parameter is set, the created pom file will not replace the project's pom file. */ @Parameter public File outputPomFile = null; /** * If true, attach the inlined artifact, if false replace the original artifact. */ @Parameter(defaultValue = "false", property = "inline.attachArtifact") public boolean inlinedArtifactAttached = false; /** * If true, replace the POM file with a new version that has all inlined dependencies removed. It is possible to write a POM file that works to build the * jar with inlined dependencies and then use the same POM file for the resulting artifact (by having all dependencies marked as
provided
and * ensure that those dependencies do not have additional, transitive dependencies. This tends to be error prone and it is recommended to have the plugin * rewrite the POM file. */ @Parameter(defaultValue = "true", property = "inline.replacePomFile") public boolean replacePomFile = true; /** * The name of the classifier used in case the inlined artifact is attached. */ @Parameter(defaultValue = "inlined") public String inlinedClassifierName = "inlined"; @Override public void execute() throws MojoExecutionException { if (this.skip) { LOG.report(quiet, "skipping plugin execution"); return; } if ("pom".equals(project.getPackaging())) { LOG.report(quiet, "ignoring POM project"); return; } if (project.getArtifact().getFile() == null) { throw new MojoExecutionException("No project artifact found!"); } try { ImmutableSetMultimap.Builder dependencyBuilder = ImmutableSetMultimap.builder(); ImmutableSet.Builder pomDependenciesToAdd = ImmutableSet.builder(); computeDependencyMap(dependencyBuilder, pomDependenciesToAdd); ImmutableSetMultimap dependencyMap = dependencyBuilder.build(); rewriteJarFile(dependencyMap); rewritePomFile(pomDependenciesToAdd.build(), ImmutableSet.copyOf(dependencyMap.values())); } catch (UncheckedIOException e) { throw new MojoExecutionException(e.getCause()); } catch (IOException | DependencyResolutionException | ProjectBuildingException | XMLStreamException | JDOMException e) { throw new MojoExecutionException(e); } } private void computeDependencyMap( ImmutableSetMultimap.Builder dependencyMapBuilder, ImmutableSet.Builder pomBuilder) throws DependencyResolutionException, ProjectBuildingException { DependencyBuilder dependencyBuilder = new DependencyBuilder(project, mavenSession, mavenProjectBuilder, projectDependenciesResolver, reactorProjects); ImmutableSet directArtifacts = project.getDependencyArtifacts().stream() .map(ArtifactIdentifier::new) .collect(ImmutableSet.toImmutableSet()); ImmutableList directDependencies = dependencyBuilder.mapProject(project, (node, parents) -> directArtifacts.contains(new ArtifactIdentifier(node))); // build the full set of dependencies with all scopes and everything. ImmutableList projectDependencies = dependencyBuilder.mapProject(project, ScopeLimitingFilter.computeDependencyScope(ScopeLimitingFilter.COMPILE_PLUS_RUNTIME)); Map dependencyScopes = projectDependencies.stream() .filter(dependency -> dependency.getArtifact() != null) .collect(ImmutableMap.toImmutableMap(this::getId, Functions.identity())); BiConsumer dependencyConsumer = (inlineDependency, dependency) -> { LOG.debug("%s matches %s for inlining.", inlineDependency, dependency); dependencyMapBuilder.put(inlineDependency, dependency); }; ImmutableSet.Builder directExcludes = ImmutableSet.builder(); // first find all the direct dependencies. Add anything that is not hit to the additional exclude list directLoop: for (Dependency dependencyArtifact : directDependencies) { for (InlineDependency inlineDependency : inlineDependencies) { if (inlineDependency.matchDependency(dependencyArtifact)) { dependencyConsumer.accept(inlineDependency, dependencyArtifact); LOG.report(quiet, "Inlining direct dependency %s (matched %s)", dependencyArtifact, inlineDependency); continue directLoop; } } directExcludes.add(dependencyArtifact); } Set excludes = directExcludes.build().stream() .map(ArtifactIdentifier::new) .collect(Collectors.toUnmodifiableSet()); this.excludes = ImmutableList.copyOf(Iterables.concat(this.excludes, excludes)); LOG.debug("Excludes after creating includes: %s", this.excludes); var directDependencyMap = dependencyMapBuilder.build().asMap(); for (var dependencyEntry : directDependencyMap.entrySet()) { InlineDependency inlineDependency = dependencyEntry.getKey(); for (Dependency projectDependency : dependencyEntry.getValue()) { Consumer consumer; if (inlineDependency.isInlineTransitive()) { // transitive deps are added to the jar consumer = dependency -> { // runtime dependencies end up being dependencies of the jar with inlines. if (JavaScopes.RUNTIME.equals(dependency.getScope())) { pomBuilder.add(dependency); } else { dependencyConsumer.accept(inlineDependency, dependency); LOG.report(quiet, "Inlining transitive dependency %s (matched %s)", dependency, inlineDependency); } }; } else { // non-transitive deps need to be written into the POM. consumer = pomBuilder::add; } dependencyBuilder.mapDependency(projectDependency, ScopeLimitingFilter.computeTransitiveScope(projectDependency.getScope())) .stream() // replace deps in the transitive set with deps in the root set if present (will // override the scope here with the root scope) .map(dependency -> dependencyScopes.getOrDefault(getId(dependency), dependency)) // remove everything that is excluded by the filter set .filter(createFilterSet()) // make sure that the inline dependency actually pulls the dep in. .filter(dependency -> isIncluded(inlineDependency, dependency)) .forEach(consumer); } } LOG.debug("Done!"); } private String getId(Dependency dependency) { Artifact artifact = dependency.getArtifact(); checkState(artifact != null, "Artifact for dependency %s is null!", dependency); return Joiner.on(':').join(artifact.getGroupId(), artifact.getArtifactId(), artifact.getClassifier()); } private Predicate createFilterSet() { // filter system scope dependencies. Those are never inlined. Predicate predicate = dependency -> !JavaScopes.SYSTEM.equals(dependency.getScope()); // filter all provided unless allowed by the flag if (this.requireProvided) { predicate = predicate.and(dependency -> JavaScopes.PROVIDED.equals(dependency.getScope())); } // filter all optional unless allowed by the flag if (this.requireOptional) { predicate = predicate.and(Dependency::isOptional); } return predicate; } private void rewriteJarFile(ImmutableSetMultimap dependencies) throws IOException { File outputJar = (this.outputJarFile != null) ? outputJarFile : inlinedArtifactFileWithClassifier(); doJarTransformation(outputJar, dependencies); if (this.outputJarFile == null) { if (this.inlinedArtifactAttached) { LOG.info("Attaching inlined artifact."); projectHelper.attachArtifact(project, project.getArtifact().getType(), inlinedClassifierName, outputJar); } else { LOG.info("Replacing original artifact with inlined artifact."); File originalArtifact = project.getArtifact().getFile(); if (originalArtifact != null) { File backupFile = new File(originalArtifact.getParentFile(), "original-" + originalArtifact.getName()); Files.move(originalArtifact.toPath(), backupFile.toPath(), ATOMIC_MOVE, REPLACE_EXISTING); Files.move(outputJar.toPath(), originalArtifact.toPath(), ATOMIC_MOVE, REPLACE_EXISTING); } } } } private void rewritePomFile(Set dependenciesToAdd, Set dependenciesToRemove) throws IOException, XMLStreamException, JDOMException { String pomContents; try (InputStreamReader reader = new FileReader(project.getFile(), StandardCharsets.UTF_8)) { pomContents = CharStreams.toString(reader); } PomUtil pomUtil = new PomUtil(pomContents); dependenciesToRemove.forEach(pomUtil::removeDependency); dependenciesToAdd.forEach(pomUtil::addDependency); // some rewriters (maven flatten plugin) rewrites the new pom name as a hidden file. String pomName = this.pomFile.getName(); pomName = "new-" + (pomName.startsWith(".") ? pomName.substring(1) : pomName); File newPomFile = this.outputPomFile != null ? outputPomFile : new File(this.outputDirectory, pomName); try (OutputStreamWriter writer = new FileWriter(newPomFile, StandardCharsets.UTF_8)) { pomUtil.writePom(writer); } if (this.replacePomFile) { project.setPomFile(newPomFile); } } private void doJarTransformation(File outputJar, ImmutableSetMultimap dependencies) throws IOException { try (JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(outputJar))) { Consumer jarConsumer = getJarWriter(jarOutputStream); JarTransformer transformer = new JarTransformer(jarConsumer); // Build the class path ClassPath classPath = new ClassPath(project.getBasedir()); // maintain the manifest file for the main artifact var artifact = project.getArtifact(); classPath.addFile(artifact.getFile(), artifact.getGroupId(), artifact.getArtifactId(), ClassPathTag.ROOT_JAR); dependencies.forEach( (inlineDependency, dependency) -> { var dependencyArtifact = dependency.getArtifact(); checkState(dependencyArtifact.getFile() != null, "Could not locate artifact file for %s", dependencyArtifact); classPath.addFile(dependencyArtifact.getFile(), prefix, dependencyArtifact.getGroupId(), dependencyArtifact.getArtifactId(), hideClasses);}); transformer.transform(classPath); } } private Consumer getJarWriter(JarOutputStream jarOutputStream) { return classPathResource -> { try { String name = classPathResource.getName(); LOG.debug(format("Writing '%s' to jar", name)); JarEntry outputEntry = new JarEntry(name); outputEntry.setTime(classPathResource.getLastModifiedTime()); outputEntry.setCompressedSize(-1); jarOutputStream.putNextEntry(outputEntry); jarOutputStream.write(classPathResource.getContent()); } catch (IOException e) { throw new UncheckedIOException(e); } }; } private File inlinedArtifactFileWithClassifier() { final var artifact = project.getArtifact(); String inlineName = String.format("%s-%s-%s.%s", project.getArtifactId(), artifact.getVersion(), this.inlinedClassifierName, artifact.getArtifactHandler().getExtension()); return new File(this.outputDirectory, inlineName); } public boolean isIncluded(InlineDependency inlineDependency, Dependency dependency) { // exclude optional dependencies unless explicitly included. if (dependency.isOptional() && !inlineDependency.isInlineOptionals()) { return false; } boolean included = this.includes.stream() .map(artifactIdentifier -> artifactIdentifier.matchDependency(dependency)) .findFirst() .orElse(this.includes.isEmpty()); boolean excluded = this.excludes.stream() .map(artifactIdentifier -> artifactIdentifier.matchDependency(dependency)) .findFirst() .orElse(false); return included && !excluded; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy