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