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