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