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.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
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
117
118 @Parameter(defaultValue = "${project.build.directory}")
119 private File outputDirectory;
120
121
122
123
124
125
126 @Parameter(defaultValue = "${project.build.outputTimestamp}")
127 private String outputTimestamp;
128
129
130
131
132 @Parameter(property = "inline.pomFile", defaultValue = "${project.file}")
133 private File pomFile;
134
135
136
137
138
139
140 @Parameter
141 private List<InlineDependency> inlineDependencies = ImmutableList.of();
142
143
144 public void setInlineDependencies(List<InlineDependency> inlineDependencies) {
145 this.inlineDependencies = ImmutableList.copyOf(inlineDependencies);
146 }
147
148
149
150
151
152
153
154 @Parameter
155 private List<ArtifactIdentifier> includes = ImmutableList.of();
156
157
158 public void setIncludes(List<String> includes) {
159 this.includes = includes.stream().map(ArtifactIdentifier::new).collect(Collectors.toList());
160 }
161
162
163
164
165
166
167
168
169 @Parameter
170 private List<ArtifactIdentifier> excludes = ImmutableList.of();
171
172
173 public void setExcludes(List<String> excludes) {
174 this.excludes = excludes.stream().map(ArtifactIdentifier::new).collect(Collectors.toList());
175 }
176
177
178
179
180 @Parameter
181 private List<String> additionalProcessors = ImmutableList.of();
182
183
184 public void setAdditionalProcessors(List<String> processors) {
185 this.additionalProcessors = ImmutableList.copyOf(processors);
186 }
187
188
189
190
191 @Parameter(defaultValue = "true", property = "inline.hide-classes")
192 private boolean hideClasses;
193
194
195
196
197 @Parameter(defaultValue = "false", property = "inline.skip")
198 private boolean skip;
199
200
201
202
203 @Parameter(defaultValue = "false", property = "inline.quiet")
204 private boolean quiet;
205
206
207
208
209 @Parameter(required = true, property = "inline.prefix")
210 private String prefix;
211
212
213
214
215 @Parameter(defaultValue = "true", property = "inline.failOnNoMatch")
216 private boolean failOnNoMatch;
217
218
219
220
221 @Parameter(defaultValue = "true", property = "inline.failOnDuplicate")
222 private boolean failOnDuplicate;
223
224
225
226
227
228
229 @Parameter
230 private File outputJarFile;
231
232
233
234
235 @Parameter
236 private File outputPomFile;
237
238
239
240
241
242 @Parameter(defaultValue = "false", property = "inline.attachArtifact")
243 private boolean inlinedArtifactAttached;
244
245
246
247
248
249
250
251 @Parameter(defaultValue = "true", property = "inline.replacePomFile")
252 private boolean replacePomFile;
253
254
255
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
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
328
329 ImmutableSortedSet.Builder<String> directLogBuilder = ImmutableSortedSet.naturalOrder();
330
331 directDependencies.stream()
332
333
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
376 consumer = dependency -> {
377 Optional<InlineDependency> explicitMatch = findInlineDependencyMatch(dependency);
378
379
380
381
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
391 consumer = pomBuilder::add;
392 }
393
394 dependencyBuilder.mapDependency(projectDependency, ScopeLimitingFilter.computeTransitiveScope(projectDependency.getScope()))
395 .stream()
396
397
398 .map(dependency -> idMap.getOrDefault(getId(dependency), dependency))
399
400 .filter(createFilterSet(inlineDependency.isInlineOptionals()))
401
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
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
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
517 ClassPath classPath = new ClassPath(project.getBasedir(), timestamp);
518
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 }