View Javadoc
1   /*
2    * Licensed under the Apache License, Version 2.0 (the "License");
3    * you may not use this file except in compliance with the License.
4    * You may obtain a copy of the License at
5    *
6    * http://www.apache.org/licenses/LICENSE-2.0
7    *
8    * Unless required by applicable law or agreed to in writing, software
9    * distributed under the License is distributed on an "AS IS" BASIS,
10   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11   * See the License for the specific language governing permissions and
12   * limitations under the License.
13   */
14  
15  package org.basepom.inline.mojo;
16  
17  import static com.google.common.base.Preconditions.checkState;
18  import static java.lang.String.format;
19  import static java.nio.file.StandardCopyOption.ATOMIC_MOVE;
20  import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
21  
22  import org.basepom.inline.transformer.ClassPath;
23  import org.basepom.inline.transformer.ClassPathResource;
24  import org.basepom.inline.transformer.ClassPathTag;
25  import org.basepom.inline.transformer.JarTransformer;
26  import org.basepom.inline.transformer.TransformerException;
27  
28  import java.io.BufferedReader;
29  import java.io.BufferedWriter;
30  import java.io.File;
31  import java.io.IOException;
32  import java.io.UncheckedIOException;
33  import java.nio.charset.StandardCharsets;
34  import java.nio.file.Files;
35  import java.util.List;
36  import java.util.Map;
37  import java.util.Optional;
38  import java.util.Set;
39  import java.util.function.BiConsumer;
40  import java.util.function.Consumer;
41  import java.util.function.Predicate;
42  import java.util.jar.JarEntry;
43  import java.util.jar.JarOutputStream;
44  import java.util.stream.Collectors;
45  import javax.xml.stream.XMLStreamException;
46  
47  import com.google.common.base.Functions;
48  import com.google.common.base.Joiner;
49  import com.google.common.collect.ImmutableList;
50  import com.google.common.collect.ImmutableMap;
51  import com.google.common.collect.ImmutableSet;
52  import com.google.common.collect.ImmutableSetMultimap;
53  import com.google.common.collect.ImmutableSortedSet;
54  import com.google.common.collect.Iterables;
55  import com.google.common.collect.Sets;
56  import com.google.common.io.CharStreams;
57  import org.apache.maven.execution.MavenSession;
58  import org.apache.maven.plugin.AbstractMojo;
59  import org.apache.maven.plugin.MojoExecutionException;
60  import org.apache.maven.plugins.annotations.Component;
61  import org.apache.maven.plugins.annotations.LifecyclePhase;
62  import org.apache.maven.plugins.annotations.Mojo;
63  import org.apache.maven.plugins.annotations.Parameter;
64  import org.apache.maven.plugins.annotations.ResolutionScope;
65  import org.apache.maven.project.DependencyResolutionException;
66  import org.apache.maven.project.MavenProject;
67  import org.apache.maven.project.MavenProjectHelper;
68  import org.apache.maven.project.ProjectBuilder;
69  import org.apache.maven.project.ProjectBuildingException;
70  import org.apache.maven.project.ProjectDependenciesResolver;
71  import org.eclipse.aether.RepositorySystem;
72  import org.eclipse.aether.artifact.Artifact;
73  import org.eclipse.aether.graph.Dependency;
74  import org.eclipse.aether.util.artifact.JavaScopes;
75  import org.jdom2.JDOMException;
76  
77  /**
78   * Inlines one or more dependencies of the project, relocated the classes and writes a new artifact.
79   */
80  @Mojo(name = "inline", defaultPhase = LifecyclePhase.PACKAGE,
81          requiresProject = true, threadSafe = true,
82          requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME,
83          requiresDependencyCollection = ResolutionScope.COMPILE_PLUS_RUNTIME)
84  public final class InlineMojo extends AbstractMojo {
85  
86      private static final PluginLog LOG = new PluginLog(InlineMojo.class);
87  
88      private static final Predicate<Dependency> EXCLUDE_SYSTEM_SCOPE = dependency -> !JavaScopes.SYSTEM.equals(dependency.getScope());
89      private static final Predicate<Dependency> EXCLUDE_PROVIDED_SCOPE = dependency -> !JavaScopes.PROVIDED.equals(dependency.getScope());
90  
91  
92      @Parameter(defaultValue = "${project}", readonly = true, required = true)
93      private MavenProject project;
94  
95      @Parameter(defaultValue = "${session}", readonly = true, required = true)
96      private MavenSession mavenSession;
97  
98      @Parameter(defaultValue = "${reactorProjects}", readonly = true, required = true)
99      private List<MavenProject> reactorProjects;
100 
101     @Component
102     private ProjectBuilder mavenProjectBuilder;
103 
104     @Component
105     private ProjectDependenciesResolver projectDependenciesResolver;
106 
107     @Component
108     private RepositorySystem repositorySystem;
109 
110     @Component
111     private MavenProjectHelper projectHelper;
112 
113     /**
114      * The destination directory for the inlined artifact.
115      */
116     @Parameter(defaultValue = "${project.build.directory}")
117     private File outputDirectory;
118 
119     /**
120      * The POM file to use.
121      */
122     @Parameter(property = "inline.pomFile", defaultValue = "${project.file}")
123     private File pomFile;
124 
125     /**
126      * Direct dependencies to inline. Each dependency here must be
127      * listed in the project POM. Any transitive dependency is added
128      * to the final jar, unless it is in {@code RUNTIME} scope.
129      * {@code RUNTIME} dependencies become a runtime dependency of the
130      * resulting final jar <b>unless</b> they are listed here. In that
131      * case, they are inlined in the final jar as well.
132      */
133     @Parameter
134     private List<InlineDependency> inlineDependencies = ImmutableList.of();
135 
136     // called by maven
137     public void setInlineDependencies(List<InlineDependency> inlineDependencies) {
138         this.inlineDependencies = ImmutableList.copyOf(inlineDependencies);
139     }
140 
141     /**
142      * Include dependencies. A dependency is given as <tt>groupId:artifactId</tt>. The wildcard character '*' is supported for group id and artifact id.
143      * <p>
144      * Includes and excludes operate on the list of potential dependencies to inline. They can not be used to add additional dependencies that are not
145      * listed in the &lt;inlineDependency&gt; elements.
146      */
147     @Parameter
148     private List<ArtifactIdentifier> includes = ImmutableList.of();
149 
150     // called by maven
151     public void setIncludes(List<String> includes) {
152         this.includes = includes.stream().map(ArtifactIdentifier::new).collect(Collectors.toList());
153     }
154 
155     /**
156      * Exclude dependencies from inclusion. A dependency is given as <tt>groupId:artifactId</tt>. Any transitive dependency that has been pulled in can be
157      * excluded here. The wildcard character '*' is supported for group id and artifact id.
158      * <p>
159      * Includes and excludes operate on the list of potential dependencies to inline. They can not be used to add additional dependencies that are not
160      * listed in the &lt;inlineDependency&gt; elements.
161      */
162     @Parameter
163     private List<ArtifactIdentifier> excludes = ImmutableList.of();
164 
165     // called by maven
166     public void setExcludes(List<String> excludes) {
167         this.excludes = excludes.stream().map(ArtifactIdentifier::new).collect(Collectors.toList());
168     }
169 
170     /**
171      * Adds external jar processors. These must be on the dependency path for the plugin. See the "Additional Processors" documentation.
172      */
173     @Parameter
174     private List<String> additionalProcessors = ImmutableList.of();
175 
176     // called by maven
177     public void setAdditionalProcessors(List<String> processors) {
178         this.additionalProcessors = ImmutableList.copyOf(processors);
179     }
180 
181     /**
182      * Hide inlined classes from IDE autocompletion.
183      */
184     @Parameter(defaultValue = "true", property = "inline.hide-classes")
185     private boolean hideClasses;
186 
187     /**
188      * Skip the execution.
189      */
190     @Parameter(defaultValue = "false", property = "inline.skip")
191     private boolean skip;
192 
193     /**
194      * Silence all non-output and non-error messages.
195      */
196     @Parameter(defaultValue = "false", property = "inline.quiet")
197     private boolean quiet;
198 
199     /**
200      * Defines the package prefix for all relocated classes. This prefix must be a valid package name. All relocated classes are put under this prefix.
201      */
202     @Parameter(required = true, property = "inline.prefix")
203     private String prefix;
204 
205     /**
206      * Fail if an inline dependency is defined but the corresponding dependency is not actually found.
207      */
208     @Parameter(defaultValue = "true", property = "inline.failOnNoMatch")
209     private boolean failOnNoMatch;
210 
211     /**
212      * Fail if any duplicate exists after processing the contents.
213      */
214     @Parameter(defaultValue = "true", property = "inline.failOnDuplicate")
215     private boolean failOnDuplicate;
216 
217     /**
218      * 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
219      * nor will it be attached. Hence, this parameter causes the parameters {@link #inlinedArtifactAttached}, {@link #inlinedClassifierName} to be ignored when
220      * used.
221      */
222     @Parameter
223     private File outputJarFile;
224 
225     /**
226      * 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.
227      */
228     @Parameter
229     private File outputPomFile;
230 
231 
232     /**
233      * If true, attach the inlined artifact, if false replace the original artifact.
234      */
235     @Parameter(defaultValue = "false", property = "inline.attachArtifact")
236     private boolean inlinedArtifactAttached;
237 
238     /**
239      * 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
240      * jar with inlined dependencies and then use the same POM file for the resulting artifact (by having all dependencies marked as <tt>provided</tt> and
241      * ensure that those dependencies do not have additional, transitive dependencies. This tends to be error prone and it is recommended to have the plugin
242      * rewrite the POM file.
243      */
244     @Parameter(defaultValue = "true", property = "inline.replacePomFile")
245     private boolean replacePomFile;
246 
247     /**
248      * The name of the classifier used in case the inlined artifact is attached.
249      */
250     @Parameter(defaultValue = "inlined")
251     private String inlinedClassifierName;
252 
253 
254     @Override
255     public void execute() throws MojoExecutionException {
256 
257         if (this.skip) {
258             LOG.report(quiet, "skipping plugin execution");
259             return;
260         }
261 
262         if ("pom".equals(project.getPackaging())) {
263             LOG.report(quiet, "ignoring POM project");
264             return;
265         }
266 
267         if (project.getArtifact().getFile() == null) {
268             throw new MojoExecutionException("No project artifact found!");
269         }
270 
271         try {
272             ImmutableSetMultimap.Builder<InlineDependency, Dependency> dependencyBuilder = ImmutableSetMultimap.builder();
273             ImmutableSet.Builder<Dependency> pomDependenciesToAdd = ImmutableSet.builder();
274 
275             computeDependencyMap(dependencyBuilder, pomDependenciesToAdd);
276 
277             ImmutableSetMultimap<InlineDependency, Dependency> dependencyMap = dependencyBuilder.build();
278 
279             rewriteJarFile(dependencyMap);
280             rewritePomFile(pomDependenciesToAdd.build(), ImmutableSet.copyOf(dependencyMap.values()));
281 
282         } catch (UncheckedIOException e) {
283             throw new MojoExecutionException(e.getCause());
284         } catch (TransformerException | IOException | DependencyResolutionException | ProjectBuildingException | XMLStreamException | JDOMException e) {
285             throw new MojoExecutionException(e);
286         }
287     }
288 
289     private void computeDependencyMap(
290             ImmutableSetMultimap.Builder<InlineDependency, Dependency> dependencyMapBuilder,
291             ImmutableSet.Builder<Dependency> pomBuilder)
292             throws DependencyResolutionException, ProjectBuildingException {
293 
294         DependencyBuilder dependencyBuilder = new DependencyBuilder(project, mavenSession, mavenProjectBuilder, projectDependenciesResolver, reactorProjects);
295 
296         ImmutableSet<ArtifactIdentifier> directArtifacts = project.getDependencyArtifacts().stream()
297                 .map(ArtifactIdentifier::new)
298                 .collect(ImmutableSet.toImmutableSet());
299 
300         ImmutableList<Dependency> directDependencies = dependencyBuilder.mapProject(project,
301                 (node, parents) -> directArtifacts.contains(new ArtifactIdentifier(node)));
302 
303         // build the full set of dependencies with all scopes and everything.
304         ImmutableList<Dependency> projectDependencies = dependencyBuilder.mapProject(project,
305                 ScopeLimitingFilter.computeDependencyScope(ScopeLimitingFilter.COMPILE_PLUS_RUNTIME));
306 
307         Map<String, Dependency> idMap = projectDependencies.stream()
308                 .filter(dependency -> dependency.getArtifact() != null)
309                 .collect(ImmutableMap.toImmutableMap(InlineMojo::getId, Functions.identity()));
310 
311         BiConsumer<InlineDependency, Dependency> dependencyConsumer = (inlineDependency, dependency) -> {
312             LOG.debug("%s matches %s for inlining.", inlineDependency, dependency);
313             dependencyMapBuilder.put(inlineDependency, dependency);
314         };
315 
316         ImmutableSet.Builder<Dependency> directExcludes = ImmutableSet.builder();
317 
318         // first find all the direct dependencies. Add anything that is not hit to the additional exclude list
319 
320         ImmutableSortedSet.Builder<String> directLogBuilder = ImmutableSortedSet.naturalOrder();
321 
322         directDependencies.stream()
323                 // remove anything that does not match the filter set.
324                 // optionals also need to be matched by the inline dependency below
325                 .filter(createFilterSet(true))
326                 .forEach(dependency -> {
327                     Optional<InlineDependency> inlineDependency = findInlineDependencyMatch(dependency);
328                     if (inlineDependency.isPresent()) {
329                         dependencyConsumer.accept(inlineDependency.get(), dependency);
330                         directLogBuilder.add(dependency.toString());
331                     } else {
332                         directExcludes.add(dependency);
333                     }
334                 });
335 
336         ImmutableSortedSet<String> directLog = directLogBuilder.build();
337 
338         if (!quiet) {
339             LOG.info("Inlined dependencies");
340             LOG.info("====================");
341 
342             for (String dependency : directLog) {
343                 LOG.info("    %s", dependency);
344             }
345             LOG.info("");
346         }
347 
348         Set<ArtifactIdentifier> excludes = directExcludes.build().stream()
349                 .map(ArtifactIdentifier::new)
350                 .collect(Collectors.toUnmodifiableSet());
351 
352         this.excludes = ImmutableList.copyOf(Iterables.concat(this.excludes, excludes));
353 
354         LOG.debug("Excludes after creating includes: %s", this.excludes);
355 
356         var directDependencyMap = dependencyMapBuilder.build().asMap();
357 
358         ImmutableSortedSet.Builder<String> transitiveLogBuilder = ImmutableSortedSet.naturalOrder();
359 
360         for (var dependencyEntry : directDependencyMap.entrySet()) {
361             InlineDependency inlineDependency = dependencyEntry.getKey();
362             for (Dependency projectDependency : dependencyEntry.getValue()) {
363 
364                 Consumer<Dependency> consumer;
365                 if (inlineDependency.isInlineTransitive()) {
366                     // transitive deps are added to the jar
367                     consumer = dependency -> {
368                         Optional<InlineDependency> explicitMatch = findInlineDependencyMatch(dependency);
369 
370                         // If the dependency is not a runtime dependency, it is included in the inline jar
371                         // Runtime dependencies are only included if they are explicitly listed as an
372                         // included dependency. Otherwise, they are added as a runtime dep to the inline jar.
373                         if (!JavaScopes.RUNTIME.equals(dependency.getScope()) || explicitMatch.isPresent()) {
374                             dependencyConsumer.accept(inlineDependency, dependency);
375                             transitiveLogBuilder.add(dependency.toString());
376                         } else {
377                             pomBuilder.add(dependency);
378                         }
379                     };
380                 } else {
381                     // non-transitive deps need to be written into the POM.
382                     consumer = pomBuilder::add;
383                 }
384 
385                 dependencyBuilder.mapDependency(projectDependency, ScopeLimitingFilter.computeTransitiveScope(projectDependency.getScope()))
386                         .stream()
387                         // replace deps in the transitive set with deps in the root set if present (will
388                         // override the scope here with the root scope)
389                         .map(dependency -> idMap.getOrDefault(getId(dependency), dependency))
390                         // remove system and provided dependencies, keep optionals if allowed
391                         .filter(createFilterSet(inlineDependency.isInlineOptionals()))
392                         // make sure that the inline dependency actually pulls the dep in.
393                         .filter(this::isDependencyIncluded)
394                         .forEach(consumer);
395             }
396         }
397 
398         if (!quiet) {
399             LOG.info("");
400             LOG.info("Transitive dependencies");
401             LOG.info("=======================");
402             for (String dependency : Sets.difference(transitiveLogBuilder.build(), directLog)) {
403                 LOG.info("    %s", dependency);
404             }
405             LOG.info("");
406         }
407     }
408 
409     private Optional<InlineDependency> findInlineDependencyMatch(Dependency dependency) {
410         for (InlineDependency inlineDependency : inlineDependencies) {
411             if (inlineDependency.matchDependency(dependency)) {
412                 return Optional.of(inlineDependency);
413             }
414         }
415         return Optional.empty();
416     }
417 
418     private static String getId(Dependency dependency) {
419         Artifact artifact = dependency.getArtifact();
420         checkState(artifact != null, "Artifact for dependency %s is null!", dependency);
421 
422         return Joiner.on(':').join(artifact.getGroupId(), artifact.getArtifactId(), artifact.getClassifier());
423     }
424 
425     private Predicate<Dependency> createFilterSet(boolean includeOptional) {
426 
427         // filter system scope dependencies. Those are never inlined.
428         Predicate<Dependency> predicate = EXCLUDE_SYSTEM_SCOPE;
429         predicate = predicate.and(EXCLUDE_PROVIDED_SCOPE);
430 
431         if (!includeOptional) {
432             predicate = predicate.and(Predicate.not(Dependency::isOptional));
433         }
434         return predicate;
435     }
436 
437     public boolean isDependencyIncluded(Dependency dependency) {
438 
439         boolean included = this.includes.stream()
440                 .map(artifactIdentifier -> artifactIdentifier.matchDependency(dependency))
441                 .findFirst()
442                 .orElse(this.includes.isEmpty());
443 
444         boolean excluded = this.excludes.stream()
445                 .map(artifactIdentifier -> artifactIdentifier.matchDependency(dependency))
446                 .findFirst()
447                 .orElse(false);
448 
449         return included && !excluded;
450     }
451 
452 
453     private void rewriteJarFile(ImmutableSetMultimap<InlineDependency, Dependency> dependencies) throws TransformerException, IOException {
454         File outputJar = (this.outputJarFile != null) ? outputJarFile : inlinedArtifactFileWithClassifier();
455 
456         doJarTransformation(outputJar, dependencies);
457 
458         if (this.outputJarFile == null) {
459             if (this.inlinedArtifactAttached) {
460                 LOG.info("Attaching inlined artifact.");
461                 projectHelper.attachArtifact(project, project.getArtifact().getType(), inlinedClassifierName, outputJar);
462             } else {
463                 LOG.info("Replacing original artifact with inlined artifact.");
464                 File originalArtifact = project.getArtifact().getFile();
465 
466                 if (originalArtifact != null) {
467                     File backupFile = new File(originalArtifact.getParentFile(), "original-" + originalArtifact.getName());
468                     Files.move(originalArtifact.toPath(), backupFile.toPath(), ATOMIC_MOVE, REPLACE_EXISTING);
469                     Files.move(outputJar.toPath(), originalArtifact.toPath(), ATOMIC_MOVE, REPLACE_EXISTING);
470                 }
471             }
472         }
473     }
474 
475     private void rewritePomFile(Set<Dependency> dependenciesToAdd, Set<Dependency> dependenciesToRemove) throws IOException, XMLStreamException, JDOMException {
476         String pomContents;
477 
478         try (BufferedReader reader = Files.newBufferedReader(project.getFile().toPath(), StandardCharsets.UTF_8)) {
479             pomContents = CharStreams.toString(reader);
480         }
481 
482         PomUtil pomUtil = new PomUtil(pomContents);
483         dependenciesToRemove.forEach(pomUtil::removeDependency);
484         dependenciesToAdd.forEach(pomUtil::addDependency);
485 
486         // some rewriters (maven flatten plugin) rewrites the new pom name as a hidden file.
487         String pomName = this.pomFile.getName();
488         pomName = "new-" + (pomName.startsWith(".") ? pomName.substring(1) : pomName);
489 
490         File newPomFile = this.outputPomFile != null ? outputPomFile : new File(this.outputDirectory, pomName);
491         try (BufferedWriter writer = Files.newBufferedWriter(newPomFile.toPath(), StandardCharsets.UTF_8)) {
492             pomUtil.writePom(writer);
493         }
494 
495         if (this.replacePomFile) {
496             project.setPomFile(newPomFile);
497         }
498     }
499 
500     private void doJarTransformation(File outputJar, ImmutableSetMultimap<InlineDependency, Dependency> dependencies) throws TransformerException, IOException {
501 
502         try (JarOutputStream jarOutputStream = new JarOutputStream(Files.newOutputStream(outputJar.toPath()))) {
503             Consumer<ClassPathResource> jarConsumer = getJarWriter(jarOutputStream);
504             JarTransformer transformer = new JarTransformer(jarConsumer, true, ImmutableSet.copyOf(additionalProcessors));
505 
506             // Build the class path
507             ClassPath classPath = new ClassPath(project.getBasedir());
508             // maintain the manifest file for the main artifact
509             var artifact = project.getArtifact();
510             classPath.addFile(artifact.getFile(), artifact.getGroupId(), artifact.getArtifactId(), ClassPathTag.ROOT_JAR);
511 
512             dependencies.forEach(
513                     (inlineDependency, dependency) -> {
514                         var dependencyArtifact = dependency.getArtifact();
515                         checkState(dependencyArtifact.getFile() != null, "Could not locate artifact file for %s", dependencyArtifact);
516                         classPath.addFile(dependencyArtifact.getFile(), prefix, dependencyArtifact.getGroupId(), dependencyArtifact.getArtifactId(),
517                                 hideClasses);
518                     });
519 
520             transformer.transform(classPath);
521         }
522     }
523 
524     private Consumer<ClassPathResource> getJarWriter(JarOutputStream jarOutputStream) {
525         return classPathResource -> {
526             try {
527                 String name = classPathResource.getName();
528                 LOG.debug(format("Writing '%s' to jar", name));
529                 JarEntry outputEntry = new JarEntry(name);
530                 outputEntry.setTime(classPathResource.getLastModifiedTime());
531                 outputEntry.setCompressedSize(-1);
532                 jarOutputStream.putNextEntry(outputEntry);
533                 jarOutputStream.write(classPathResource.getContent());
534             } catch (IOException e) {
535                 throw new UncheckedIOException(e);
536             }
537         };
538     }
539 
540     private File inlinedArtifactFileWithClassifier() {
541         final var artifact = project.getArtifact();
542         String inlineName = String.format("%s-%s-%s.%s",
543                 project.getArtifactId(),
544                 artifact.getVersion(),
545                 this.inlinedClassifierName,
546                 artifact.getArtifactHandler().getExtension());
547 
548         return new File(this.outputDirectory, inlineName);
549     }
550 }