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.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
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
115
116 @Parameter(defaultValue = "${project.build.directory}")
117 private File outputDirectory;
118
119
120
121
122 @Parameter(property = "inline.pomFile", defaultValue = "${project.file}")
123 private File pomFile;
124
125
126
127
128
129
130
131
132
133 @Parameter
134 private List<InlineDependency> inlineDependencies = ImmutableList.of();
135
136
137 public void setInlineDependencies(List<InlineDependency> inlineDependencies) {
138 this.inlineDependencies = ImmutableList.copyOf(inlineDependencies);
139 }
140
141
142
143
144
145
146
147 @Parameter
148 private List<ArtifactIdentifier> includes = ImmutableList.of();
149
150
151 public void setIncludes(List<String> includes) {
152 this.includes = includes.stream().map(ArtifactIdentifier::new).collect(Collectors.toList());
153 }
154
155
156
157
158
159
160
161
162 @Parameter
163 private List<ArtifactIdentifier> excludes = ImmutableList.of();
164
165
166 public void setExcludes(List<String> excludes) {
167 this.excludes = excludes.stream().map(ArtifactIdentifier::new).collect(Collectors.toList());
168 }
169
170
171
172
173 @Parameter
174 private List<String> additionalProcessors = ImmutableList.of();
175
176
177 public void setAdditionalProcessors(List<String> processors) {
178 this.additionalProcessors = ImmutableList.copyOf(processors);
179 }
180
181
182
183
184 @Parameter(defaultValue = "true", property = "inline.hide-classes")
185 private boolean hideClasses;
186
187
188
189
190 @Parameter(defaultValue = "false", property = "inline.skip")
191 private boolean skip;
192
193
194
195
196 @Parameter(defaultValue = "false", property = "inline.quiet")
197 private boolean quiet;
198
199
200
201
202 @Parameter(required = true, property = "inline.prefix")
203 private String prefix;
204
205
206
207
208 @Parameter(defaultValue = "true", property = "inline.failOnNoMatch")
209 private boolean failOnNoMatch;
210
211
212
213
214 @Parameter(defaultValue = "true", property = "inline.failOnDuplicate")
215 private boolean failOnDuplicate;
216
217
218
219
220
221
222 @Parameter
223 private File outputJarFile;
224
225
226
227
228 @Parameter
229 private File outputPomFile;
230
231
232
233
234
235 @Parameter(defaultValue = "false", property = "inline.attachArtifact")
236 private boolean inlinedArtifactAttached;
237
238
239
240
241
242
243
244 @Parameter(defaultValue = "true", property = "inline.replacePomFile")
245 private boolean replacePomFile;
246
247
248
249
250 @Parameter(defaultValue = "inlined")
251 private String inlinedClassifierName;
252
253
254 @Override
255 public void execute() throws MojoExecutionException {
256
257 if (this.skip) {
258 LOG.report(quiet, "skipping plugin execution");
259 return;
260 }
261
262 if ("pom".equals(project.getPackaging())) {
263 LOG.report(quiet, "ignoring POM project");
264 return;
265 }
266
267 if (project.getArtifact().getFile() == null) {
268 throw new MojoExecutionException("No project artifact found!");
269 }
270
271 try {
272 ImmutableSetMultimap.Builder<InlineDependency, Dependency> dependencyBuilder = ImmutableSetMultimap.builder();
273 ImmutableSet.Builder<Dependency> pomDependenciesToAdd = ImmutableSet.builder();
274
275 computeDependencyMap(dependencyBuilder, pomDependenciesToAdd);
276
277 ImmutableSetMultimap<InlineDependency, Dependency> dependencyMap = dependencyBuilder.build();
278
279 rewriteJarFile(dependencyMap);
280 rewritePomFile(pomDependenciesToAdd.build(), ImmutableSet.copyOf(dependencyMap.values()));
281
282 } catch (UncheckedIOException e) {
283 throw new MojoExecutionException(e.getCause());
284 } catch (TransformerException | IOException | DependencyResolutionException | ProjectBuildingException | XMLStreamException | JDOMException e) {
285 throw new MojoExecutionException(e);
286 }
287 }
288
289 private void computeDependencyMap(
290 ImmutableSetMultimap.Builder<InlineDependency, Dependency> dependencyMapBuilder,
291 ImmutableSet.Builder<Dependency> pomBuilder)
292 throws DependencyResolutionException, ProjectBuildingException {
293
294 DependencyBuilder dependencyBuilder = new DependencyBuilder(project, mavenSession, mavenProjectBuilder, projectDependenciesResolver, reactorProjects);
295
296 ImmutableSet<ArtifactIdentifier> directArtifacts = project.getDependencyArtifacts().stream()
297 .map(ArtifactIdentifier::new)
298 .collect(ImmutableSet.toImmutableSet());
299
300 ImmutableList<Dependency> directDependencies = dependencyBuilder.mapProject(project,
301 (node, parents) -> directArtifacts.contains(new ArtifactIdentifier(node)));
302
303
304 ImmutableList<Dependency> projectDependencies = dependencyBuilder.mapProject(project,
305 ScopeLimitingFilter.computeDependencyScope(ScopeLimitingFilter.COMPILE_PLUS_RUNTIME));
306
307 Map<String, Dependency> idMap = projectDependencies.stream()
308 .filter(dependency -> dependency.getArtifact() != null)
309 .collect(ImmutableMap.toImmutableMap(InlineMojo::getId, Functions.identity()));
310
311 BiConsumer<InlineDependency, Dependency> dependencyConsumer = (inlineDependency, dependency) -> {
312 LOG.debug("%s matches %s for inlining.", inlineDependency, dependency);
313 dependencyMapBuilder.put(inlineDependency, dependency);
314 };
315
316 ImmutableSet.Builder<Dependency> directExcludes = ImmutableSet.builder();
317
318
319
320 ImmutableSortedSet.Builder<String> directLogBuilder = ImmutableSortedSet.naturalOrder();
321
322 directDependencies.stream()
323
324
325 .filter(createFilterSet(true))
326 .forEach(dependency -> {
327 Optional<InlineDependency> inlineDependency = findInlineDependencyMatch(dependency);
328 if (inlineDependency.isPresent()) {
329 dependencyConsumer.accept(inlineDependency.get(), dependency);
330 directLogBuilder.add(dependency.toString());
331 } else {
332 directExcludes.add(dependency);
333 }
334 });
335
336 ImmutableSortedSet<String> directLog = directLogBuilder.build();
337
338 if (!quiet) {
339 LOG.info("Inlined dependencies");
340 LOG.info("====================");
341
342 for (String dependency : directLog) {
343 LOG.info(" %s", dependency);
344 }
345 LOG.info("");
346 }
347
348 Set<ArtifactIdentifier> excludes = directExcludes.build().stream()
349 .map(ArtifactIdentifier::new)
350 .collect(Collectors.toUnmodifiableSet());
351
352 this.excludes = ImmutableList.copyOf(Iterables.concat(this.excludes, excludes));
353
354 LOG.debug("Excludes after creating includes: %s", this.excludes);
355
356 var directDependencyMap = dependencyMapBuilder.build().asMap();
357
358 ImmutableSortedSet.Builder<String> transitiveLogBuilder = ImmutableSortedSet.naturalOrder();
359
360 for (var dependencyEntry : directDependencyMap.entrySet()) {
361 InlineDependency inlineDependency = dependencyEntry.getKey();
362 for (Dependency projectDependency : dependencyEntry.getValue()) {
363
364 Consumer<Dependency> consumer;
365 if (inlineDependency.isInlineTransitive()) {
366
367 consumer = dependency -> {
368 Optional<InlineDependency> explicitMatch = findInlineDependencyMatch(dependency);
369
370
371
372
373 if (!JavaScopes.RUNTIME.equals(dependency.getScope()) || explicitMatch.isPresent()) {
374 dependencyConsumer.accept(inlineDependency, dependency);
375 transitiveLogBuilder.add(dependency.toString());
376 } else {
377 pomBuilder.add(dependency);
378 }
379 };
380 } else {
381
382 consumer = pomBuilder::add;
383 }
384
385 dependencyBuilder.mapDependency(projectDependency, ScopeLimitingFilter.computeTransitiveScope(projectDependency.getScope()))
386 .stream()
387
388
389 .map(dependency -> idMap.getOrDefault(getId(dependency), dependency))
390
391 .filter(createFilterSet(inlineDependency.isInlineOptionals()))
392
393 .filter(this::isDependencyIncluded)
394 .forEach(consumer);
395 }
396 }
397
398 if (!quiet) {
399 LOG.info("");
400 LOG.info("Transitive dependencies");
401 LOG.info("=======================");
402 for (String dependency : Sets.difference(transitiveLogBuilder.build(), directLog)) {
403 LOG.info(" %s", dependency);
404 }
405 LOG.info("");
406 }
407 }
408
409 private Optional<InlineDependency> findInlineDependencyMatch(Dependency dependency) {
410 for (InlineDependency inlineDependency : inlineDependencies) {
411 if (inlineDependency.matchDependency(dependency)) {
412 return Optional.of(inlineDependency);
413 }
414 }
415 return Optional.empty();
416 }
417
418 private static String getId(Dependency dependency) {
419 Artifact artifact = dependency.getArtifact();
420 checkState(artifact != null, "Artifact for dependency %s is null!", dependency);
421
422 return Joiner.on(':').join(artifact.getGroupId(), artifact.getArtifactId(), artifact.getClassifier());
423 }
424
425 private Predicate<Dependency> createFilterSet(boolean includeOptional) {
426
427
428 Predicate<Dependency> predicate = EXCLUDE_SYSTEM_SCOPE;
429 predicate = predicate.and(EXCLUDE_PROVIDED_SCOPE);
430
431 if (!includeOptional) {
432 predicate = predicate.and(Predicate.not(Dependency::isOptional));
433 }
434 return predicate;
435 }
436
437 public boolean isDependencyIncluded(Dependency dependency) {
438
439 boolean included = this.includes.stream()
440 .map(artifactIdentifier -> artifactIdentifier.matchDependency(dependency))
441 .findFirst()
442 .orElse(this.includes.isEmpty());
443
444 boolean excluded = this.excludes.stream()
445 .map(artifactIdentifier -> artifactIdentifier.matchDependency(dependency))
446 .findFirst()
447 .orElse(false);
448
449 return included && !excluded;
450 }
451
452
453 private void rewriteJarFile(ImmutableSetMultimap<InlineDependency, Dependency> dependencies) throws TransformerException, IOException {
454 File outputJar = (this.outputJarFile != null) ? outputJarFile : inlinedArtifactFileWithClassifier();
455
456 doJarTransformation(outputJar, dependencies);
457
458 if (this.outputJarFile == null) {
459 if (this.inlinedArtifactAttached) {
460 LOG.info("Attaching inlined artifact.");
461 projectHelper.attachArtifact(project, project.getArtifact().getType(), inlinedClassifierName, outputJar);
462 } else {
463 LOG.info("Replacing original artifact with inlined artifact.");
464 File originalArtifact = project.getArtifact().getFile();
465
466 if (originalArtifact != null) {
467 File backupFile = new File(originalArtifact.getParentFile(), "original-" + originalArtifact.getName());
468 Files.move(originalArtifact.toPath(), backupFile.toPath(), ATOMIC_MOVE, REPLACE_EXISTING);
469 Files.move(outputJar.toPath(), originalArtifact.toPath(), ATOMIC_MOVE, REPLACE_EXISTING);
470 }
471 }
472 }
473 }
474
475 private void rewritePomFile(Set<Dependency> dependenciesToAdd, Set<Dependency> dependenciesToRemove) throws IOException, XMLStreamException, JDOMException {
476 String pomContents;
477
478 try (BufferedReader reader = Files.newBufferedReader(project.getFile().toPath(), StandardCharsets.UTF_8)) {
479 pomContents = CharStreams.toString(reader);
480 }
481
482 PomUtil pomUtil = new PomUtil(pomContents);
483 dependenciesToRemove.forEach(pomUtil::removeDependency);
484 dependenciesToAdd.forEach(pomUtil::addDependency);
485
486
487 String pomName = this.pomFile.getName();
488 pomName = "new-" + (pomName.startsWith(".") ? pomName.substring(1) : pomName);
489
490 File newPomFile = this.outputPomFile != null ? outputPomFile : new File(this.outputDirectory, pomName);
491 try (BufferedWriter writer = Files.newBufferedWriter(newPomFile.toPath(), StandardCharsets.UTF_8)) {
492 pomUtil.writePom(writer);
493 }
494
495 if (this.replacePomFile) {
496 project.setPomFile(newPomFile);
497 }
498 }
499
500 private void doJarTransformation(File outputJar, ImmutableSetMultimap<InlineDependency, Dependency> dependencies) throws TransformerException, IOException {
501
502 try (JarOutputStream jarOutputStream = new JarOutputStream(Files.newOutputStream(outputJar.toPath()))) {
503 Consumer<ClassPathResource> jarConsumer = getJarWriter(jarOutputStream);
504 JarTransformer transformer = new JarTransformer(jarConsumer, true, ImmutableSet.copyOf(additionalProcessors));
505
506
507 ClassPath classPath = new ClassPath(project.getBasedir());
508
509 var artifact = project.getArtifact();
510 classPath.addFile(artifact.getFile(), artifact.getGroupId(), artifact.getArtifactId(), ClassPathTag.ROOT_JAR);
511
512 dependencies.forEach(
513 (inlineDependency, dependency) -> {
514 var dependencyArtifact = dependency.getArtifact();
515 checkState(dependencyArtifact.getFile() != null, "Could not locate artifact file for %s", dependencyArtifact);
516 classPath.addFile(dependencyArtifact.getFile(), prefix, dependencyArtifact.getGroupId(), dependencyArtifact.getArtifactId(),
517 hideClasses);
518 });
519
520 transformer.transform(classPath);
521 }
522 }
523
524 private Consumer<ClassPathResource> getJarWriter(JarOutputStream jarOutputStream) {
525 return classPathResource -> {
526 try {
527 String name = classPathResource.getName();
528 LOG.debug(format("Writing '%s' to jar", name));
529 JarEntry outputEntry = new JarEntry(name);
530 outputEntry.setTime(classPathResource.getLastModifiedTime());
531 outputEntry.setCompressedSize(-1);
532 jarOutputStream.putNextEntry(outputEntry);
533 jarOutputStream.write(classPathResource.getContent());
534 } catch (IOException e) {
535 throw new UncheckedIOException(e);
536 }
537 };
538 }
539
540 private File inlinedArtifactFileWithClassifier() {
541 final var artifact = project.getArtifact();
542 String inlineName = String.format("%s-%s-%s.%s",
543 project.getArtifactId(),
544 artifact.getVersion(),
545 this.inlinedClassifierName,
546 artifact.getArtifactHandler().getExtension());
547
548 return new File(this.outputDirectory, inlineName);
549 }
550 }