View Javadoc
1   /*
2     Copyright (C) 2020 - 2022 Alexander Kapitman
3   
4     Licensed under the Apache License, Version 2.0 (the "License");
5     you may not use this file except in compliance with the License.
6     You may obtain a copy of the License at
7   
8       http://www.apache.org/licenses/LICENSE-2.0
9   
10    Unless required by applicable law or agreed to in writing, software
11    distributed under the License is distributed on an "AS IS" BASIS,
12    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13    See the License for the specific language governing permissions and
14    limitations under the License.
15  */
16  
17  package ru.akman.maven.plugins.jlink;
18  
19  import java.io.File;
20  import java.io.IOException;
21  import java.nio.file.FileSystems;
22  import java.nio.file.Files;
23  import java.nio.file.Path;
24  import java.nio.file.PathMatcher;
25  import java.text.MessageFormat;
26  import java.util.ArrayList;
27  import java.util.HashMap;
28  import java.util.List;
29  import java.util.Locale;
30  import java.util.Map;
31  import java.util.Objects;
32  import java.util.Set;
33  import java.util.regex.Matcher;
34  import java.util.regex.Pattern;
35  import java.util.stream.Collectors;
36  import java.util.stream.Stream;
37  import org.apache.commons.lang3.JavaVersion;
38  import org.apache.commons.lang3.StringUtils;
39  import org.apache.commons.text.StringSubstitutor;
40  import org.apache.maven.artifact.Artifact;
41  import org.apache.maven.plugin.MojoExecutionException;
42  import org.apache.maven.plugins.annotations.Component;
43  // import org.apache.maven.plugins.annotations.Execute;
44  // import org.apache.maven.plugins.annotations.LifecyclePhase;
45  import org.apache.maven.plugins.annotations.Mojo;
46  import org.apache.maven.plugins.annotations.Parameter;
47  import org.apache.maven.plugins.annotations.ResolutionScope;
48  import org.apache.maven.shared.model.fileset.FileSet;
49  import org.codehaus.plexus.languages.java.jpms.JavaModuleDescriptor;
50  import org.codehaus.plexus.languages.java.jpms.LocationManager;
51  import org.codehaus.plexus.languages.java.jpms.ModuleNameSource;
52  import org.codehaus.plexus.languages.java.jpms.ResolvePathsRequest;
53  import org.codehaus.plexus.languages.java.jpms.ResolvePathsResult;
54  import org.codehaus.plexus.util.FileUtils;
55  import org.codehaus.plexus.util.cli.CommandLineException;
56  import org.codehaus.plexus.util.cli.Commandline;
57  import ru.akman.maven.plugins.BaseToolMojo;
58  import ru.akman.maven.plugins.CommandLineBuilder;
59  import ru.akman.maven.plugins.CommandLineOption;
60  
61  /**
62   * The jlink goal lets you create a custom runtime image with
63   * the jlink tool introduced in Java 9. It used to link a set of modules,
64   * along with their transitive dependences.
65   *
66   * <p>
67   * The main idea is to avoid being tied to project artifacts and allow the user
68   * to fully control the process of creating an image. However, it is possible,
69   * of course, to customize the process using project artifacts.
70   * </p>
71   */
72  @Mojo(
73      name = "jlink",
74      requiresDependencyResolution = ResolutionScope.RUNTIME
75  //    defaultPhase = LifecyclePhase.VERIFY,
76  //    requiresProject = true,
77  //    aggregator = <false|true>,
78  //    configurator = "<role hint>",
79  //    executionStrategy = "<once-per-session|always>",
80  //    inheritByDefault = <true|false>,
81  //    instantiationStrategy = InstantiationStrategy.<strategy>,
82  //    requiresDependencyCollection = ResolutionScope.<scope>,
83  //    requiresDirectInvocation = <false|true>,
84  //    requiresOnline = <false|true>,
85  //    threadSafe = <false|true>
86  )
87  // @Execute(
88  //    This will fork an alternate build lifecycle up to the specified phase
89  //    before continuing to execute the current one.
90  //    If no lifecycle is specified, Maven will use the lifecycle
91  //    of the current build.
92  //    phase = LifecyclePhase.VERIFY
93  //
94  //    This will execute the given goal before execution of this one.
95  //    The goal name is specified using the prefix:goal notation.
96  //    goal = "prefix:goal"
97  //
98  //    This will execute the given alternate lifecycle. A custom lifecycle
99  //    can be defined in META-INF/maven/lifecycle.xml.
100 //    lifecycle = "<lifecycle>", phase="<phase>"
101 // )
102 public class JlinkMojo extends BaseToolMojo {
103 
104   /**
105    * The name of the subdirectory where the tool live.
106    */
107   private static final String TOOL_HOME_BIN = "bin";
108 
109   /**
110    * The tool name.
111    */
112   private static final String TOOL_NAME = "jlink";
113 
114   /**
115    * Filename for temporary file contains the tool options.
116    */
117   private static final String OPTS_FILE = TOOL_NAME + ".opts";
118 
119   /**
120    * Error message pattern for unability to resolve file path.
121    */
122   private static final String ERROR_RESOLVE =
123       "Error: Unable to resolve file path for {0} [{1}]";
124 
125   /**
126    * Filename of a module descriptor.
127    */
128   private static final String DESCRIPTOR_NAME = "module-info.class";
129 
130   /**
131    * Resolved java corresponding version for tool.
132    */
133   private JavaVersion toolJavaVersion;
134 
135   /**
136    * Resolved project dependencies.
137    */
138   private ResolvePathsResult<File> projectDependencies;
139 
140   /**
141    * Resolved main module descriptor.
142    */
143   private JavaModuleDescriptor mainModuleDescriptor;
144 
145   /**
146    * JPMS location manager.
147    */
148   @Component
149   private LocationManager locationManager;
150 
151   /**
152    * Specifies the path to the JDK home directory providing the tool needed.
153    */
154   @Parameter
155   private File toolhome;
156 
157   /**
158    * Specifies the location in which modular dependencies will be copied.
159    */
160   @Parameter(
161       defaultValue = "${project.build.directory}/jlink/mods"
162   )
163   private File modsdir;
164 
165   /**
166    * Specifies the location in which non modular dependencies will be copied.
167    */
168   @Parameter(
169       defaultValue = "${project.build.directory}/jlink/libs"
170   )
171   private File libsdir;
172 
173   /**
174    * Specifies the module path. The path where the jlink tool discovers
175    * observable modules: modular JAR files, JMOD files, exploded modules.
176    * If this option is not specified, then the default module path
177    * is $JAVA_HOME/jmods. This directory contains the java.base module
178    * and the other standard and JDK modules. If this option is specified
179    * but the java.base module cannot be resolved from it, then
180    * the jlink command appends $JAVA_HOME/jmods to the module path.
181    *
182    * <p>
183    * pathelements - passed to jlink as is
184    * filesets - sets of files (without directories)
185    * dirsets - sets of directories (without files)
186    * dependencysets - sets of dependencies with specified includes and
187    *                  excludes patterns (glob: or regex:) for file names
188    *                  and regex patterns only for module names
189    * </p>
190    *
191    * <p><pre>
192    * &lt;modulepath&gt;
193    *   &lt;pathelements&gt;
194    *     &lt;pathelement&gt;mod.jar&lt;/pathelement&gt;
195    *     &lt;pathelement&gt;mod.jmod&lt;/pathelement&gt;
196    *     &lt;pathelement&gt;mods/exploded/mod&lt;/pathelement&gt;
197    *   &lt;/pathelements&gt;
198    *   &lt;filesets&gt;
199    *     &lt;fileset&gt;
200    *       &lt;directory&gt;${project.build.directory}&lt;/directory&gt;
201    *       &lt;includes&gt;
202    *         &lt;include&gt;*&#42;/*&lt;/include&gt;
203    *       &lt;/includes&gt;
204    *       &lt;excludes&gt;
205    *         &lt;exclude&gt;*&#42;/*Empty.jar&lt;/exclude&gt;
206    *       &lt;/excludes&gt;
207    *       &lt;followSymlinks&gt;false&lt;/followSymlinks&gt;
208    *     &lt;/fileset&gt;
209    *   &lt;/filesets&gt;
210    *   &lt;dirsets&gt;
211    *     &lt;dirset&gt;
212    *       &lt;directory&gt;target&lt;/directory&gt;
213    *       &lt;includes&gt;
214    *         &lt;include&gt;*&#42;/*&lt;/include&gt;
215    *       &lt;/includes&gt;
216    *       &lt;excludes&gt;
217    *         &lt;exclude&gt;*&#42;/*Test&lt;/exclude&gt;
218    *       &lt;/excludes&gt;
219    *       &lt;followSymlinks&gt;true&lt;/followSymlinks&gt;
220    *     &lt;/dirset&gt;
221    *   &lt;/dirsets&gt;
222    *   &lt;dependencysets&gt;
223    *     &lt;dependencyset&gt;
224    *       &lt;includeoutput&gt;false&lt;/includeoutput&gt;
225    *       &lt;excludeautomatic&gt;false&lt;/excludeautomatic&gt;
226    *       &lt;includes&gt;
227    *         &lt;include&gt;glob:*&#42;/*.jar&lt;/include&gt;
228    *         &lt;include&gt;regex:foo-(bar|baz)-.*?\.jar&lt;/include&gt;
229    *       &lt;/includes&gt;
230    *       &lt;includenames&gt;
231    *         &lt;includename&gt;.*&lt;/includename&gt;
232    *       &lt;/includenames&gt;
233    *       &lt;excludes&gt;
234    *         &lt;exclude&gt;glob:*&#42;/javafx.*Empty&lt;/exclude&gt;
235    *       &lt;/excludes&gt;
236    *       &lt;excludenames&gt;
237    *         &lt;excludename&gt;javafx\..+Empty&lt;/excludename&gt;
238    *       &lt;/excludenames&gt;
239    *     &lt;/dependencyset&gt;
240    *   &lt;/dependencysets&gt;
241    * &lt;/modulepath&gt;
242    * </pre></p>
243    *
244    * <p>The jlink CLI is: <code>--modulepath path</code></p>
245    */
246   @Parameter
247   private ModulePath modulepath;
248 
249   /**
250    * Specifies the modules names (names of root modules) to add to
251    * the runtime image. Their transitive dependencies will add too.
252    *
253    * <p><pre>
254    * &lt;addmodules&gt;
255    *   &lt;addmodule&gt;java.base&lt;/addmodule&gt;
256    *   &lt;addmodule&gt;org.example.rootmodule&lt;/addmodule&gt;
257    * &lt;/addmodules&gt;
258    * </pre></p>
259    *
260    * <p>The jlink CLI is: <code>--add-modules module [, module...]</code></p>
261    */
262   @Parameter
263   private List<String> addmodules;
264 
265   /**
266    * Specifies the location of the generated runtime image.
267    *
268    * <p>The jlink CLI is: <code>--output path</code></p>
269    */
270   @Parameter(
271       defaultValue = "${project.build.directory}/jlink/image"
272   )
273   private File output;
274 
275   /**
276    * Limits the universe of observable modules to those in
277    * the transitive closure of the named modules, mod,
278    * plus the main module, if any, plus any further
279    * modules specified in the "addmodules" property.
280    * It used to limit resolve any services other than
281    * the selected services, if the property "bindservices"
282    * set to true.
283    *
284    * <p><pre>
285    * &lt;limitmodules&gt;
286    *   &lt;limitmodule&gt;java.base&lt;/limitmodule&gt;
287    *   &lt;limitmodule&gt;org.example.limitmodule&lt;/limitmodule&gt;
288    * &lt;/limitmodules&gt;
289    * </pre></p>
290    *
291    * <p>The jlink CLI is: <code>--limit-modules module [, module...]</code></p>
292    */
293   @Parameter
294   private List<String> limitmodules;
295 
296   /**
297    * Suggest providers that implement the given service types
298    * from the module path.
299    *
300    * <p><pre>
301    * &lt;suggestproviders&gt;
302    *   &lt;suggestprovider&gt;provider.name&lt;/suggestprovider&gt;
303    * &lt;/suggestproviders&gt;
304    * </pre></p>
305    *
306    * <p>The jlink CLI is: <code>--suggest-providers [name, ...]</code></p>
307    */
308   @Parameter
309   private List<String> suggestproviders;
310 
311   /**
312    * Save jlink options in the given file.
313    *
314    * <p>The jlink CLI is: <code>--save-opts filename</code></p>
315    */
316   @Parameter
317   private File saveopts;
318 
319   /**
320    * The last plugin allowed to sort resources.
321    *
322    * <p>The jlink CLI is: <code>--resources-last-sorter name</code></p>
323    */
324   @Parameter
325   private String resourceslastsorter;
326 
327   /**
328    * Post process an existing image.
329    *
330    * <p>The jlink CLI is: <code>--post-process-path imagefile</code></p>
331    */
332   @Parameter
333   private File postprocesspath;
334 
335   /**
336    * Enable verbose tracing.
337    *
338    * <p>The jlink CLI is: <code>--verbose</code></p>
339    */
340   @Parameter(
341       defaultValue = "false"
342   )
343   private boolean verbose;
344 
345   /**
346    * Link service provider modules and their dependencies.
347    *
348    * <p>The jlink CLI is: <code>--bind-services</code></p>
349    */
350   @Parameter(
351       defaultValue = "false"
352   )
353   private boolean bindservices;
354 
355   /**
356    * Specifies the launcher command name for the module (and the main class).
357    *
358    * <p><pre>
359    * &lt;launcher&gt;
360    *   &lt;command&gt;mylauncher&lt;/command&gt;
361    *   &lt;mainmodule&gt;mainModule&lt;/mainmodule&gt;
362    *   &lt;mainclass&gt;mainClass&lt;/mainclass&gt;
363    * &lt;/launcher&gt;
364    * </pre></p>
365    *
366    * <p>The jlink CLI is:
367    * <code>--launcher command=main-module[/main-class]</code></p>
368    */
369   @Parameter
370   private Launcher launcher;
371 
372   /**
373    * Excludes header files.
374    *
375    * <p>The jlink CLI is: <code>--no-header-files</code></p>
376    */
377   @Parameter(
378       defaultValue = "false"
379   )
380   private boolean noheaderfiles;
381 
382   /**
383    * Excludes man pages.
384    *
385    * <p>The jlink CLI is: <code>--no-man-pages</code></p>
386    */
387   @Parameter(
388       defaultValue = "false"
389   )
390   private boolean nomanpages;
391 
392   /**
393    * Specifies the byte order of the generated image: { NATIVE | LITTLE | BIG }.
394    *
395    * <p>The jlink CLI is: <code>--endian {little|big}</code></p>
396    */
397   @Parameter(
398       defaultValue = "NATIVE"
399   )
400   private Endian endian;
401 
402   /**
403    * Suppresses a fatal error when signed modular JARs are linked
404    * in the runtime image. The signature-related files of the signed
405    * modular JARs aren't copied to the runtime image.
406    *
407    * <p>The jlink CLI is: <code>--ignore-signing-information</code></p>
408    */
409   @Parameter(
410       defaultValue = "false"
411   )
412   private boolean ignoresigninginformation;
413 
414   /**
415    * Disables the specified plug-ins.
416    * For a complete list of all available plug-ins,
417    * run the command: <code>jlink --list-plugins</code>
418    *
419    * <p><pre>
420    * &lt;disableplugins&gt;
421    *   &lt;disableplugin&gt;compress&lt;/disableplugin&gt;
422    *   &lt;disableplugin&gt;dedup-legal-notices&lt;/disableplugin&gt;
423    * &lt;/disableplugins&gt;
424    * </pre></p>
425    *
426    * <p>The jlink CLI is: <code>--disable-plugin pluginname</code></p>
427    */
428   @Parameter
429   private List<String> disableplugins;
430 
431   /*
432     For plug-in options that require a pattern-list, the value is
433     a comma-separated list of elements, with each element using one
434     the following forms:
435 
436       - glob-pattern
437       - glob:glob-pattern
438       - regex:regex-pattern
439       - @filename
440 
441     Example: *&#42;/module-info.class,glob:/java.base/java/lang/**,@file
442   */
443 
444   /**
445    * Compresses all resources in the output image. Specify
446    * compression { NO_COMPRESSION | CONSTANT_STRING_SHARING | ZIP }.
447    * An optional pattern-list filter can be specified to list
448    * the pattern of files to include.
449    *
450    * <p><pre>
451    * &lt;compress&gt;
452    *   &lt;compression&gt;ZIP&lt;/compression&gt;
453    *   &lt;filters&gt;
454    *     &lt;filter&gt;*&#42;/*-info.class&lt;/filter&gt;
455    *     &lt;filter&gt;glob:*&#42;/module-info.class&lt;/filter&gt;
456    *     &lt;filter&gt;regex:/java[a-z]+$&lt;/filter&gt;
457    *     &lt;filter&gt;@filename&lt;/filter&gt;
458    *   &lt;/filters&gt;
459    * &lt;/compress&gt;
460    * </pre></p>
461    *
462    * <p>The jlink CLI is:
463    * <code>--compress={0|1|2}[:filter=pattern-list]</code></p>
464    */
465   @Parameter
466   private Compress compress;
467 
468   /**
469    * Includes the list of locales where langtag is
470    * a BCP 47 language tag. This option supports locale matching as
471    * defined in RFC 4647. CAUTION! Ensure that you specified:
472    * <code>‒‒add-modules jdk.localedata</code> when using this property.
473    *
474    * <p><pre>
475    * &lt;includelocales&gt;
476    *   &lt;includelocale&gt;en&lt;/includelocale&gt;
477    *   &lt;includelocale&gt;ja&lt;/includelocale&gt;
478    *   &lt;includelocale&gt;*-IN&lt;/includelocale&gt;
479    * &lt;/includelocales&gt;
480    * </pre></p>
481    *
482    * <p>The jlink CLI is:
483    * <code>--include-locales=langtag[,langtag ...]</code></p>
484    */
485   @Parameter
486   private List<String> includelocales;
487 
488   /**
489    * Orders the specified paths in priority order.
490    *
491    * <p><pre>
492    * &lt;orderresources&gt;
493    *   &lt;orderresource&gt;*&#42;/*-info.class&lt;/orderresource&gt;
494    *   &lt;orderresource&gt;glob:*&#42;/module-info.class&lt;/orderresource&gt;
495    *   &lt;orderresource&gt;regex:/java[a-z]+$&lt;/orderresource&gt;
496    *   &lt;orderresource&gt;@filename&lt;/orderresource&gt;
497    * &lt;/orderresources&gt;
498    * </pre></p>
499    *
500    * <p>The jlink CLI is: <code>--order-resources=pattern-list</code></p>
501    */
502   @Parameter
503   private List<String> orderresources;
504 
505   /**
506    * Specify resources to exclude.
507    *
508    * <p><pre>
509    * &lt;excluderesources&gt;
510    *   &lt;excluderesource&gt;*&#42;/*-info.class&lt;/excluderesource&gt;
511    *   &lt;excluderesource&gt;glob:*&#42;/module-info.class&lt;/excluderesource&gt;
512    *   &lt;excluderesource&gt;regex:/java[a-z]+$&lt;/excluderesource&gt;
513    *   &lt;excluderesource&gt;@filename&lt;/excluderesource&gt;
514    * &lt;/excluderesources&gt;
515    * </pre></p>
516    *
517    * <p>The jlink CLI is: <code>--order-resources=pattern-list</code></p>
518    */
519   @Parameter
520   private List<String> excluderesources;
521 
522   /**
523    * Strips debug information from the output image.
524    *
525    * <p>The jlink CLI is: <code>--strip-debug</code></p>
526    */
527   @Parameter(
528       defaultValue = "false"
529   )
530   private boolean stripdebug;
531 
532   /**
533    * Strip Java debug attributes from classes in the output image.
534    *
535    * <p>The jlink CLI is: <code>--strip-java-debug-attributes</code></p>
536    */
537   @Parameter(
538       defaultValue = "false"
539   )
540   private boolean stripjavadebugattributes;
541 
542   /**
543    * Exclude native commands (such as java/java.exe) from the image.
544    *
545    * <p>The jlink CLI is: <code>--strip-native-commands</code></p>
546    */
547   @Parameter(
548       defaultValue = "false"
549   )
550   private boolean stripnativecommands;
551 
552   /**
553    * De-duplicate all legal notices. If true is specified then
554    * it will be an error if two files of the same filename
555    * are different.
556    *
557    * <p>The jlink CLI is:
558    * <code>--dedup-legal-notices=error-if-not-same-content</code></p>
559    */
560   @Parameter(
561       defaultValue = "false"
562   )
563   private boolean deduplegalnotices;
564 
565   /**
566    * Specify files to exclude.
567    *
568    * <p><pre>
569    * &lt;excludefiles&gt;
570    *   &lt;excludefile&gt;*&#42;/*-info.class&lt;/excludefile&gt;
571    *   &lt;excludefile&gt;glob:*&#42;/module-info.class&lt;/excludefile&gt;
572    *   &lt;excludefile&gt;regex:/java[a-z]+$&lt;/excludefile&gt;
573    *   &lt;excludefile&gt;@filename&lt;/excludefile&gt;
574    * &lt;/excludefiles&gt;
575    * </pre></p>
576    *
577    * <p>The jlink CLI is: <code>--exclude-files=pattern-list</code></p>
578    */
579   @Parameter
580   private List<String> excludefiles;
581 
582   /**
583    * Specify a JMOD section to exclude { MAN | HEADERS }.
584    *
585    * <p>The jlink CLI is: <code>--exclude-jmod-section={man|headers}</code></p>
586    */
587   @Parameter
588   private Section excludejmodsection;
589 
590   /**
591    * Specify a file listing the java.lang.invoke classes to pre-generate.
592    * By default, this plugin may use a builtin list of classes
593    * to pre-generate. If this plugin runs on a different runtime
594    * version than the image being created then code generation
595    * will be disabled by default to guarantee correctness add
596    * ignore-version=true to override this.
597    *
598    * <p>The jlink CLI is: <code>--generate-jli-classes=@filename</code></p>
599    */
600   @Parameter
601   private File generatejliclasses;
602 
603   /**
604    * Load release properties from the supplied option file.
605    * - adds: is to add properties to the release file.
606    * - dels: is to delete the list of keys in release file.
607    * - Any number of key=value pairs can be passed.
608    *
609    * <p><pre>
610    * &lt;releaseinfo&gt;
611    *   &lt;file&gt;file&lt;/file&gt;
612    *   &lt;adds&gt;
613    *     &lt;key1&gt;value1&lt;/key1&gt;
614    *     &lt;key2&gt;value2&lt;/key2&gt;
615    *   &lt;/adds&gt;
616    *   &lt;dells&gt;
617    *     &lt;key1 /&gt;
618    *     &lt;key2 /&gt;
619    *   &lt;/dells&gt;
620    * &lt;/releaseinfo&gt;
621    * </pre></p>
622    *
623    * </p>The jlink CLI is:
624    * <code>--release-info=file|add:key1=value1:key2=value2:...|del:key-list
625    * </code></p>
626    */
627   @Parameter
628   private ReleaseInfo releaseinfo;
629 
630   // /**
631   //  * Fast loading of module descriptors. Always on.
632   //  *
633   //  * <p>Default value: true.</p>
634   //  *
635   //  * <p>The jlink CLI is: <code>--system-modules=</code></p>
636   //  */
637   // @Parameter(
638   //     defaultValue = "true"
639   // )
640   // private boolean systemmodules;
641 
642   /**
643    * Select the HotSpot VM in
644    * the output image: { CLIENT | SERVER | MINIMAL | ALL }.
645    *
646    * <p>Default is ALL.</p>
647    *
648    * <p>The jlink CLI is: <code>--vm={client|server|minimal|all}</code></p>
649    */
650   @Parameter
651   private HotSpot vm;
652 
653   /**
654    * Resolve project dependencies.
655    *
656    * @return map of the resolved project dependencies
657    *
658    * @throws MojoExecutionException if any errors occurred while resolving
659    *                                dependencies
660    */
661   private ResolvePathsResult<File> resolveDependencies()
662       throws MojoExecutionException {
663 
664     // get project artifacts - all dependencies that this project has,
665     // including transitive ones (depends on what phases have run)
666     final Set<Artifact> artifacts = getProject().getArtifacts();
667     if (getLog().isDebugEnabled()) {
668       getLog().debug(PluginUtils.getArtifactSetDebugInfo(artifacts));
669     }
670 
671     // create a list of the paths which will be resolved
672     final List<File> paths = new ArrayList<>();
673 
674     // add the project output directory
675     paths.add(getOutputDir());
676 
677     // SCOPE_COMPILE  - This is the default scope, used if none is specified.
678     //                  Compile dependencies are available in all classpaths.
679     //                  Furthermore, those dependencies are propagated to
680     //                  dependent projects.
681     // SCOPE_PROVIDED - This is much like compile, but indicates you expect
682     //                  the JDK or a container to provide it at runtime.
683     //                  It is only available on the compilation and
684     //                  test classpath, and is not transitive.
685     // SCOPE_SYSTEM   - This scope is similar to provided except that you
686     //                  have to provide the JAR which contains it explicitly.
687     //                  The artifact is always available and is not looked up
688     //                  in a repository.    
689     // SCOPE_RUNTIME  - This scope indicates that the dependency is not
690     //                  required for compilation, but is for execution.
691     //                  It is in the runtime and test classpaths, but not
692     //                  the compile classpath.
693     // SCOPE_TEST     - This scope indicates that the dependency is not
694     //                  required for normal use of the application, and is
695     //                  only available for the test compilation and execution
696     //                  phases. It is not transitive.
697     // SCOPE_IMPORT   - This scope indicates that the dependency is a managed
698     //                  POM dependency i.e. only other POM into
699     //                  the dependencyManagement section.
700 
701     // [ !SCOPE_TEST ] add the project artifacts files
702     paths.addAll(artifacts.stream()
703         .filter(a -> a != null && !Artifact.SCOPE_TEST.equals(a.getScope()))
704         .map(a -> a.getFile())
705         .collect(Collectors.toList()));
706 
707     // [ SCOPE_SYSTEM ] add the project system dependencies
708     // getSystemPath() is used only if the dependency scope is system
709     paths.addAll(getProject().getDependencies().stream()
710         .filter(d -> d != null && !StringUtils.isBlank(d.getSystemPath()))
711         .map(d -> new File(StringUtils.stripToEmpty(d.getSystemPath())))
712         .collect(Collectors.toList()));
713 
714     // create request contains all information
715     // required to analyze the project
716     final ResolvePathsRequest<File> request =
717         ResolvePathsRequest.ofFiles(paths);
718 
719     // this is used to resolve main module descriptor
720     final File descriptorFile =
721         getOutputDir().toPath().resolve(DESCRIPTOR_NAME).toFile();
722     if (descriptorFile.exists() && !descriptorFile.isDirectory()) {
723       request.setMainModuleDescriptor(descriptorFile);
724     }
725 
726     // this is used to extract the module name
727     if (getToolHomeDirectory() != null) {
728       request.setJdkHome(getToolHomeDirectory());
729     }
730 
731     // resolve project dependencies
732     try {
733       return locationManager.resolvePaths(request);
734     } catch (IOException ex) {
735       throw new MojoExecutionException(
736           "Error: Unable to resolve project dependencies", ex);
737     }
738 
739   }
740 
741   /**
742    * Fetch the resolved main module descriptor.
743    *
744    * @return main module descriptor or null if it not exists
745    */
746   private JavaModuleDescriptor fetchMainModuleDescriptor() {
747     final JavaModuleDescriptor descriptor =
748         projectDependencies.getMainModuleDescriptor();
749     if (descriptor == null) {
750       // detected that the project is non modular
751       if (getLog().isWarnEnabled()) {
752         getLog().warn("The main module descriptor not found");
753       }
754     } else {
755       if (getLog().isDebugEnabled()) {
756         getLog().debug(MessageFormat.format(
757             "Found the main module descriptor: [{0}]", descriptor.name()));
758       }
759     }
760     return descriptor;
761   }
762 
763   /**
764    * Fetch path exceptions for every modulename which resolution failed.
765    *
766    * @return pairs of path exception file and cause
767    */
768   private Map<File, String> fetchPathExceptions() {
769     return projectDependencies.getPathExceptions()
770         .entrySet().stream()
771         .filter(entry -> entry != null && entry.getKey() != null)
772         .collect(Collectors.toMap(
773             entry -> entry.getKey(),
774             entry -> PluginUtils.getThrowableCause(entry.getValue())
775         ));
776   }
777 
778   /**
779    * Fetch classpath elements.
780    *
781    * @return classpath elements
782    */
783   private List<File> fetchClasspathElements() {
784     final List<File> result = projectDependencies.getClasspathElements()
785         .stream()
786         .filter(Objects::nonNull)
787         .collect(Collectors.toList());
788     if (getLog().isDebugEnabled()) {
789       getLog().debug("Found classpath elements: " + result.size()
790           + System.lineSeparator()
791           + result.stream()
792               .map(file -> file.toString())
793               .collect(Collectors.joining(System.lineSeparator())));
794     }
795     return result;
796   }
797 
798   /**
799    * Fetch modulepath elements.
800    *
801    * @return modulepath elements
802    */
803   private List<File> fetchModulepathElements() {
804     final List<File> result = projectDependencies.getModulepathElements()
805         .keySet()
806         .stream()
807         .filter(Objects::nonNull)
808         .collect(Collectors.toList());
809     if (getLog().isDebugEnabled()) {
810       getLog().debug("Found modulepath elements: " + result.size()
811           + System.lineSeparator()
812           + projectDependencies.getModulepathElements().entrySet().stream()
813               .filter(entry -> entry != null && entry.getKey() != null)
814               .map(entry -> entry.getKey().toString()
815                   + (ModuleNameSource.FILENAME.equals(entry.getValue())
816                       ? System.lineSeparator()
817                           + "[!] Detected 'requires' filename based "
818                           + "automatic module"
819                           + System.lineSeparator()
820                           + "[!] Please don't publish this project to "
821                           + "a public artifact repository"
822                           + System.lineSeparator()
823                           + (mainModuleDescriptor != null
824                               && mainModuleDescriptor.exports().isEmpty()
825                                   ? "[!] APPLICATION"
826                                   : "[!] LIBRARY")
827                       : ""))
828               .collect(Collectors.joining(System.lineSeparator())));
829     }
830     return result;
831   }
832 
833   /**
834    * Get path from the pathelements parameter.
835    *
836    * @return path contains parameter elements
837    */
838   private String getPathElements() {
839     String result = null;
840     if (modulepath != null) {
841       final List<File> pathelements = modulepath.getPathElements();
842       if (pathelements != null && !pathelements.isEmpty()) {
843         result = pathelements.stream()
844             .filter(Objects::nonNull)
845             .map(file -> file.toString())
846             .collect(Collectors.joining(File.pathSeparator));
847         if (getLog().isDebugEnabled()) {
848           getLog().debug(PluginUtils.getPathElementsDebugInfo("PATHELEMENTS",
849               pathelements));
850           getLog().debug(result);
851         }
852       }
853     }
854     return result;
855   }
856 
857   /**
858    * Get filesets from modulepath parameter.
859    *
860    * @return path contains filesets
861    *
862    * @throws MojoExecutionException if any errors occurred while resolving
863    *                                a fileset
864    */
865   private String getFileSets() throws MojoExecutionException {
866     String result = null;
867     if (modulepath != null) {
868       final List<FileSet> filesets = modulepath.getFileSets();
869       if (filesets != null && !filesets.isEmpty()) {
870         for (final FileSet fileSet : filesets) {
871           final File fileSetDir;
872           try {
873             fileSetDir =
874                 PluginUtils.normalizeFileSetBaseDir(getBaseDir(), fileSet);
875           } catch (IOException ex) {
876             throw new MojoExecutionException(
877                 "Error: Unable to resolve fileset", ex);
878           }
879           result = Stream.of(getFileSetManager().getIncludedFiles(fileSet))
880               .filter(fileName -> !StringUtils.isBlank(fileName))
881               .map(fileName -> fileSetDir.toPath().resolve(
882                   StringUtils.stripToEmpty(fileName)).toString())
883               .collect(Collectors.joining(File.pathSeparator));
884           if (getLog().isDebugEnabled()) {
885             getLog().debug(PluginUtils.getFileSetDebugInfo("FILESET",
886                 fileSet, result));
887           }
888         }
889       }
890     }
891     return result;
892   }
893 
894   /**
895    * Get dirsets from modulepath parameter.
896    *
897    * @return path contains dirsets
898    *
899    * @throws MojoExecutionException if any errors occurred while resolving
900    *                                a dirset
901    */
902   private String getDirSets() throws MojoExecutionException {
903     String result = null;
904     if (modulepath != null) {
905       final List<FileSet> dirsets = modulepath.getDirSets();
906       if (dirsets != null && !dirsets.isEmpty()) {
907         for (final FileSet dirSet : dirsets) {
908           final File dirSetDir;
909           try {
910             dirSetDir =
911                 PluginUtils.normalizeFileSetBaseDir(getBaseDir(), dirSet);
912           } catch (IOException ex) {
913             throw new MojoExecutionException(
914                 "Error: Unable to resolve dirset", ex);
915           }
916           result = Stream.of(getFileSetManager().getIncludedDirectories(dirSet))
917               .filter(dirName -> !StringUtils.isBlank(dirName))
918               .map(dirName -> dirSetDir.toPath().resolve(
919                   StringUtils.stripToEmpty(dirName)).toString())
920               .collect(Collectors.joining(File.pathSeparator));
921           if (getLog().isDebugEnabled()) {
922             getLog().debug(PluginUtils.getFileSetDebugInfo("DIRSET",
923                 dirSet, result));
924           }
925         }
926       }
927     }
928     return result;
929   }
930 
931   /**
932    * Get dependencysets from modulepath parameter.
933    *
934    * @return path contains dependencysets
935    */
936   private String getDependencySets() {
937     String result = null;
938     if (modulepath != null) {
939       final List<DependencySet> dependencysets =
940           modulepath.getDependencySets();
941       if (dependencysets != null && !dependencysets.isEmpty()) {
942         for (final DependencySet dependencySet : dependencysets) {
943           result = getIncludedDependencies(dependencySet)
944               .stream()
945               .collect(Collectors.joining(File.pathSeparator));
946           if (getLog().isDebugEnabled()) {
947             getLog().debug(PluginUtils.getDependencySetDebugInfo(
948                 "DEPENDENCYSET", dependencySet, result));
949           }
950         }
951       }
952     }
953     return result;
954   }
955 
956   /**
957    * Get the included project dependencies
958    * defined in the specified dependencyset.
959    *
960    * @param depSet the dependencyset
961    *
962    * @return the set of the included project dependencies
963    */
964   private Set<String> getIncludedDependencies(final DependencySet depSet) {
965     return projectDependencies.getPathElements().entrySet().stream()
966         .filter(entry -> entry != null
967             && entry.getKey() != null
968             && filterDependency(depSet, entry.getKey(), entry.getValue()))
969         .map(entry -> entry.getKey().toString())
970         .collect(Collectors.toSet());
971   }
972 
973   /**
974    * Get the excluded project dependencies
975    * defined in the specified dependencyset.
976    *
977    * @param depSet the dependencyset
978    *
979    * @return the set of the excluded project dependencies
980    */
981   @SuppressWarnings("unused")
982   private Set<String> getExcludedDependencies(final DependencySet depSet) {
983     return projectDependencies.getPathElements().entrySet().stream()
984         .filter(entry -> entry != null
985             && entry.getKey() != null
986             && !filterDependency(depSet, entry.getKey(), entry.getValue()))
987         .map(entry -> entry.getKey().toString())
988         .collect(Collectors.toSet());
989   }
990 
991   /**
992    * Checks whether the dependency defined by the file and
993    * the module descriptor matches the rules defined in the dependencyset.
994    * The dependency that matches at least one include pattern will be included,
995    * but if the dependency matches at least one exclude pattern too,
996    * then the dependency will not be included.
997    *
998    * @param depSet the dependencyset
999    * @param file the dependency file
1000    * @param descriptor the dependency module descriptor
1001    *
1002    * @return will the dependency be accepted
1003    */
1004   private boolean filterDependency(final DependencySet depSet, final File file,
1005       final JavaModuleDescriptor descriptor) {
1006 
1007     if (descriptor == null) {
1008       if (getLog().isWarnEnabled()) {
1009         getLog().warn("Missing module descriptor: " + file);
1010       }
1011     } else {
1012       if (descriptor.isAutomatic() && getLog().isDebugEnabled()) {
1013         getLog().debug("Found automatic module: " + file);
1014       }
1015     }
1016 
1017     boolean isIncluded = false;
1018 
1019     if (depSet == null) {
1020       // include module by default
1021       isIncluded = true;
1022       // include automatic module by default
1023       if (descriptor != null && descriptor.isAutomatic()
1024           && getLog().isDebugEnabled()) {
1025         getLog().debug("Included automatic module: " + file);
1026       }
1027       // exclude output module by default
1028       if (file.compareTo(getOutputDir()) == 0) {
1029         isIncluded = false;
1030         if (getLog().isDebugEnabled()) {
1031           getLog().debug("Excluded output module: " + file);
1032         }
1033       }
1034     } else {
1035       if (descriptor != null && descriptor.isAutomatic()
1036           && depSet.isAutomaticExcluded()) {
1037         if (getLog().isDebugEnabled()) {
1038           getLog().debug("Excluded automatic module: " + file);
1039         }
1040       } else {
1041         if (file.compareTo(getOutputDir()) == 0) {
1042           if (depSet.isOutputIncluded()) {
1043             isIncluded = true;
1044             if (getLog().isDebugEnabled()) {
1045               getLog().debug("Included output module: " + file);
1046             }
1047           } else {
1048             if (getLog().isDebugEnabled()) {
1049               getLog().debug("Excluded output module: " + file);
1050             }
1051           }
1052         } else {
1053           isIncluded = matchesIncludes(depSet, file, descriptor)
1054               && !matchesExcludes(depSet, file, descriptor);
1055         }
1056       }
1057     }
1058 
1059     if (getLog().isDebugEnabled()) {
1060       getLog().debug(PluginUtils.getDependencyDebugInfo(file, descriptor,
1061           isIncluded));
1062     }
1063 
1064     return isIncluded;
1065   }
1066 
1067   /**
1068    * Checks whether the dependency defined by the file and
1069    * the module descriptor matches the include patterns
1070    * from the dependencyset.
1071    *
1072    * @param depSet the dependencyset
1073    * @param file the file
1074    * @param descriptor the module descriptor
1075    *
1076    * @return should the dependency be included
1077    */
1078   private boolean matchesIncludes(final DependencySet depSet, final File file,
1079       final JavaModuleDescriptor descriptor) {
1080 
1081     final String name = descriptor == null ? "" : descriptor.name();
1082 
1083     final List<String> includes = depSet.getIncludes();
1084     final List<String> includenames = depSet.getIncludeNames();
1085 
1086     boolean result = true;
1087 
1088     if (includenames == null || includenames.isEmpty()) {
1089       if (includes == null || includes.isEmpty()) {
1090         result = true;
1091       } else {
1092         result = pathMatches(includes, file.toPath());
1093       }
1094     } else {
1095       if (includes == null || includes.isEmpty()) {
1096         result = nameMatches(includenames, name);
1097       } else {
1098         result = pathMatches(includes, file.toPath())
1099             || nameMatches(includenames, name);
1100       }
1101     }
1102     return result;
1103   }
1104 
1105   /**
1106    * Checks whether the dependency defined by the file and
1107    * the module descriptor matches the exclude patterns
1108    * from the dependencyset.
1109    *
1110    * @param depSet the dependencyset
1111    * @param file the file
1112    * @param descriptor the module descriptor
1113    *
1114    * @return should the dependency be excluded
1115    */
1116   private boolean matchesExcludes(final DependencySet depSet, final File file,
1117       final JavaModuleDescriptor descriptor) {
1118 
1119     final String name = descriptor == null ? "" : descriptor.name();
1120 
1121     final List<String> excludes = depSet.getExcludes();
1122     final List<String> excludenames = depSet.getExcludeNames();
1123 
1124     boolean result = false;
1125 
1126     if (excludenames == null || excludenames.isEmpty()) {
1127       if (excludes == null || excludes.isEmpty()) {
1128         result = false;
1129       } else {
1130         result = pathMatches(excludes, file.toPath());
1131       }
1132     } else {
1133       if (excludes == null || excludes.isEmpty()) {
1134         result = nameMatches(excludenames, name);
1135       } else {
1136         result = pathMatches(excludes, file.toPath())
1137             || nameMatches(excludenames, name);
1138       }
1139     }
1140     return result;
1141   }
1142 
1143   /**
1144    * Checks if the path matches at least one of the patterns.
1145    * The pattern should be regex or glob, this is determined
1146    * by the prefix specified in the pattern.
1147    *
1148    * @param patterns the list of patterns
1149    * @param path the file path
1150    *
1151    * @return true if the path matches at least one of the patterns or
1152    *              if no patterns are specified
1153    */
1154   private boolean pathMatches(final List<String> patterns, final Path path) {
1155     for (final String pattern : patterns) {
1156       final PathMatcher pathMatcher =
1157           FileSystems.getDefault().getPathMatcher(pattern);
1158       if (pathMatcher.matches(path)) {
1159         return true;
1160       }
1161     }
1162     return false;
1163   }
1164 
1165   /**
1166    * Checks if the name matches at least one of the patterns.
1167    * The pattern should be regex only.
1168    *
1169    * @param patterns the list of patterns
1170    * @param name the name
1171    *
1172    * @return true if the name matches at least one of the patterns or
1173    *              if no patterns are specified
1174    */
1175   private boolean nameMatches(final List<String> patterns, final String name) {
1176     for (final String pattern : patterns) {
1177       final Pattern regexPattern = Pattern.compile(pattern);
1178       final Matcher nameMatcher = regexPattern.matcher(name);
1179       if (nameMatcher.matches()) {
1180         return true;
1181       }
1182     }
1183     return false;
1184   }
1185 
1186   /**
1187    * Process modules.
1188    *
1189    * @param cmdLine the command line builder
1190    *
1191    * @throws MojoExecutionException if any errors occurred
1192    */
1193   private void processModules(final CommandLineBuilder cmdLine)
1194       throws MojoExecutionException {
1195     CommandLineOption opt = null;
1196     // modulepath
1197     if (modulepath != null) {
1198       final StringBuilder path = new StringBuilder();
1199       final String pathElements = getPathElements();
1200       if (!StringUtils.isBlank(pathElements)) {
1201         path.append(StringUtils.stripToEmpty(pathElements));
1202       }
1203       final String fileSets = getFileSets();
1204       if (!StringUtils.isBlank(fileSets)) {
1205         if (path.length() != 0) {
1206           path.append(File.pathSeparator);
1207         }
1208         path.append(StringUtils.stripToEmpty(fileSets));
1209       }
1210       final String dirSets = getDirSets();
1211       if (!StringUtils.isBlank(dirSets)) {
1212         if (path.length() != 0) {
1213           path.append(File.pathSeparator);
1214         }
1215         path.append(StringUtils.stripToEmpty(dirSets));
1216       }
1217       final String dependencySets = getDependencySets();
1218       if (!StringUtils.isBlank(dependencySets)) {
1219         if (path.length() != 0) {
1220           path.append(File.pathSeparator);
1221         }
1222         path.append(StringUtils.stripToEmpty(dependencySets));
1223       }
1224       if (path.length() != 0) {
1225         opt = cmdLine.createOpt();
1226         opt.createArg().setValue("--module-path");
1227         opt.createArg().setValue(path.toString());
1228       }
1229     }
1230     // addmodules
1231     if (includelocales != null && !includelocales.isEmpty()) {
1232       if (addmodules == null) {
1233         addmodules = new ArrayList<>();
1234       }
1235       addmodules.add("jdk.localedata");
1236     }
1237     if (addmodules != null && !addmodules.isEmpty()) {
1238       opt = cmdLine.createOpt();
1239       opt.createArg().setValue("--add-modules");
1240       opt.createArg().setValue(
1241           addmodules.stream().collect(Collectors.joining(",")));
1242     }
1243   }
1244 
1245   /**
1246    * Process options.
1247    *
1248    * @param cmdLine the command line builder
1249    *
1250    * @throws MojoExecutionException if any errors occurred
1251    */
1252   private void processOptions(final CommandLineBuilder cmdLine)
1253       throws MojoExecutionException {
1254     CommandLineOption opt = null;
1255     // output
1256     opt = cmdLine.createOpt();
1257     opt.createArg().setValue("--output");
1258     try {
1259       opt.createArg().setValue(output.getCanonicalPath());
1260     } catch (IOException ex) {
1261       throw new MojoExecutionException(MessageFormat.format(
1262           ERROR_RESOLVE,
1263           "--output",
1264           output.toString()), ex);
1265     }
1266     // saveopts
1267     if (saveopts != null) {
1268       opt = cmdLine.createOpt();
1269       opt.createArg().setValue("--save-opts");
1270       try {
1271         opt.createArg().setValue(saveopts.getCanonicalPath());
1272       } catch (IOException ex) {
1273         throw new MojoExecutionException(MessageFormat.format(
1274             ERROR_RESOLVE,
1275             "--save-opts",
1276             saveopts.toString()), ex);
1277       }
1278     }
1279     // postprocesspath
1280     if (postprocesspath != null) {
1281       opt = cmdLine.createOpt();
1282       opt.createArg().setValue("--post-process-path");
1283       try {
1284         opt.createArg().setValue(postprocesspath.getCanonicalPath());
1285       } catch (IOException ex) {
1286         throw new MojoExecutionException(MessageFormat.format(
1287             ERROR_RESOLVE,
1288             "--post-process-path",
1289             postprocesspath.toString()), ex);
1290       }
1291     }
1292     // resourceslastsorter
1293     if (!StringUtils.isBlank(resourceslastsorter)) {
1294       opt = cmdLine.createOpt();
1295       opt.createArg().setValue("--resources-last-sorter");
1296       opt.createArg().setValue(StringUtils.stripToEmpty(resourceslastsorter));
1297     }
1298     // verbose
1299     if (verbose) {
1300       opt = cmdLine.createOpt();
1301       opt.createArg().setValue("--verbose");
1302     }
1303     // bindservices
1304     if (bindservices) {
1305       opt = cmdLine.createOpt();
1306       opt.createArg().setValue("--bind-services");
1307     }
1308     // noheaderfiles
1309     if (noheaderfiles) {
1310       opt = cmdLine.createOpt();
1311       opt.createArg().setValue("--no-header-files");
1312     }
1313     // nomanpages
1314     if (nomanpages) {
1315       opt = cmdLine.createOpt();
1316       opt.createArg().setValue("--no-man-pages");
1317     }
1318     // ignoresigninginformation
1319     if (ignoresigninginformation) {
1320       opt = cmdLine.createOpt();
1321       opt.createArg().setValue("--ignore-signing-information");
1322     }
1323     // stripdebug
1324     if (stripdebug) {
1325       opt = cmdLine.createOpt();
1326       opt.createArg().setValue("--strip-debug");
1327     }
1328     // stripjavadebugattributes
1329     if (stripjavadebugattributes) {
1330       if (toolJavaVersion.atLeast(JavaVersion.JAVA_13)) {
1331         opt = cmdLine.createOpt();
1332         opt.createArg().setValue("--strip-java-debug-attributes");
1333       } else {
1334         stripjavadebugattributes = false;
1335         if (getLog().isWarnEnabled()) {
1336           getLog().warn(MessageFormat.format(
1337               "Parameter [{0}] skiped, at least {1} is required to use it",
1338               "--strip-java-debug-attributes",
1339               JavaVersion.JAVA_13));
1340         }
1341       }
1342     }
1343     // stripnativecommands
1344     if (stripnativecommands) {
1345       opt = cmdLine.createOpt();
1346       opt.createArg().setValue("--strip-native-commands");
1347     }
1348     // deduplegalnotices
1349     if (deduplegalnotices) {
1350       opt = cmdLine.createOpt();
1351       opt.createArg().setValue(
1352           "--dedup-legal-notices=error-if-not-same-content");
1353     }
1354     // limitmodules
1355     if (limitmodules != null && !limitmodules.isEmpty()) {
1356       opt = cmdLine.createOpt();
1357       opt.createArg().setValue("--limit-modules");
1358       opt.createArg().setValue(
1359           limitmodules.stream().collect(Collectors.joining(",")));
1360     }
1361     // suggestproviders
1362     if (suggestproviders != null && !suggestproviders.isEmpty()) {
1363       opt = cmdLine.createOpt();
1364       opt.createArg().setValue("--suggest-providers");
1365       opt.createArg().setValue(
1366           suggestproviders.stream().collect(Collectors.joining(",")));
1367     }
1368     // endian
1369     if (endian != null && !endian.equals(Endian.NATIVE)) {
1370       opt = cmdLine.createOpt();
1371       opt.createArg().setValue("--endian");
1372       opt.createArg().setValue(endian.toString().toLowerCase(Locale.ROOT));
1373     }
1374     // disableplugins
1375     if (disableplugins != null) {
1376       for (final String plugin : disableplugins) {
1377         opt = cmdLine.createOpt();
1378         opt.createArg().setValue("--disable-plugin");
1379         opt.createArg().setValue(plugin);
1380       }
1381     }
1382     // includelocales
1383     if (includelocales != null && !includelocales.isEmpty()) {
1384       opt = cmdLine.createOpt();
1385       opt.createArg().setValue(
1386           includelocales.stream()
1387               .collect(Collectors.joining(",", "--include-locales=", "")));
1388     }
1389     // excludejmodsection
1390     if (excludejmodsection != null) {
1391       opt = cmdLine.createOpt();
1392       opt.createArg().setValue("--exclude-jmod-section="
1393           + excludejmodsection.toString().toLowerCase(Locale.ROOT));
1394     }
1395     // generatejliclasses
1396     if (generatejliclasses != null) {
1397       opt = cmdLine.createOpt();
1398       try {
1399         opt.createArg().setValue("--generate-jli-classes=@"
1400             + generatejliclasses.getCanonicalPath());
1401       } catch (IOException ex) {
1402         throw new MojoExecutionException(MessageFormat.format(
1403             ERROR_RESOLVE,
1404             "----generate-jli-classes",
1405             generatejliclasses.toString()), ex);
1406       }
1407     }
1408     // vm
1409     if (vm != null) {
1410       opt = cmdLine.createOpt();
1411       opt.createArg().setValue("--vm="
1412           + vm.toString().toLowerCase(Locale.ROOT));
1413     }
1414     // launcher
1415     if (launcher != null) {
1416       final String launcherCommand =
1417           StringUtils.stripToEmpty(launcher.getCommand());
1418       if (!StringUtils.isBlank(launcherCommand)) {
1419         final String launcherModule =
1420             StringUtils.stripToEmpty(launcher.getMainModule());
1421         if (!StringUtils.isBlank(launcherModule)) {
1422           opt = cmdLine.createOpt();
1423           opt.createArg().setValue("--launcher");
1424           final String launcherClass =
1425               StringUtils.stripToEmpty(launcher.getMainClass());
1426           if (StringUtils.isBlank(launcherClass)) {
1427             opt.createArg().setValue(launcherCommand + "="
1428                 + launcherModule);
1429           } else {
1430             opt.createArg().setValue(launcherCommand + "="
1431                 + launcherModule + "/" + launcherClass);
1432           }
1433         }
1434       }
1435     }
1436     // compress
1437     if (compress != null) {
1438       final Compression compression = compress.getCompression();
1439       final List<String> filters = compress.getFilters();
1440       if (compression != null) {
1441         final StringBuilder option = new StringBuilder("--compress=");
1442         option.append(compression.getValue());
1443         if (filters != null) {
1444           option.append(filters.stream()
1445               .collect(Collectors.joining(",", ":filter=", "")));
1446         }
1447         opt = cmdLine.createOpt();
1448         opt.createArg().setValue(option.toString());
1449       }
1450     }
1451     // orderresources
1452     if (orderresources != null && !orderresources.isEmpty()) {
1453       opt = cmdLine.createOpt();
1454       opt.createArg().setValue(orderresources.stream()
1455           .collect(Collectors.joining(",", "--order-resources=", "")));
1456     }
1457     // excluderesources
1458     if (excluderesources != null && !excluderesources.isEmpty()) {
1459       opt = cmdLine.createOpt();
1460       opt.createArg().setValue(excluderesources.stream()
1461           .collect(Collectors.joining(",", "--exclude-resources=", "")));
1462     }
1463     // excludefiles
1464     if (excludefiles != null && !excludefiles.isEmpty()) {
1465       opt = cmdLine.createOpt();
1466       opt.createArg().setValue(excludefiles.stream()
1467           .collect(Collectors.joining(",", "--exclude-files=", "")));
1468     }
1469     // releaseinfo
1470     if (releaseinfo != null) {
1471       final StringBuilder option = new StringBuilder();
1472       final File releaseinfofile = releaseinfo.getFile();
1473       if (releaseinfofile != null) {
1474         option.append(releaseinfofile.toString());
1475       }
1476       final Map<String, String> adds = releaseinfo.getAdds();
1477       if (adds != null && !adds.entrySet().isEmpty()) {
1478         if (option.length() != 0) {
1479           option.append(':');
1480         }
1481         option.append(adds.entrySet().stream()
1482             .filter(add -> add != null && !StringUtils.isBlank(add.getKey()))
1483             .map(add -> StringUtils.stripToEmpty(add.getKey()) + "="
1484                 + StringUtils.stripToEmpty(add.getValue()))
1485             .collect(Collectors.joining(":", "add:", "")));
1486       }
1487       final Map<String, String> dels = releaseinfo.getDels();
1488       if (dels != null && !dels.entrySet().isEmpty()) {
1489         if (option.length() != 0) {
1490           option.append(':');
1491         }
1492         option.append(dels.entrySet().stream()
1493             .filter(del -> del != null && !StringUtils.isBlank(del.getKey()))
1494             .map(del -> StringUtils.stripToEmpty(del.getKey()))
1495             .collect(Collectors.joining(":", "del:", "")));
1496       }
1497       opt = cmdLine.createOpt();
1498       opt.createArg().setValue("--release-info=" + option.toString());
1499     }
1500   }
1501 
1502   /**
1503    * Copy files (only files, not directories) to the specified directory.
1504    *
1505    * @param files the list of files
1506    * @param dir the destination directory
1507    *
1508    * @throws MojoExecutionException if any errors occurred while copying a file
1509    */
1510   private void copyFiles(final List<File> files, final File dir)
1511       throws MojoExecutionException {
1512     if (getLog().isDebugEnabled()) {
1513       getLog().debug(MessageFormat.format("Copy files to: [{0}]", dir));
1514     }
1515     for (final File file : files) {
1516       try {
1517         if (file.exists()) {
1518           if (file.isDirectory()) {
1519             if (getLog().isDebugEnabled()) {
1520               getLog().debug(MessageFormat.format("Skiped directory: [{0}]",
1521                   file));
1522             }
1523           } else {
1524             FileUtils.copyFileToDirectory(file, dir);
1525             if (getLog().isDebugEnabled()) {
1526               getLog().debug(MessageFormat.format("Copied file: [{0}]", file));
1527             }
1528           }
1529         }
1530       } catch (IOException | IllegalArgumentException ex) {
1531         throw new MojoExecutionException(MessageFormat.format(
1532             "Error: Unable to copy file: [{0}]", file), ex);
1533       }
1534     }
1535   }
1536 
1537   /**
1538    * Process launcher scripts.
1539    *
1540    * @throws MojoExecutionException if any errors occurred
1541    */
1542   private void processLauncherScripts() throws MojoExecutionException {
1543     if (launcher == null) {
1544       return;
1545     }
1546 
1547     final String scriptName = StringUtils.stripToEmpty(launcher.getCommand());
1548     if (StringUtils.isBlank(scriptName)) {
1549       return;
1550     }
1551 
1552     final Path nixScript = output.toPath().resolve("bin/" + scriptName);
1553     final Path winScript = output.toPath().resolve("bin/" + scriptName
1554         + ".bat");
1555 
1556     if (stripnativecommands) {
1557       if (Files.exists(nixScript) && !Files.isDirectory(nixScript)) {
1558         try {
1559           FileUtils.forceDelete(nixScript.toFile());
1560         } catch (IOException ex) {
1561           if (getLog().isWarnEnabled()) {
1562             getLog().warn(MessageFormat.format(
1563                 "Unable to delete launcher script: [{0}]", nixScript));
1564           }
1565         }
1566       }
1567       if (Files.exists(winScript) && !Files.isDirectory(winScript)) {
1568         try {
1569           FileUtils.forceDelete(winScript.toFile());
1570         } catch (IOException ex) {
1571           if (getLog().isWarnEnabled()) {
1572             getLog().warn(MessageFormat.format(
1573                 "Unable to delete launcher script: [{0}]", winScript));
1574           }
1575         }
1576       }
1577       return;
1578     }
1579 
1580     final String moduleName = StringUtils.stripToEmpty(
1581         launcher.getMainModule());
1582     if (StringUtils.isEmpty(moduleName)) {
1583       return;
1584     }
1585 
1586     final String mainClassName = StringUtils.stripToEmpty(
1587         launcher.getMainClass());
1588 
1589     final StringBuilder mainName = new StringBuilder(moduleName);
1590     if (!StringUtils.isEmpty(mainClassName)) {
1591       mainName
1592           .append('/')
1593           .append(mainClassName);
1594     }
1595 
1596     final String args = StringUtils.stripToEmpty(launcher.getArgs());
1597 
1598     final String jvmArgs = StringUtils.stripToEmpty(launcher.getJvmArgs());
1599 
1600     if (getLog().isDebugEnabled()) {
1601       getLog().debug(System.lineSeparator()
1602           + "Processing launcher scripts with following variables:"
1603           + System.lineSeparator()
1604           + MessageFormat.format("  - moduleName = [{0}]", moduleName)
1605           + System.lineSeparator()
1606           + MessageFormat.format("  - mainClassName = [{0}]", mainClassName)
1607           + System.lineSeparator()
1608           + MessageFormat.format("  - mainName = [{0}]", mainName.toString())
1609           + System.lineSeparator()
1610           + MessageFormat.format("  - args = [{0}]", args)
1611           + System.lineSeparator()
1612           + MessageFormat.format("  - jvmArgs = [{0}]", jvmArgs));
1613     }
1614 
1615     final Map<String, String> data = new HashMap<>();
1616     data.put("moduleName", moduleName);
1617     data.put("mainClassName", mainClassName);
1618     data.put("mainName", mainName.toString());
1619     data.put("args", args);
1620     data.put("jvmArgs", jvmArgs);
1621 
1622     final File nixTemplate = launcher.getNixTemplate();
1623     if (nixTemplate != null && Files.exists(nixTemplate.toPath())
1624         && !Files.isDirectory(nixTemplate.toPath())) {
1625       createLauncherScript(nixScript, nixTemplate.toPath(), data);
1626     }
1627 
1628     final File winTemplate = launcher.getWinTemplate();
1629     if (winTemplate != null && Files.exists(winTemplate.toPath())
1630         && !Files.isDirectory(winTemplate.toPath())) {
1631       createLauncherScript(winScript, winTemplate.toPath(), data);
1632     }
1633 
1634   }
1635 
1636   /**
1637    * Create launcher script.
1638    *
1639    * @param script the launcher script file path
1640    * @param template the launcher template file path
1641    * @param data the hash map contains variable names and values to substitute
1642    *
1643    * @throws MojoExecutionException if any errors occurred while processing
1644    *                                launcher script files
1645    */
1646   private void createLauncherScript(final Path script, final Path template,
1647       final Map<String, String> data) throws MojoExecutionException {
1648     if (getLog().isDebugEnabled()) {
1649       getLog().debug(System.lineSeparator()
1650           + MessageFormat.format("Fixing launcher script: [{0}]", script)
1651           + System.lineSeparator()
1652           + MessageFormat.format("with template: [{0}]", template));
1653     }
1654     final StringSubstitutor engine = new StringSubstitutor(data)
1655         .setEnableUndefinedVariableException(true)
1656         .setPreserveEscapes(true)
1657         .setEscapeChar('\\');
1658     try {
1659       Files.write(script,
1660           Files.lines(template, getCharset())
1661               .map(line -> engine.replace(line).replace("\\$", "$"))
1662               .collect(Collectors.toList()),
1663           getCharset());
1664     } catch (IllegalArgumentException ex) {
1665       throw new MojoExecutionException(MessageFormat.format(
1666           "Error: Variable not found in the launcher template file: [{0}]",
1667           template), ex);
1668     } catch (IOException ex) {
1669       throw new MojoExecutionException(MessageFormat.format(
1670           "Error: Unable to write to the launcher script file: [{0}]",
1671           script), ex);
1672     }
1673   }
1674 
1675   /**
1676    * Execute goal.
1677    *
1678    * @throws MojoExecutionException if any errors occurred
1679    */
1680   @Override
1681   public void execute() throws MojoExecutionException {
1682 
1683     // Init
1684     init(TOOL_NAME, toolhome, TOOL_HOME_BIN); // from BaseToolMojo
1685 
1686     // Check version
1687     toolJavaVersion = getToolJavaVersion();
1688     if (toolJavaVersion == null
1689         || !toolJavaVersion.atLeast(JavaVersion.JAVA_9)) {
1690       throw new MojoExecutionException(MessageFormat.format(
1691           "Error: At least {0} is required to use [{1}]", JavaVersion.JAVA_9,
1692           TOOL_NAME));
1693     }
1694 
1695     // Create mods directory
1696     try {
1697       FileUtils.forceMkdir(modsdir);
1698     } catch (IOException | IllegalArgumentException ex) {
1699       throw new MojoExecutionException(MessageFormat.format(
1700           "Error: Unable to create mods directory: [{0}]", modsdir), ex);
1701     }
1702 
1703     // Create libs directory
1704     try {
1705       FileUtils.forceMkdir(libsdir);
1706     } catch (IOException | IllegalArgumentException ex) {
1707       throw new MojoExecutionException(MessageFormat.format(
1708           "Error: Unable to create libs directory: [{0}]", libsdir), ex);
1709     }
1710 
1711     // Delete image output directory if it exists
1712     if (getLog().isDebugEnabled()) {
1713       getLog().debug(MessageFormat.format("Output directory: [{0}]", output));
1714     }
1715     if (output.exists() && output.isDirectory()) {
1716       try {
1717         FileUtils.deleteDirectory(output);
1718       } catch (IOException ex) {
1719         throw new MojoExecutionException(MessageFormat.format(
1720             "Error: Unable to delete image output directory: [{0}]", output),
1721             ex);
1722       }
1723     }
1724 
1725     // Resolve and fetch project dependencies
1726     projectDependencies = resolveDependencies();
1727     mainModuleDescriptor = fetchMainModuleDescriptor();
1728     List<File> classpathElements = fetchClasspathElements();
1729     List<File> modulepathElements = fetchModulepathElements();
1730     Map<File, String> pathExceptions = fetchPathExceptions();
1731     if (!pathExceptions.isEmpty() && getLog().isWarnEnabled()) {
1732       getLog().warn("Found path exceptions: " + pathExceptions.size()
1733           + System.lineSeparator()
1734           + pathExceptions.entrySet().stream()
1735               .map(entry -> entry.getKey().toString()
1736                   + System.lineSeparator()
1737                   + entry.getValue())
1738               .collect(Collectors.joining(System.lineSeparator())));
1739     }
1740 
1741     // copy dependencies
1742     copyFiles(modulepathElements, modsdir);
1743     copyFiles(classpathElements, libsdir);
1744 
1745     // Build command line and populate the list of the command options
1746     final CommandLineBuilder cmdLineBuilder = new CommandLineBuilder();
1747     cmdLineBuilder.setExecutable(getToolExecutable().toString());
1748     processOptions(cmdLineBuilder);
1749     processModules(cmdLineBuilder);
1750     final List<String> optsLines = new ArrayList<>();
1751     optsLines.add("# " + TOOL_NAME);
1752     optsLines.addAll(cmdLineBuilder.buildOptionList());
1753     if (getLog().isDebugEnabled()) {
1754       getLog().debug(optsLines.stream()
1755           .collect(Collectors.joining(System.lineSeparator(),
1756               System.lineSeparator(), "")));
1757     }
1758 
1759     // Save the list of command options to the file
1760     // will be used in the tool command line
1761     final Path cmdOptsPath = getBuildDir().toPath().resolve(OPTS_FILE);
1762     try {
1763       Files.write(cmdOptsPath, optsLines, getCharset());
1764     } catch (IOException ex) {
1765       throw new MojoExecutionException(MessageFormat.format(
1766           "Error: Unable to write command options to file: [{0}]",
1767           cmdOptsPath), ex);
1768     }
1769 
1770     // Prepare command line with command options
1771     // specified in the file created early
1772     final Commandline cmdLine = new Commandline();
1773     cmdLine.setExecutable(getToolExecutable().toString());
1774     cmdLine.createArg().setValue("@" + cmdOptsPath.toString());
1775 
1776     // Execute command line
1777     int exitCode = 0;
1778     try {
1779       exitCode = execCmdLine(cmdLine); // from BaseToolMojo
1780     } catch (CommandLineException ex) {
1781       throw new MojoExecutionException(MessageFormat.format(
1782           "Error: Unable to execute [{0}] tool", TOOL_NAME), ex);
1783     }
1784     if (exitCode != 0) {
1785       if (getLog().isErrorEnabled()) {
1786         getLog().error(System.lineSeparator()
1787             + "Command options was: "
1788             + System.lineSeparator()
1789             + optsLines.stream()
1790                 .collect(Collectors.joining(System.lineSeparator())));
1791       }
1792       throw new MojoExecutionException(MessageFormat.format(
1793           "Error: Tool execution failed [{0}] with exit code: {1}", TOOL_NAME,
1794           exitCode));
1795     }
1796 
1797     // Process launcher scripts
1798     processLauncherScripts();
1799 
1800     // Delete temporary file
1801     try {
1802       FileUtils.forceDelete(cmdOptsPath.toFile());
1803     } catch (IOException ex) {
1804       throw new MojoExecutionException(MessageFormat.format(
1805           "Error: Unable to delete temporary file: [{0}]", cmdOptsPath), ex);
1806     }
1807 
1808   }
1809 
1810 }