1
2
3
4
5
6
7
8
9
10
11
12
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.base.Splitter;
51 import com.google.common.collect.ImmutableList;
52 import com.google.common.collect.ImmutableMap;
53 import com.google.common.collect.ImmutableSet;
54 import com.google.common.collect.ImmutableSetMultimap;
55 import com.google.common.collect.ImmutableSortedSet;
56 import com.google.common.collect.Iterables;
57 import com.google.common.collect.Sets;
58 import com.google.common.io.CharStreams;
59 import com.google.common.io.Closer;
60 import org.apache.maven.archiver.MavenArchiver;
61 import org.apache.maven.execution.MavenSession;
62 import org.apache.maven.plugin.AbstractMojo;
63 import org.apache.maven.plugin.MojoExecutionException;
64 import org.apache.maven.plugins.annotations.Component;
65 import org.apache.maven.plugins.annotations.LifecyclePhase;
66 import org.apache.maven.plugins.annotations.Mojo;
67 import org.apache.maven.plugins.annotations.Parameter;
68 import org.apache.maven.plugins.annotations.ResolutionScope;
69 import org.apache.maven.project.DependencyResolutionException;
70 import org.apache.maven.project.MavenProject;
71 import org.apache.maven.project.MavenProjectHelper;
72 import org.apache.maven.project.ProjectBuilder;
73 import org.apache.maven.project.ProjectBuildingException;
74 import org.apache.maven.project.ProjectDependenciesResolver;
75 import org.eclipse.aether.RepositorySystem;
76 import org.eclipse.aether.artifact.Artifact;
77 import org.eclipse.aether.graph.Dependency;
78 import org.eclipse.aether.util.artifact.JavaScopes;
79 import org.jdom2.JDOMException;
80
81
82
83
84 @Mojo(name = "inline", defaultPhase = LifecyclePhase.PACKAGE,
85 requiresProject = true, threadSafe = true,
86 requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME,
87 requiresDependencyCollection = ResolutionScope.COMPILE_PLUS_RUNTIME)
88 public final class InlineMojo extends AbstractMojo {
89
90 private static final PluginLog LOG = new PluginLog(InlineMojo.class);
91
92 private static final Predicate<Dependency> EXCLUDE_SYSTEM_SCOPE = dependency -> !JavaScopes.SYSTEM.equals(dependency.getScope());
93 private static final Predicate<Dependency> EXCLUDE_PROVIDED_SCOPE = dependency -> !JavaScopes.PROVIDED.equals(dependency.getScope());
94
95
96 @Parameter(defaultValue = "${project}", readonly = true, required = true)
97 private MavenProject project;
98
99 @Parameter(defaultValue = "${session}", readonly = true, required = true)
100 private MavenSession mavenSession;
101
102 @Parameter(defaultValue = "${reactorProjects}", readonly = true, required = true)
103 private List<MavenProject> reactorProjects;
104
105 @Component
106 private ProjectBuilder mavenProjectBuilder;
107
108 @Component
109 private ProjectDependenciesResolver projectDependenciesResolver;
110
111 @Component
112 private RepositorySystem repositorySystem;
113
114 @Component
115 private MavenProjectHelper projectHelper;
116
117
118
119
120 @Parameter(defaultValue = "${project.build.directory}")
121 private File outputDirectory;
122
123
124
125
126
127
128 @Parameter(defaultValue = "${project.build.outputTimestamp}")
129 private String outputTimestamp;
130
131
132
133
134 @Parameter(property = "inline.pomFile", defaultValue = "${project.file}")
135 private File pomFile;
136
137
138
139
140
141
142 @Parameter
143 private List<InlineDependency> inlineDependencies = ImmutableList.of();
144
145
146 public void setInlineDependencies(List<InlineDependency> inlineDependencies) {
147 this.inlineDependencies = ImmutableList.copyOf(inlineDependencies);
148 }
149
150
151
152
153
154
155
156 @Parameter
157 private List<ArtifactIdentifier> includes = ImmutableList.of();
158
159
160 public void setIncludes(List<String> includes) {
161 this.includes = includes.stream().map(ArtifactIdentifier::new).collect(Collectors.toList());
162 }
163
164
165
166
167
168
169
170
171 @Parameter
172 private List<ArtifactIdentifier> excludes = ImmutableList.of();
173
174
175 public void setExcludes(List<String> excludes) {
176 this.excludes = excludes.stream().map(ArtifactIdentifier::new).collect(Collectors.toList());
177 }
178
179
180
181
182 @Parameter
183 private List<String> additionalProcessors = ImmutableList.of();
184
185
186 public void setAdditionalProcessors(List<String> processors) {
187 this.additionalProcessors = ImmutableList.copyOf(processors);
188 }
189
190
191
192
193 @Parameter(defaultValue = "true", property = "inline.hide-classes")
194 private boolean hideClasses;
195
196
197
198
199 @Parameter(defaultValue = "false", property = "inline.skip")
200 private boolean skip;
201
202
203
204
205 @Parameter(defaultValue = "false", property = "inline.quiet")
206 private boolean quiet;
207
208
209
210
211 @Parameter(required = true, property = "inline.prefix")
212 private String prefix;
213
214
215
216
217 @Parameter(defaultValue = "true", property = "inline.failOnNoMatch")
218 private boolean failOnNoMatch;
219
220
221
222
223 @Parameter(defaultValue = "true", property = "inline.failOnDuplicate")
224 private boolean failOnDuplicate;
225
226
227
228
229
230
231 @Parameter
232 private File outputJarFile;
233
234
235
236
237 @Parameter
238 private File outputPomFile;
239
240
241
242
243
244 @Parameter(defaultValue = "false", property = "inline.attachArtifact")
245 private boolean inlinedArtifactAttached;
246
247
248
249
250
251
252
253 @Parameter(defaultValue = "true", property = "inline.replacePomFile")
254 private boolean replacePomFile;
255
256
257
258
259 @Parameter(defaultValue = "inlined")
260 private String inlinedClassifierName;
261
262 private final Closer closer = Closer.create();
263
264
265 @Override
266 public void execute() throws MojoExecutionException {
267
268 if (this.skip) {
269 LOG.report(quiet, "skipping plugin execution");
270 return;
271 }
272
273 if ("pom".equals(project.getPackaging())) {
274 LOG.report(quiet, "ignoring POM project");
275 return;
276 }
277
278 if (project.getArtifact().getFile() == null) {
279 throw new MojoExecutionException("No project artifact found!");
280 }
281
282 Instant timestamp = MavenArchiver.parseBuildOutputTimestamp(outputTimestamp).orElseGet(Instant::now);
283
284 try {
285 ImmutableSetMultimap.Builder<InlineDependency, Dependency> dependencyBuilder = ImmutableSetMultimap.builder();
286 ImmutableSet.Builder<Dependency> pomDependenciesToAdd = ImmutableSet.builder();
287
288 try {
289 computeDependencyMap(dependencyBuilder, pomDependenciesToAdd);
290
291 ImmutableSetMultimap<InlineDependency, Dependency> dependencyMap = dependencyBuilder.build();
292
293 rewriteJarFile(timestamp.toEpochMilli(), dependencyMap);
294 rewritePomFile(pomDependenciesToAdd.build(), ImmutableSet.copyOf(dependencyMap.values()));
295 } finally {
296 closer.close();
297 }
298
299 } catch (UncheckedIOException e) {
300 throw new MojoExecutionException(e.getCause());
301 } catch (TransformerException | IOException | DependencyResolutionException | ProjectBuildingException | XMLStreamException | JDOMException e) {
302 throw new MojoExecutionException(e);
303 }
304 }
305
306 private void computeDependencyMap(
307 ImmutableSetMultimap.Builder<InlineDependency, Dependency> dependencyMapBuilder,
308 ImmutableSet.Builder<Dependency> pomBuilder)
309 throws DependencyResolutionException, ProjectBuildingException {
310
311 DependencyBuilder dependencyBuilder = new DependencyBuilder(project, mavenSession, mavenProjectBuilder, projectDependenciesResolver, reactorProjects);
312
313 ImmutableSet<ArtifactIdentifier> directArtifacts = project.getDependencyArtifacts().stream()
314 .map(ArtifactIdentifier::new)
315 .collect(ImmutableSet.toImmutableSet());
316
317 ImmutableList<Dependency> directDependencies = dependencyBuilder.mapProject(project,
318 (node, parents) -> directArtifacts.contains(new ArtifactIdentifier(node)));
319
320
321 ImmutableList<Dependency> projectDependencies = dependencyBuilder.mapProject(project,
322 ScopeLimitingFilter.computeDependencyScope(ScopeLimitingFilter.COMPILE_PLUS_RUNTIME));
323
324 Map<String, Dependency> idMap = projectDependencies.stream()
325 .filter(dependency -> dependency.getArtifact() != null)
326 .collect(ImmutableMap.toImmutableMap(InlineMojo::getId, Functions.identity()));
327
328 BiConsumer<InlineDependency, Dependency> dependencyConsumer = (inlineDependency, dependency) -> {
329 LOG.debug("%s matches %s for inlining.", inlineDependency, dependency);
330 dependencyMapBuilder.put(inlineDependency, dependency);
331 };
332
333 ImmutableSet.Builder<Dependency> directExcludes = ImmutableSet.builder();
334
335
336
337 ImmutableSortedSet.Builder<String> directLogBuilder = ImmutableSortedSet.naturalOrder();
338
339 directDependencies.stream()
340
341
342 .filter(createFilterSet(true))
343 .forEach(dependency -> {
344 Optional<InlineDependency> inlineDependency = findInlineDependencyMatch(dependency);
345 if (inlineDependency.isPresent()) {
346 dependencyConsumer.accept(inlineDependency.get(), dependency);
347 directLogBuilder.add(dependency.toString());
348 } else {
349 directExcludes.add(dependency);
350 }
351 });
352
353 ImmutableSortedSet<String> directLog = directLogBuilder.build();
354
355 if (!quiet) {
356 LOG.info("Inlined dependencies");
357 LOG.info("====================");
358
359 for (String dependency : directLog) {
360 LOG.info(" %s", dependency);
361 }
362 LOG.info("");
363 }
364
365 Set<ArtifactIdentifier> excludes = directExcludes.build().stream()
366 .map(ArtifactIdentifier::new)
367 .collect(Collectors.toUnmodifiableSet());
368
369 this.excludes = ImmutableList.copyOf(Iterables.concat(this.excludes, excludes));
370
371 LOG.debug("Excludes after creating includes: %s", this.excludes);
372
373 var directDependencyMap = dependencyMapBuilder.build().asMap();
374
375 ImmutableSortedSet.Builder<String> transitiveLogBuilder = ImmutableSortedSet.naturalOrder();
376
377 for (var dependencyEntry : directDependencyMap.entrySet()) {
378 InlineDependency inlineDependency = dependencyEntry.getKey();
379 for (Dependency projectDependency : dependencyEntry.getValue()) {
380
381 Consumer<Dependency> consumer;
382 if (inlineDependency.isInlineTransitive()) {
383
384 consumer = dependency -> {
385 Optional<InlineDependency> explicitMatch = findInlineDependencyMatch(dependency);
386
387
388
389
390 if (!JavaScopes.RUNTIME.equals(dependency.getScope()) || explicitMatch.isPresent()) {
391 dependencyConsumer.accept(inlineDependency, dependency);
392 transitiveLogBuilder.add(dependency.toString());
393 } else {
394 pomBuilder.add(dependency);
395 }
396 };
397 } else {
398
399 consumer = pomBuilder::add;
400 }
401
402 dependencyBuilder.mapDependency(projectDependency, ScopeLimitingFilter.computeTransitiveScope(projectDependency.getScope()))
403 .stream()
404
405
406 .map(dependency -> idMap.getOrDefault(getId(dependency), dependency))
407
408 .filter(createFilterSet(inlineDependency.isInlineOptionals()))
409
410 .filter(this::isDependencyIncluded)
411 .forEach(consumer);
412 }
413 }
414
415 if (!quiet) {
416 LOG.info("");
417 LOG.info("Transitive dependencies");
418 LOG.info("=======================");
419 for (String dependency : Sets.difference(transitiveLogBuilder.build(), directLog)) {
420 LOG.info(" %s", dependency);
421 }
422 LOG.info("");
423 }
424 }
425
426 private Optional<InlineDependency> findInlineDependencyMatch(Dependency dependency) {
427 for (InlineDependency inlineDependency : inlineDependencies) {
428 if (inlineDependency.matchDependency(dependency)) {
429 return Optional.of(inlineDependency);
430 }
431 }
432 return Optional.empty();
433 }
434
435 private static String getId(Dependency dependency) {
436 Artifact artifact = dependency.getArtifact();
437 checkState(artifact != null, "Artifact for dependency %s is null!", dependency);
438
439 return Joiner.on(':').join(artifact.getGroupId(), artifact.getArtifactId(), artifact.getClassifier());
440 }
441
442 private Predicate<Dependency> createFilterSet(boolean includeOptional) {
443
444
445 Predicate<Dependency> predicate = EXCLUDE_SYSTEM_SCOPE;
446 predicate = predicate.and(EXCLUDE_PROVIDED_SCOPE);
447
448 if (!includeOptional) {
449 predicate = predicate.and(Predicate.not(Dependency::isOptional));
450 }
451 return predicate;
452 }
453
454 public boolean isDependencyIncluded(Dependency dependency) {
455
456 boolean included = this.includes.stream()
457 .map(artifactIdentifier -> artifactIdentifier.matchDependency(dependency))
458 .findFirst()
459 .orElse(this.includes.isEmpty());
460
461 boolean excluded = this.excludes.stream()
462 .map(artifactIdentifier -> artifactIdentifier.matchDependency(dependency))
463 .findFirst()
464 .orElse(false);
465
466 return included && !excluded;
467 }
468
469
470 private void rewriteJarFile(long timestamp, ImmutableSetMultimap<InlineDependency, Dependency> dependencies) throws TransformerException, IOException {
471 File outputJar = (this.outputJarFile != null) ? outputJarFile : inlinedArtifactFileWithClassifier();
472
473 TreeNode treeRoot = createJarContents(timestamp, dependencies);
474
475 try (JarOutputStream jarOutputStream = new JarOutputStream(Files.newOutputStream(outputJar.toPath()))) {
476 var jarConsumer = getJarWriter(jarOutputStream);
477
478
479 writeSubtree("META-INF/MANIFEST.MF", treeRoot, jarConsumer);
480
481 writeSubtree("META-INF", treeRoot, jarConsumer);
482
483 writeSubtree("", treeRoot, jarConsumer);
484 }
485
486 if (this.outputJarFile == null) {
487 if (this.inlinedArtifactAttached) {
488 LOG.info("Attaching inlined artifact.");
489 projectHelper.attachArtifact(project, project.getArtifact().getType(), inlinedClassifierName, outputJar);
490 } else {
491 LOG.info("Replacing original artifact with inlined artifact.");
492 File originalArtifact = project.getArtifact().getFile();
493
494 if (originalArtifact != null) {
495 File backupFile = new File(originalArtifact.getParentFile(), "original-" + originalArtifact.getName());
496 Files.move(originalArtifact.toPath(), backupFile.toPath(), ATOMIC_MOVE, REPLACE_EXISTING);
497 Files.move(outputJar.toPath(), originalArtifact.toPath(), ATOMIC_MOVE, REPLACE_EXISTING);
498 }
499 }
500 }
501 }
502
503 private void rewritePomFile(Set<Dependency> dependenciesToAdd, Set<Dependency> dependenciesToRemove) throws IOException, XMLStreamException, JDOMException {
504 String pomContents;
505
506 try (BufferedReader reader = Files.newBufferedReader(project.getFile().toPath(), StandardCharsets.UTF_8)) {
507 pomContents = CharStreams.toString(reader);
508 }
509
510 PomUtil pomUtil = new PomUtil(pomContents);
511 dependenciesToRemove.forEach(pomUtil::removeDependency);
512 dependenciesToAdd.forEach(pomUtil::addDependency);
513
514
515 String pomName = this.pomFile.getName();
516 pomName = "new-" + (pomName.startsWith(".") ? pomName.substring(1) : pomName);
517
518 File newPomFile = this.outputPomFile != null ? outputPomFile : new File(this.outputDirectory, pomName);
519 try (BufferedWriter writer = Files.newBufferedWriter(newPomFile.toPath(), StandardCharsets.UTF_8)) {
520 pomUtil.writePom(writer);
521 }
522
523 if (this.replacePomFile) {
524 project.setPomFile(newPomFile);
525 }
526 }
527
528 private TreeNode createJarContents(long timestamp, ImmutableSetMultimap<InlineDependency, Dependency> dependencies)
529 throws TransformerException, IOException {
530 var treeRoot = TreeNode.getRootNode();
531
532 Consumer<ClassPathResource> jarConsumer = getJarBuilder(treeRoot);
533 JarTransformer transformer = new JarTransformer(jarConsumer, timestamp, true, ImmutableSet.copyOf(additionalProcessors));
534
535
536 ClassPath classPath = new ClassPath(project.getBasedir(), timestamp, closer);
537
538 var artifact = project.getArtifact();
539 classPath.addFile(artifact.getFile(), artifact.getGroupId(), artifact.getArtifactId(), ClassPathTag.ROOT_JAR);
540
541 dependencies.forEach(
542 (inlineDependency, dependency) -> {
543 var dependencyArtifact = dependency.getArtifact();
544 checkState(dependencyArtifact.getFile() != null, "Could not locate artifact file for %s", dependencyArtifact);
545 classPath.addFile(dependencyArtifact.getFile(), prefix, dependencyArtifact.getGroupId(), dependencyArtifact.getArtifactId(),
546 hideClasses);
547 });
548
549 transformer.transform(classPath);
550
551 return treeRoot;
552 }
553
554 private void writeSubtree(String name, TreeNode root, Consumer<ClassPathResource> jarWriter) {
555 List<String> elements = Splitter.on('/').omitEmptyStrings().splitToList(name);
556
557
558 TreeNode parent = root;
559 for (String element : elements) {
560 TreeNode child = parent.getChild(element);
561 checkState(child != null, "Could not find child '%s' for parent '%s' (%s)", element, parent.getName(), name);
562 if (child.needsWriting()) {
563 jarWriter.accept(child.getClassPathResource());
564 child.write();
565 }
566 parent = child;
567 }
568
569 writeChildrenDepthFirst(parent, jarWriter);
570 }
571
572 private void writeChildrenDepthFirst(TreeNode writeNode, Consumer<ClassPathResource> jarWriter) {
573 if (writeNode.needsWriting()) {
574 jarWriter.accept(writeNode.getClassPathResource());
575 writeNode.write();
576 }
577
578 var children = writeNode.getChildren();
579 if (children.isEmpty()) {
580 return;
581 }
582
583 for (var childNode : children.values()) {
584 writeChildrenDepthFirst(childNode, jarWriter);
585 }
586 }
587
588 private Consumer<ClassPathResource> getJarWriter(JarOutputStream jarOutputStream) {
589 return classPathResource -> {
590 try {
591 String name = classPathResource.getName();
592 LOG.debug(format("Writing '%s' to jar", name));
593 JarEntry outputEntry = new JarEntry(name);
594 outputEntry.setTime(classPathResource.getLastModifiedTime());
595 outputEntry.setCompressedSize(-1);
596 jarOutputStream.putNextEntry(outputEntry);
597 jarOutputStream.write(classPathResource.getContent());
598 } catch (IOException e) {
599 throw new UncheckedIOException(e);
600 }
601 };
602 }
603
604 private Consumer<ClassPathResource> getJarBuilder(TreeNode root) {
605 return classPathResource -> {
606 String name = classPathResource.getName();
607 LOG.debug(format("Adding '%s' to jar", name));
608
609 List<String> elements = Splitter.on('/').omitEmptyStrings().splitToList(name);
610
611 TreeNode parent = root;
612 for (int i = 0; i < elements.size() - 1; i++) {
613 var child = parent.getChild(elements.get(i));
614 checkState(child != null, "Could not locate child '%s' in parent element '%s', this is a transformer problem!", elements.get(i), parent);
615 parent = child;
616 }
617 parent.addChild(elements.get(elements.size() - 1), classPathResource);
618 };
619 }
620
621 private File inlinedArtifactFileWithClassifier() {
622 final var artifact = project.getArtifact();
623 String inlineName = format("%s-%s-%s.%s",
624 project.getArtifactId(),
625 artifact.getVersion(),
626 this.inlinedClassifierName,
627 artifact.getArtifactHandler().getExtension());
628
629 return new File(this.outputDirectory, inlineName);
630 }
631 }