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      */
144     @Parameter
145     private List<ArtifactIdentifier> includes = ImmutableList.of();
146 
147     // called by maven
148     public void setIncludes(List<ArtifactIdentifier> includes) {
149         this.includes = ImmutableList.copyOf(includes);
150     }
151 
152     /**
153      * Exclude dependencies from inclusion. A dependency is given as <tt>groupId:artifactId</tt>. Any transitive dependency that has been pulled in can be
154      * excluded here. The wildcard character '*' is supported for group id and artifact id.
155      */
156     @Parameter
157     private List<ArtifactIdentifier> excludes = ImmutableList.of();
158 
159     // called by maven
160     public void setExcludes(List<ArtifactIdentifier> excludes) {
161         this.excludes = ImmutableList.copyOf(excludes);
162     }
163 
164     /**
165      * Adds external jar processors. These must be on the dependency path for the plugin. See the "Additional Processors" documentation.
166      */
167     @Parameter
168     private List<String> additionalProcessors = ImmutableList.of();
169 
170     // called by maven
171     public void setAdditionalProcessors(List<String> processors) {
172         this.additionalProcessors = ImmutableList.copyOf(processors);
173     }
174 
175     /**
176      * Hide inlined classes from IDE autocompletion.
177      */
178     @Parameter(defaultValue = "true", property = "inline.hide-classes")
179     private boolean hideClasses;
180 
181     /**
182      * Skip the execution.
183      */
184     @Parameter(defaultValue = "false", property = "inline.skip")
185     private boolean skip;
186 
187     /**
188      * Silence all non-output and non-error messages.
189      */
190     @Parameter(defaultValue = "false", property = "inline.quiet")
191     private boolean quiet;
192 
193     /**
194      * Defines the package prefix for all relocated classes. This prefix must be a valid package name. All relocated classes are put under this prefix.
195      */
196     @Parameter(required = true, property = "inline.prefix")
197     private String prefix;
198 
199     /**
200      * Fail if an inline dependency is defined but the corresponding dependency is not actually found.
201      */
202     @Parameter(defaultValue = "true", property = "inline.failOnNoMatch")
203     private boolean failOnNoMatch;
204 
205     /**
206      * Fail if any duplicate exists after processing the contents.
207      */
208     @Parameter(defaultValue = "true", property = "inline.failOnDuplicate")
209     private boolean failOnDuplicate;
210 
211     /**
212      * 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
213      * nor will it be attached. Hence, this parameter causes the parameters {@link #inlinedArtifactAttached}, {@link #inlinedClassifierName} to be ignored when
214      * used.
215      */
216     @Parameter
217     private File outputJarFile;
218 
219     /**
220      * 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.
221      */
222     @Parameter
223     private File outputPomFile;
224 
225 
226     /**
227      * If true, attach the inlined artifact, if false replace the original artifact.
228      */
229     @Parameter(defaultValue = "false", property = "inline.attachArtifact")
230     private boolean inlinedArtifactAttached;
231 
232     /**
233      * 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
234      * 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
235      * ensure that those dependencies do not have additional, transitive dependencies. This tends to be error prone and it is recommended to have the plugin
236      * rewrite the POM file.
237      */
238     @Parameter(defaultValue = "true", property = "inline.replacePomFile")
239     private boolean replacePomFile;
240 
241     /**
242      * The name of the classifier used in case the inlined artifact is attached.
243      */
244     @Parameter(defaultValue = "inlined")
245     private String inlinedClassifierName;
246 
247 
248     @Override
249     public void execute() throws MojoExecutionException {
250 
251         if (this.skip) {
252             LOG.report(quiet, "skipping plugin execution");
253             return;
254         }
255 
256         if ("pom".equals(project.getPackaging())) {
257             LOG.report(quiet, "ignoring POM project");
258             return;
259         }
260 
261         if (project.getArtifact().getFile() == null) {
262             throw new MojoExecutionException("No project artifact found!");
263         }
264 
265         try {
266             ImmutableSetMultimap.Builder<InlineDependency, Dependency> dependencyBuilder = ImmutableSetMultimap.builder();
267             ImmutableSet.Builder<Dependency> pomDependenciesToAdd = ImmutableSet.builder();
268 
269             computeDependencyMap(dependencyBuilder, pomDependenciesToAdd);
270 
271             ImmutableSetMultimap<InlineDependency, Dependency> dependencyMap = dependencyBuilder.build();
272 
273             rewriteJarFile(dependencyMap);
274             rewritePomFile(pomDependenciesToAdd.build(), ImmutableSet.copyOf(dependencyMap.values()));
275 
276         } catch (UncheckedIOException e) {
277             throw new MojoExecutionException(e.getCause());
278         } catch (TransformerException | IOException | DependencyResolutionException | ProjectBuildingException | XMLStreamException | JDOMException e) {
279             throw new MojoExecutionException(e);
280         }
281     }
282 
283     private void computeDependencyMap(
284             ImmutableSetMultimap.Builder<InlineDependency, Dependency> dependencyMapBuilder,
285             ImmutableSet.Builder<Dependency> pomBuilder)
286             throws DependencyResolutionException, ProjectBuildingException {
287 
288         DependencyBuilder dependencyBuilder = new DependencyBuilder(project, mavenSession, mavenProjectBuilder, projectDependenciesResolver, reactorProjects);
289 
290         ImmutableSet<ArtifactIdentifier> directArtifacts = project.getDependencyArtifacts().stream()
291                 .map(ArtifactIdentifier::new)
292                 .collect(ImmutableSet.toImmutableSet());
293 
294         ImmutableList<Dependency> directDependencies = dependencyBuilder.mapProject(project,
295                 (node, parents) -> directArtifacts.contains(new ArtifactIdentifier(node)));
296 
297         // build the full set of dependencies with all scopes and everything.
298         ImmutableList<Dependency> projectDependencies = dependencyBuilder.mapProject(project,
299                 ScopeLimitingFilter.computeDependencyScope(ScopeLimitingFilter.COMPILE_PLUS_RUNTIME));
300 
301         Map<String, Dependency> idMap = projectDependencies.stream()
302                 .filter(dependency -> dependency.getArtifact() != null)
303                 .collect(ImmutableMap.toImmutableMap(InlineMojo::getId, Functions.identity()));
304 
305         BiConsumer<InlineDependency, Dependency> dependencyConsumer = (inlineDependency, dependency) -> {
306             LOG.debug("%s matches %s for inlining.", inlineDependency, dependency);
307             dependencyMapBuilder.put(inlineDependency, dependency);
308         };
309 
310         ImmutableSet.Builder<Dependency> directExcludes = ImmutableSet.builder();
311 
312         // first find all the direct dependencies. Add anything that is not hit to the additional exclude list
313 
314         ImmutableSortedSet.Builder<String> directLogBuilder = ImmutableSortedSet.naturalOrder();
315 
316         directDependencies.stream()
317                 // remove anything that does not match the filter set.
318                 // optionals also need to be matched by the inline dependency below
319                 .filter(createFilterSet(true))
320                 .forEach(dependency -> {
321                     Optional<InlineDependency> inlineDependency = findInlineDependencyMatch(dependency);
322                     if (inlineDependency.isPresent()) {
323                         dependencyConsumer.accept(inlineDependency.get(), dependency);
324                         directLogBuilder.add(dependency.toString());
325                     } else {
326                         directExcludes.add(dependency);
327                     }
328                 });
329 
330         ImmutableSortedSet<String> directLog = directLogBuilder.build();
331 
332         if (!quiet) {
333             LOG.info("Inlined dependencies");
334             LOG.info("====================");
335 
336             for (String dependency : directLog) {
337                 LOG.info("    %s", dependency);
338             }
339             LOG.info("");
340         }
341 
342         Set<ArtifactIdentifier> excludes = directExcludes.build().stream()
343                 .map(ArtifactIdentifier::new)
344                 .collect(Collectors.toUnmodifiableSet());
345 
346         this.excludes = ImmutableList.copyOf(Iterables.concat(this.excludes, excludes));
347 
348         LOG.debug("Excludes after creating includes: %s", this.excludes);
349 
350         var directDependencyMap = dependencyMapBuilder.build().asMap();
351 
352         ImmutableSortedSet.Builder<String> transitiveLogBuilder = ImmutableSortedSet.naturalOrder();
353 
354         for (var dependencyEntry : directDependencyMap.entrySet()) {
355             InlineDependency inlineDependency = dependencyEntry.getKey();
356             for (Dependency projectDependency : dependencyEntry.getValue()) {
357 
358                 Consumer<Dependency> consumer;
359                 if (inlineDependency.isInlineTransitive()) {
360                     // transitive deps are added to the jar
361                     consumer = dependency -> {
362                         Optional<InlineDependency> explicitMatch = findInlineDependencyMatch(dependency);
363 
364                         // If the dependency is not a runtime dependency, it is included in the inline jar
365                         // Runtime dependencies are only included if they are explicitly listed as an
366                         // included dependency. Otherwise, they are added as a runtime dep to the inline jar.
367                         if (!JavaScopes.RUNTIME.equals(dependency.getScope()) || explicitMatch.isPresent()) {
368                             dependencyConsumer.accept(inlineDependency, dependency);
369                             transitiveLogBuilder.add(dependency.toString());
370                         } else {
371                             pomBuilder.add(dependency);
372                         }
373                     };
374                 } else {
375                     // non-transitive deps need to be written into the POM.
376                     consumer = pomBuilder::add;
377                 }
378 
379                 dependencyBuilder.mapDependency(projectDependency, ScopeLimitingFilter.computeTransitiveScope(projectDependency.getScope()))
380                         .stream()
381                         // replace deps in the transitive set with deps in the root set if present (will
382                         // override the scope here with the root scope)
383                         .map(dependency -> idMap.getOrDefault(getId(dependency), dependency))
384                         // remove system and provided dependencies, keep optionals if allowed
385                         .filter(createFilterSet(inlineDependency.isInlineOptionals()))
386                         // make sure that the inline dependency actually pulls the dep in.
387                         .filter(this::isDependencyIncluded)
388                         .forEach(consumer);
389             }
390         }
391 
392         if (!quiet) {
393             LOG.info("");
394             LOG.info("Transitive dependencies");
395             LOG.info("=======================");
396             for (String dependency : Sets.difference(transitiveLogBuilder.build(), directLog)) {
397                 LOG.info("    %s", dependency);
398             }
399             LOG.info("");
400         }
401     }
402 
403     private Optional<InlineDependency> findInlineDependencyMatch(Dependency dependency) {
404         for (InlineDependency inlineDependency : inlineDependencies) {
405             if (inlineDependency.matchDependency(dependency)) {
406                 return Optional.of(inlineDependency);
407             }
408         }
409         return Optional.empty();
410     }
411 
412     private static String getId(Dependency dependency) {
413         Artifact artifact = dependency.getArtifact();
414         checkState(artifact != null, "Artifact for dependency %s is null!", dependency);
415 
416         return Joiner.on(':').join(artifact.getGroupId(), artifact.getArtifactId(), artifact.getClassifier());
417     }
418 
419     private Predicate<Dependency> createFilterSet(boolean includeOptional) {
420 
421         // filter system scope dependencies. Those are never inlined.
422         Predicate<Dependency> predicate = EXCLUDE_SYSTEM_SCOPE;
423         predicate = predicate.and(EXCLUDE_PROVIDED_SCOPE);
424 
425         if (!includeOptional) {
426             predicate = predicate.and(Predicate.not(Dependency::isOptional));
427         }
428         return predicate;
429     }
430 
431     public boolean isDependencyIncluded(Dependency dependency) {
432 
433         boolean included = this.includes.stream()
434                 .map(artifactIdentifier -> artifactIdentifier.matchDependency(dependency))
435                 .findFirst()
436                 .orElse(this.includes.isEmpty());
437 
438         boolean excluded = this.excludes.stream()
439                 .map(artifactIdentifier -> artifactIdentifier.matchDependency(dependency))
440                 .findFirst()
441                 .orElse(false);
442 
443         return included && !excluded;
444     }
445 
446 
447     private void rewriteJarFile(ImmutableSetMultimap<InlineDependency, Dependency> dependencies) throws TransformerException, IOException {
448         File outputJar = (this.outputJarFile != null) ? outputJarFile : inlinedArtifactFileWithClassifier();
449 
450         doJarTransformation(outputJar, dependencies);
451 
452         if (this.outputJarFile == null) {
453             if (this.inlinedArtifactAttached) {
454                 LOG.info("Attaching inlined artifact.");
455                 projectHelper.attachArtifact(project, project.getArtifact().getType(), inlinedClassifierName, outputJar);
456             } else {
457                 LOG.info("Replacing original artifact with inlined artifact.");
458                 File originalArtifact = project.getArtifact().getFile();
459 
460                 if (originalArtifact != null) {
461                     File backupFile = new File(originalArtifact.getParentFile(), "original-" + originalArtifact.getName());
462                     Files.move(originalArtifact.toPath(), backupFile.toPath(), ATOMIC_MOVE, REPLACE_EXISTING);
463                     Files.move(outputJar.toPath(), originalArtifact.toPath(), ATOMIC_MOVE, REPLACE_EXISTING);
464                 }
465             }
466         }
467     }
468 
469     private void rewritePomFile(Set<Dependency> dependenciesToAdd, Set<Dependency> dependenciesToRemove) throws IOException, XMLStreamException, JDOMException {
470         String pomContents;
471 
472         try (BufferedReader reader = Files.newBufferedReader(project.getFile().toPath(), StandardCharsets.UTF_8)) {
473             pomContents = CharStreams.toString(reader);
474         }
475 
476         PomUtil pomUtil = new PomUtil(pomContents);
477         dependenciesToRemove.forEach(pomUtil::removeDependency);
478         dependenciesToAdd.forEach(pomUtil::addDependency);
479 
480         // some rewriters (maven flatten plugin) rewrites the new pom name as a hidden file.
481         String pomName = this.pomFile.getName();
482         pomName = "new-" + (pomName.startsWith(".") ? pomName.substring(1) : pomName);
483 
484         File newPomFile = this.outputPomFile != null ? outputPomFile : new File(this.outputDirectory, pomName);
485         try (BufferedWriter writer = Files.newBufferedWriter(newPomFile.toPath(), StandardCharsets.UTF_8)) {
486             pomUtil.writePom(writer);
487         }
488 
489         if (this.replacePomFile) {
490             project.setPomFile(newPomFile);
491         }
492     }
493 
494     private void doJarTransformation(File outputJar, ImmutableSetMultimap<InlineDependency, Dependency> dependencies) throws TransformerException, IOException {
495 
496         try (JarOutputStream jarOutputStream = new JarOutputStream(Files.newOutputStream(outputJar.toPath()))) {
497             Consumer<ClassPathResource> jarConsumer = getJarWriter(jarOutputStream);
498             JarTransformer transformer = new JarTransformer(jarConsumer, true, ImmutableSet.copyOf(additionalProcessors));
499 
500             // Build the class path
501             ClassPath classPath = new ClassPath(project.getBasedir());
502             // maintain the manifest file for the main artifact
503             var artifact = project.getArtifact();
504             classPath.addFile(artifact.getFile(), artifact.getGroupId(), artifact.getArtifactId(), ClassPathTag.ROOT_JAR);
505 
506             dependencies.forEach(
507                     (inlineDependency, dependency) -> {
508                         var dependencyArtifact = dependency.getArtifact();
509                         checkState(dependencyArtifact.getFile() != null, "Could not locate artifact file for %s", dependencyArtifact);
510                         classPath.addFile(dependencyArtifact.getFile(), prefix, dependencyArtifact.getGroupId(), dependencyArtifact.getArtifactId(),
511                                 hideClasses);
512                     });
513 
514             transformer.transform(classPath);
515         }
516     }
517 
518     private Consumer<ClassPathResource> getJarWriter(JarOutputStream jarOutputStream) {
519         return classPathResource -> {
520             try {
521                 String name = classPathResource.getName();
522                 LOG.debug(format("Writing '%s' to jar", name));
523                 JarEntry outputEntry = new JarEntry(name);
524                 outputEntry.setTime(classPathResource.getLastModifiedTime());
525                 outputEntry.setCompressedSize(-1);
526                 jarOutputStream.putNextEntry(outputEntry);
527                 jarOutputStream.write(classPathResource.getContent());
528             } catch (IOException e) {
529                 throw new UncheckedIOException(e);
530             }
531         };
532     }
533 
534     private File inlinedArtifactFileWithClassifier() {
535         final var artifact = project.getArtifact();
536         String inlineName = String.format("%s-%s-%s.%s",
537                 project.getArtifactId(),
538                 artifact.getVersion(),
539                 this.inlinedClassifierName,
540                 artifact.getArtifactHandler().getExtension());
541 
542         return new File(this.outputDirectory, inlineName);
543     }
544 }