View Javadoc
1   /*
2    * Licensed under the Apache License, Version 2.0 (the "License");
3    * you may not use this file except in compliance with the License.
4    * You may obtain a copy of the License at
5    *
6    * http://www.apache.org/licenses/LICENSE-2.0
7    *
8    * Unless required by applicable law or agreed to in writing, software
9    * distributed under the License is distributed on an "AS IS" BASIS,
10   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11   * See the License for the specific language governing permissions and
12   * limitations under the License.
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   * Inlines one or more dependencies of the project, relocated the classes and writes a new artifact.
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      * The destination directory for the inlined artifact.
119      */
120     @Parameter(defaultValue = "${project.build.directory}")
121     private File outputDirectory;
122 
123     /**
124      * Timestamp for reproducible output archive entries, either formatted as ISO 8601
125      * <code>yyyy-MM-dd'T'HH:mm:ssXXX</code> or as an int representing seconds since the epoch (like
126      * <a href="https://reproducible-builds.org/docs/source-date-epoch/">SOURCE_DATE_EPOCH</a>).
127      */
128     @Parameter(defaultValue = "${project.build.outputTimestamp}")
129     private String outputTimestamp;
130 
131     /**
132      * The POM file to use.
133      */
134     @Parameter(property = "inline.pomFile", defaultValue = "${project.file}")
135     private File pomFile;
136 
137     /**
138      * Direct dependencies to inline. Each dependency here must be listed in the project POM. Any transitive dependency is added to the final jar, unless it is
139      * in {@code RUNTIME} scope. {@code RUNTIME} dependencies become a runtime dependency of the resulting final jar <b>unless</b> they are listed here. In that
140      * case, they are inlined in the final jar as well.
141      */
142     @Parameter
143     private List<InlineDependency> inlineDependencies = ImmutableList.of();
144 
145     // called by maven
146     public void setInlineDependencies(List<InlineDependency> inlineDependencies) {
147         this.inlineDependencies = ImmutableList.copyOf(inlineDependencies);
148     }
149 
150     /**
151      * Include dependencies. A dependency is given as <tt>groupId:artifactId</tt>. The wildcard character '*' is supported for group id and artifact id.
152      * <p>
153      * Includes and excludes operate on the list of potential dependencies to inline. They can not be used to add additional dependencies that are not listed in
154      * the &lt;inlineDependency&gt; elements.
155      */
156     @Parameter
157     private List<ArtifactIdentifier> includes = ImmutableList.of();
158 
159     // called by maven
160     public void setIncludes(List<String> includes) {
161         this.includes = includes.stream().map(ArtifactIdentifier::new).collect(Collectors.toList());
162     }
163 
164     /**
165      * Exclude dependencies from inclusion. A dependency is given as <tt>groupId:artifactId</tt>. Any transitive dependency that has been pulled in can be
166      * excluded here. The wildcard character '*' is supported for group id and artifact id.
167      * <p>
168      * Includes and excludes operate on the list of potential dependencies to inline. They can not be used to add additional dependencies that are not listed in
169      * the &lt;inlineDependency&gt; elements.
170      */
171     @Parameter
172     private List<ArtifactIdentifier> excludes = ImmutableList.of();
173 
174     // called by maven
175     public void setExcludes(List<String> excludes) {
176         this.excludes = excludes.stream().map(ArtifactIdentifier::new).collect(Collectors.toList());
177     }
178 
179     /**
180      * Adds external jar processors. These must be on the dependency path for the plugin. See the "Additional Processors" documentation.
181      */
182     @Parameter
183     private List<String> additionalProcessors = ImmutableList.of();
184 
185     // called by maven
186     public void setAdditionalProcessors(List<String> processors) {
187         this.additionalProcessors = ImmutableList.copyOf(processors);
188     }
189 
190     /**
191      * Hide inlined classes from IDE autocompletion.
192      */
193     @Parameter(defaultValue = "true", property = "inline.hide-classes")
194     private boolean hideClasses;
195 
196     /**
197      * Skip the execution.
198      */
199     @Parameter(defaultValue = "false", property = "inline.skip")
200     private boolean skip;
201 
202     /**
203      * Silence all non-output and non-error messages.
204      */
205     @Parameter(defaultValue = "false", property = "inline.quiet")
206     private boolean quiet;
207 
208     /**
209      * Defines the package prefix for all relocated classes. This prefix must be a valid package name. All relocated classes are put under this prefix.
210      */
211     @Parameter(required = true, property = "inline.prefix")
212     private String prefix;
213 
214     /**
215      * Fail if an inline dependency is defined but the corresponding dependency is not actually found.
216      */
217     @Parameter(defaultValue = "true", property = "inline.failOnNoMatch")
218     private boolean failOnNoMatch;
219 
220     /**
221      * Fail if any duplicate exists after processing the contents.
222      */
223     @Parameter(defaultValue = "true", property = "inline.failOnDuplicate")
224     private boolean failOnDuplicate;
225 
226     /**
227      * The path to the output file for the inlined artifact. When this parameter is set, the created archive will neither replace the project's main artifact
228      * nor will it be attached. Hence, this parameter causes the parameters {@link #inlinedArtifactAttached}, {@link #inlinedClassifierName} to be ignored when
229      * used.
230      */
231     @Parameter
232     private File outputJarFile;
233 
234     /**
235      * The path to the output file for the new POM file. When this parameter is set, the created pom file will not replace the project's pom file.
236      */
237     @Parameter
238     private File outputPomFile;
239 
240 
241     /**
242      * If true, attach the inlined artifact, if false replace the original artifact.
243      */
244     @Parameter(defaultValue = "false", property = "inline.attachArtifact")
245     private boolean inlinedArtifactAttached;
246 
247     /**
248      * If true, replace the POM file with a new version that has all inlined dependencies removed. It is possible to write a POM file that works to build the
249      * jar with inlined dependencies and then use the same POM file for the resulting artifact (by having all dependencies marked as <tt>provided</tt> and
250      * ensure that those dependencies do not have additional, transitive dependencies. This tends to be error prone and it is recommended to have the plugin
251      * rewrite the POM file.
252      */
253     @Parameter(defaultValue = "true", property = "inline.replacePomFile")
254     private boolean replacePomFile;
255 
256     /**
257      * The name of the classifier used in case the inlined artifact is attached.
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         // build the full set of dependencies with all scopes and everything.
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         // first find all the direct dependencies. Add anything that is not hit to the additional exclude list
336 
337         ImmutableSortedSet.Builder<String> directLogBuilder = ImmutableSortedSet.naturalOrder();
338 
339         directDependencies.stream()
340                 // remove anything that does not match the filter set.
341                 // optionals also need to be matched by the inline dependency below
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                     // transitive deps are added to the jar
384                     consumer = dependency -> {
385                         Optional<InlineDependency> explicitMatch = findInlineDependencyMatch(dependency);
386 
387                         // If the dependency is not a runtime dependency, it is included in the inline jar
388                         // Runtime dependencies are only included if they are explicitly listed as an
389                         // included dependency. Otherwise, they are added as a runtime dep to the inline jar.
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                     // non-transitive deps need to be written into the POM.
399                     consumer = pomBuilder::add;
400                 }
401 
402                 dependencyBuilder.mapDependency(projectDependency, ScopeLimitingFilter.computeTransitiveScope(projectDependency.getScope()))
403                         .stream()
404                         // replace deps in the transitive set with deps in the root set if present (will
405                         // override the scope here with the root scope)
406                         .map(dependency -> idMap.getOrDefault(getId(dependency), dependency))
407                         // remove system and provided dependencies, keep optionals if allowed
408                         .filter(createFilterSet(inlineDependency.isInlineOptionals()))
409                         // make sure that the inline dependency actually pulls the dep in.
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         // filter system scope dependencies. Those are never inlined.
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             // ensure that the MANIFEST file always comes first
479             writeSubtree("META-INF/MANIFEST.MF", treeRoot, jarConsumer);
480             // then write all the META-INF contents
481             writeSubtree("META-INF", treeRoot, jarConsumer);
482             // then all the rest
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         // some rewriters (maven flatten plugin) rewrites the new pom name as a hidden file.
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         // Build the class path
536         ClassPath classPath = new ClassPath(project.getBasedir(), timestamp, closer);
537         // maintain the manifest file for the main artifact
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         // navigate to the parent node, writing elements on the way.
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 }