1
2
3
4
5
6
7
8
9
10
11
12
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
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
115
116 @Parameter(defaultValue = "${project.build.directory}")
117 public File outputDirectory = null;
118
119
120
121
122 @Parameter(property = "inline.pomFile", defaultValue = "${project.file}")
123 public File pomFile = null;
124
125
126
127
128 @Parameter
129 private List<InlineDependency> inlineDependencies = ImmutableList.of();
130
131
132 public void setInlineDependencies(List<InlineDependency> inlineDependencies) {
133 this.inlineDependencies = ImmutableList.copyOf(inlineDependencies);
134 }
135
136
137
138
139 @Parameter
140 private List<ArtifactIdentifier> includes = ImmutableList.of();
141
142
143 public void setIncludes(List<ArtifactIdentifier> includes) {
144 this.includes = ImmutableList.copyOf(includes);
145 }
146
147
148
149
150
151 @Parameter
152 private List<ArtifactIdentifier> excludes = ImmutableList.of();
153
154
155 public void setExcludes(List<ArtifactIdentifier> excludes) {
156 this.excludes = ImmutableList.copyOf(excludes);
157 }
158
159
160
161
162 @Parameter
163 private List<String> additionalProcessors = ImmutableList.of();
164
165
166 public void setAdditionalProcessors(List<String> processors) {
167 this.additionalProcessors = ImmutableList.copyOf(processors);
168 }
169
170
171
172
173 @Parameter(defaultValue = "true", property = "inline.hide-classes")
174 public boolean hideClasses = true;
175
176
177
178
179 @Parameter(defaultValue = "false", property = "inline.skip")
180 public boolean skip = false;
181
182
183
184
185 @Parameter(defaultValue = "false", property = "inline.quiet")
186 public boolean quiet = false;
187
188
189
190
191 @Parameter(required = true, property = "inline.prefix")
192 public String prefix = null;
193
194
195
196
197 @Parameter(defaultValue = "true", property = "inline.failOnNoMatch")
198 public boolean failOnNoMatch = true;
199
200
201
202
203 @Parameter(defaultValue = "true", property = "inline.failOnDuplicate")
204 public boolean failOnDuplicate = true;
205
206
207
208
209
210
211 @Parameter
212 public File outputJarFile = null;
213
214
215
216
217 @Parameter
218 public File outputPomFile = null;
219
220
221
222
223
224 @Parameter(defaultValue = "false", property = "inline.attachArtifact")
225 public boolean inlinedArtifactAttached = false;
226
227
228
229
230
231
232
233 @Parameter(defaultValue = "true", property = "inline.replacePomFile")
234 public boolean replacePomFile = true;
235
236
237
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
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
308
309 ImmutableSortedSet.Builder<String> directLogBuilder = ImmutableSortedSet.naturalOrder();
310
311 directDependencies.stream()
312
313
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
357 consumer = dependency -> {
358
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
368 consumer = pomBuilder::add;
369 }
370
371 dependencyBuilder.mapDependency(projectDependency, ScopeLimitingFilter.computeTransitiveScope(projectDependency.getScope()))
372 .stream()
373
374
375 .map(dependency -> idMap.getOrDefault(getId(dependency), dependency))
376
377 .filter(createFilterSet(inlineDependency.isInlineOptionals()))
378
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
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
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
484 ClassPath classPath = new ClassPath(project.getBasedir());
485
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 }