1 /*
2  * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4  *
5  * This code is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License version 2 only, as
7  * published by the Free Software Foundation.
8  *
9  * This code is distributed in the hope that it will be useful, but WITHOUT
10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12  * version 2 for more details (a copy is included in the LICENSE file that
13  * accompanied this code).
14  *
15  * You should have received a copy of the GNU General Public License version
16  * 2 along with this work; if not, write to the Free Software Foundation,
17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18  *
19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20  * or visit www.oracle.com if you need additional information or have any
21  * questions.
22  */
23 package tests;
24 
25 import java.io.ByteArrayInputStream;
26 import java.io.ByteArrayOutputStream;
27 import java.io.File;
28 import java.io.FileOutputStream;
29 import java.io.IOException;
30 import java.io.InputStream;
31 import java.io.OutputStream;
32 import java.io.PrintStream;
33 import java.io.PrintWriter;
34 import java.io.StringWriter;
35 import java.nio.file.Files;
36 import java.nio.file.Path;
37 import java.nio.file.Paths;
38 import java.nio.file.StandardCopyOption;
39 import java.util.ArrayList;
40 import java.util.Arrays;
41 import java.util.Collections;
42 import java.util.HashSet;
43 import java.util.List;
44 import java.util.Objects;
45 import java.util.Set;
46 import java.util.jar.JarEntry;
47 import java.util.jar.JarInputStream;
48 import java.util.jar.JarOutputStream;
49 import java.util.stream.Collectors;
50 import java.util.stream.Stream;
51 import java.util.zip.ZipEntry;
52 
53 import javax.tools.JavaCompiler;
54 import javax.tools.StandardJavaFileManager;
55 import javax.tools.StandardLocation;
56 import javax.tools.ToolProvider;
57 
58 /**
59  *
60  * A generator for jmods, jars and images.
61  */
62 public class JImageGenerator {
63 
64     public static final String LOAD_ALL_CLASSES_TEMPLATE = "package PACKAGE;\n"
65             + "\n"
66             + "import java.net.URI;\n"
67             + "import java.nio.file.FileSystems;\n"
68             + "import java.nio.file.Files;\n"
69             + "import java.nio.file.Path;\n"
70             + "import java.util.function.Function;\n"
71             + "\n"
72             + "public class CLASS {\n"
73             + "    private static long total_time;\n"
74             + "    private static long num_classes;\n"
75             + "    public static void main(String[] args) throws Exception {\n"
76             + "        Function<Path, String> formatter = (path) -> {\n"
77             + "            String clazz = path.toString().substring(\"modules/\".length()+1, path.toString().lastIndexOf(\".\"));\n"
78             + "            clazz = clazz.substring(clazz.indexOf(\"/\") + 1);\n"
79             + "            return clazz.replaceAll(\"/\", \"\\\\.\");\n"
80             + "        };\n"
81             + "        Files.walk(FileSystems.getFileSystem(URI.create(\"jrt:/\")).getPath(\"/modules/\")).\n"
82             + "                filter((p) -> {\n"
83             + "                    return Files.isRegularFile(p) && p.toString().endsWith(\".class\")\n"
84             + "                    && !p.toString().endsWith(\"module-info.class\");\n"
85             + "                }).\n"
86             + "                map(formatter).forEach((clazz) -> {\n"
87             + "                    try {\n"
88             + "                        long t = System.currentTimeMillis();\n"
89             + "                        Class.forName(clazz, false, Thread.currentThread().getContextClassLoader());\n"
90             + "                        total_time+= System.currentTimeMillis()-t;\n"
91             + "                        num_classes+=1;\n"
92             + "                    } catch (IllegalAccessError ex) {\n"
93             + "                        // Security exceptions can occur, this is not what we are testing\n"
94             + "                        System.err.println(\"Access error, OK \" + clazz);\n"
95             + "                    } catch (Exception ex) {\n"
96             + "                        System.err.println(\"ERROR \" + clazz);\n"
97             + "                        throw new RuntimeException(ex);\n"
98             + "                    }\n"
99             + "                });\n"
100             + "    double res = (double) total_time / num_classes;\n"
101             + "    // System.out.println(\"Total time \" + total_time + \" num classes \" + num_classes + \" average \" + res);\n"
102             + "    }\n"
103             + "}\n";
104 
105     private static final String OUTPUT_OPTION = "--output";
106     private static final String POST_PROCESS_OPTION = "--post-process-path";
107     private static final String MAIN_CLASS_OPTION = "--main-class";
108     private static final String CLASS_PATH_OPTION = "--class-path";
109     private static final String MODULE_PATH_OPTION = "--module-path";
110     private static final String ADD_MODULES_OPTION = "--add-modules";
111     private static final String LIMIT_MODULES_OPTION = "--limit-modules";
112     private static final String PLUGIN_MODULE_PATH = "--plugin-module-path";
113     private static final String LAUNCHER = "--launcher";
114 
115     private static final String CMDS_OPTION = "--cmds";
116     private static final String CONFIG_OPTION = "--config";
117     private static final String HASH_MODULES_OPTION = "--hash-modules";
118     private static final String LIBS_OPTION = "--libs";
119     private static final String MODULE_VERSION_OPTION = "--module-version";
120 
JImageGenerator()121     private JImageGenerator() {}
122 
optionsPrettyPrint(String... args)123     private static String optionsPrettyPrint(String... args) {
124         return Stream.of(args).collect(Collectors.joining(" "));
125     }
126 
getJModsDir(File jdkHome)127     public static File getJModsDir(File jdkHome) {
128         File jdkjmods = new File(jdkHome, "jmods");
129         if (!jdkjmods.exists()) {
130             return null;
131         }
132         return jdkjmods;
133     }
134 
addFiles(Path module, InMemoryFile... resources)135     public static Path addFiles(Path module, InMemoryFile... resources) throws IOException {
136         Path tempFile = Files.createTempFile("jlink-test", "");
137         try (JarInputStream in = new JarInputStream(Files.newInputStream(module));
138              JarOutputStream out = new JarOutputStream(new FileOutputStream(tempFile.toFile()))) {
139             ZipEntry entry;
140             while ((entry = in.getNextEntry()) != null) {
141                 String name = entry.getName();
142                 out.putNextEntry(new ZipEntry(name));
143                 copy(in, out);
144                 out.closeEntry();
145             }
146             for (InMemoryFile r : resources) {
147                 addFile(r, out);
148             }
149         }
150         Files.move(tempFile, module, StandardCopyOption.REPLACE_EXISTING);
151         return module;
152     }
153 
copy(InputStream in, OutputStream out)154     private static void copy(InputStream in, OutputStream out) throws IOException {
155         int len;
156         byte[] buf = new byte[4096];
157         while ((len = in.read(buf)) > 0) {
158             out.write(buf, 0, len);
159         }
160     }
161 
getJModTask()162     public static JModTask getJModTask() {
163         return new JModTask();
164     }
165 
getJLinkTask()166     public static JLinkTask getJLinkTask() {
167         return new JLinkTask();
168     }
169 
getJImageTask()170     public static JImageTask getJImageTask() {
171         return new JImageTask();
172     }
173 
addFile(InMemoryFile resource, JarOutputStream target)174     private static void addFile(InMemoryFile resource, JarOutputStream target) throws IOException {
175         String fileName = resource.getPath();
176         fileName = fileName.replace("\\", "/");
177         String[] ss = fileName.split("/");
178         Path p = Paths.get("");
179         for (int i = 0; i < ss.length; ++i) {
180             if (i < ss.length - 1) {
181                 if (!ss[i].isEmpty()) {
182                     p = p.resolve(ss[i]);
183                     JarEntry entry = new JarEntry(p.toString() + "/");
184                     target.putNextEntry(entry);
185                     target.closeEntry();
186                 }
187             } else {
188                 p = p.resolve(ss[i]);
189                 JarEntry entry = new JarEntry(p.toString());
190                 target.putNextEntry(entry);
191                 copy(resource.getBytes(), target);
192                 target.closeEntry();
193             }
194         }
195     }
196 
createNewFile(Path root, String pathName, String extension)197     public static Path createNewFile(Path root, String pathName, String extension) {
198         Path out = root.resolve(pathName + extension);
199         int i = 1;
200         while (Files.exists(out)) {
201             out = root.resolve(pathName + "-" + (++i) + extension);
202         }
203         return out;
204     }
205 
generateSources(Path output, String moduleName, List<InMemorySourceFile> sources)206     public static Path generateSources(Path output, String moduleName, List<InMemorySourceFile> sources) throws IOException {
207         Path moduleDir = output.resolve(moduleName);
208         Files.createDirectory(moduleDir);
209         for (InMemorySourceFile source : sources) {
210             Path fileDir = moduleDir;
211             if (!source.packageName.isEmpty()) {
212                 String dir = source.packageName.replace('.', File.separatorChar);
213                 fileDir = moduleDir.resolve(dir);
214                 Files.createDirectories(fileDir);
215             }
216             Files.write(fileDir.resolve(source.className + ".java"), source.source.getBytes());
217         }
218         return moduleDir;
219     }
220 
generateSourcesFromTemplate(Path output, String moduleName, String... classNames)221     public static Path generateSourcesFromTemplate(Path output, String moduleName, String... classNames) throws IOException {
222         List<InMemorySourceFile> sources = new ArrayList<>();
223         for (String className : classNames) {
224             String packageName = getPackageName(className);
225             String simpleName = getSimpleName(className);
226             String content = LOAD_ALL_CLASSES_TEMPLATE
227                     .replace("CLASS", simpleName);
228             if (packageName.isEmpty()) {
229                 content = content.replace("package PACKAGE;", packageName);
230             } else {
231                 content = content.replace("PACKAGE", packageName);
232             }
233             sources.add(new InMemorySourceFile(packageName, simpleName, content));
234         }
235         return generateSources(output, moduleName, sources);
236     }
237 
generateModuleInfo(Path moduleDir, List<String> packages, String... dependencies)238     public static void generateModuleInfo(Path moduleDir, List<String> packages, String... dependencies) throws IOException {
239         StringBuilder moduleInfoBuilder = new StringBuilder();
240         Path file = moduleDir.resolve("module-info.java");
241         String moduleName = moduleDir.getFileName().toString();
242         moduleInfoBuilder.append("module ").append(moduleName).append("{\n");
243         for (String dep : dependencies) {
244             moduleInfoBuilder.append("requires ").append(dep).append(";\n");
245         }
246         for (String pkg : packages) {
247             if (!pkg.trim().isEmpty()) {
248                 moduleInfoBuilder.append("exports ").append(pkg).append(";\n");
249             }
250         }
251         moduleInfoBuilder.append("}");
252         Files.write(file, moduleInfoBuilder.toString().getBytes());
253     }
254 
compileSuccess(Path source, Path destination, String... options)255     public static void compileSuccess(Path source, Path destination, String... options) throws IOException {
256         if (!compile(source, destination, options)) {
257             throw new AssertionError("Compilation failed.");
258         }
259     }
260 
compile(Path source, Path destination, String... options)261     public static boolean compile(Path source, Path destination, String... options) throws IOException {
262         JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
263         try (StandardJavaFileManager jfm = compiler.getStandardFileManager(null, null, null)) {
264             List<Path> sources
265                     = Files.find(source, Integer.MAX_VALUE,
266                     (file, attrs) -> file.toString().endsWith(".java"))
267                     .collect(Collectors.toList());
268 
269             Files.createDirectories(destination);
270             jfm.setLocationFromPaths(StandardLocation.CLASS_OUTPUT, Collections.singleton(destination));
271 
272             List<String> opts = Arrays.asList(options);
273             JavaCompiler.CompilationTask task
274                     = compiler.getTask(null, jfm, null, opts, null,
275                     jfm.getJavaFileObjectsFromPaths(sources));
276             List<String> list = new ArrayList<>(opts);
277             list.addAll(sources.stream()
278                     .map(Path::toString)
279                     .collect(Collectors.toList()));
280             System.err.println("javac options: " + optionsPrettyPrint(list.toArray(new String[list.size()])));
281             return task.call();
282         }
283     }
284 
createJarFile(Path jarfile, Path dir)285     public static Path createJarFile(Path jarfile, Path dir) throws IOException {
286         return createJarFile(jarfile, dir, Paths.get("."));
287     }
288 
createJarFile(Path jarfile, Path dir, Path file)289     public static Path createJarFile(Path jarfile, Path dir, Path file) throws IOException {
290         // create the target directory
291         Path parent = jarfile.getParent();
292         if (parent != null)
293             Files.createDirectories(parent);
294 
295         List<Path> entries = Files.find(dir.resolve(file), Integer.MAX_VALUE,
296                 (p, attrs) -> attrs.isRegularFile())
297                 .map(dir::relativize)
298                 .collect(Collectors.toList());
299 
300         try (OutputStream out = Files.newOutputStream(jarfile);
301              JarOutputStream jos = new JarOutputStream(out)) {
302             for (Path entry : entries) {
303                 // map the file path to a name in the JAR file
304                 Path normalized = entry.normalize();
305                 String name = normalized
306                         .subpath(0, normalized.getNameCount())  // drop root
307                         .toString()
308                         .replace(File.separatorChar, '/');
309 
310                 jos.putNextEntry(new JarEntry(name));
311                 Files.copy(dir.resolve(entry), jos);
312             }
313         }
314         return jarfile;
315     }
316 
getModuleContent(Path module)317     public static Set<String> getModuleContent(Path module) {
318         Result result = JImageGenerator.getJModTask()
319                 .jmod(module)
320                 .list();
321         result.assertSuccess();
322         return Stream.of(result.getMessage().split("\r?\n"))
323                 .collect(Collectors.toSet());
324     }
325 
checkModule(Path module, Set<String> expected)326     public static void checkModule(Path module, Set<String> expected) throws IOException {
327         Set<String> actual = getModuleContent(module);
328         if (!Objects.equals(actual, expected)) {
329             Set<String> unexpected = new HashSet<>(actual);
330             unexpected.removeAll(expected);
331             Set<String> notFound = new HashSet<>(expected);
332             notFound.removeAll(actual);
333             System.err.println("Unexpected files:");
334             unexpected.forEach(s -> System.err.println("\t" + s));
335             System.err.println("Not found files:");
336             notFound.forEach(s -> System.err.println("\t" + s));
337             throw new AssertionError("Module check failed.");
338         }
339     }
340 
341     public static class JModTask {
342         static final java.util.spi.ToolProvider JMOD_TOOL =
343             java.util.spi.ToolProvider.findFirst("jmod").orElseThrow(() ->
344                 new RuntimeException("jmod tool not found")
345             );
346 
347         private final List<Path> classpath = new ArrayList<>();
348         private final List<Path> libs = new ArrayList<>();
349         private final List<Path> cmds = new ArrayList<>();
350         private final List<Path> config = new ArrayList<>();
351         private final List<Path> jars = new ArrayList<>();
352         private final List<Path> jmods = new ArrayList<>();
353         private final List<String> options = new ArrayList<>();
354         private Path output;
355         private String hashModules;
356         private String mainClass;
357         private String moduleVersion;
358 
addNativeLibraries(Path cp)359         public JModTask addNativeLibraries(Path cp) {
360             this.libs.add(cp);
361             return this;
362         }
363 
hashModules(String hash)364         public JModTask hashModules(String hash) {
365             this.hashModules = hash;
366             return this;
367         }
368 
addCmds(Path cp)369         public JModTask addCmds(Path cp) {
370             this.cmds.add(cp);
371             return this;
372         }
373 
addClassPath(Path cp)374         public JModTask addClassPath(Path cp) {
375             this.classpath.add(cp);
376             return this;
377         }
378 
addConfig(Path cp)379         public JModTask addConfig(Path cp) {
380             this.config.add(cp);
381             return this;
382         }
383 
addJars(Path jars)384         public JModTask addJars(Path jars) {
385             this.jars.add(jars);
386             return this;
387         }
388 
addJmods(Path jmods)389         public JModTask addJmods(Path jmods) {
390             this.jmods.add(jmods);
391             return this;
392         }
393 
jmod(Path output)394         public JModTask jmod(Path output) {
395             this.output = output;
396             return this;
397         }
398 
moduleVersion(String moduleVersion)399         public JModTask moduleVersion(String moduleVersion) {
400             this.moduleVersion = moduleVersion;
401             return this;
402         }
403 
mainClass(String mainClass)404         public JModTask mainClass(String mainClass) {
405             this.mainClass = mainClass;
406             return this;
407         }
408 
option(String o)409         public JModTask option(String o) {
410             this.options.add(o);
411             return this;
412         }
413 
modulePath()414         private String modulePath() {
415             // This is expect FIRST jmods THEN jars, if you change this, some tests could fail
416             String jmods = toPath(this.jmods);
417             String jars = toPath(this.jars);
418             return jmods + File.pathSeparator + jars;
419         }
420 
toPath(List<Path> paths)421         private String toPath(List<Path> paths) {
422             return paths.stream()
423                     .map(Path::toString)
424                     .collect(Collectors.joining(File.pathSeparator));
425         }
426 
optionsJMod(String cmd)427         private String[] optionsJMod(String cmd) {
428             List<String> options = new ArrayList<>();
429             options.add(cmd);
430             if (!cmds.isEmpty()) {
431                 options.add(CMDS_OPTION);
432                 options.add(toPath(cmds));
433             }
434             if (!config.isEmpty()) {
435                 options.add(CONFIG_OPTION);
436                 options.add(toPath(config));
437             }
438             if (hashModules != null) {
439                 options.add(HASH_MODULES_OPTION);
440                 options.add(hashModules);
441             }
442             if (mainClass != null) {
443                 options.add(MAIN_CLASS_OPTION);
444                 options.add(mainClass);
445             }
446             if (!libs.isEmpty()) {
447                 options.add(LIBS_OPTION);
448                 options.add(toPath(libs));
449             }
450             if (!classpath.isEmpty()) {
451                 options.add(CLASS_PATH_OPTION);
452                 options.add(toPath(classpath));
453             }
454             if (!jars.isEmpty() || !jmods.isEmpty()) {
455                 options.add(MODULE_PATH_OPTION);
456                 options.add(modulePath());
457             }
458             if (moduleVersion != null) {
459                 options.add(MODULE_VERSION_OPTION);
460                 options.add(moduleVersion);
461             }
462             options.addAll(this.options);
463             if (output != null) {
464                 options.add(output.toString());
465             }
466             return options.toArray(new String[options.size()]);
467         }
468 
create()469         public Result create() {
470             return cmd("create");
471         }
472 
list()473         public Result list() {
474             return cmd("list");
475         }
476 
call()477         public Result call() {
478             return cmd("");
479         }
480 
cmd(String cmd)481         private Result cmd(String cmd) {
482             String[] args = optionsJMod(cmd);
483             System.err.println("jmod options: " + optionsPrettyPrint(args));
484             ByteArrayOutputStream baos = new ByteArrayOutputStream();
485             PrintStream ps = new PrintStream(baos);
486             int exitCode = JMOD_TOOL.run(ps, ps, args);
487             String msg = new String(baos.toByteArray());
488             return new Result(exitCode, msg, output);
489         }
490     }
491 
getPackageName(String canonicalName)492     public static String getPackageName(String canonicalName) {
493         int index = canonicalName.lastIndexOf('.');
494         return index > 0 ? canonicalName.substring(0, index) : "";
495     }
496 
getSimpleName(String canonicalName)497     public static String getSimpleName(String canonicalName) {
498         int index = canonicalName.lastIndexOf('.');
499         return canonicalName.substring(index + 1);
500     }
501 
502     public static class JImageTask {
503 
504         private final List<Path> pluginModulePath = new ArrayList<>();
505         private final List<String> options = new ArrayList<>();
506         private Path dir;
507         private Path image;
508 
pluginModulePath(Path p)509         public JImageTask pluginModulePath(Path p) {
510             this.pluginModulePath.add(p);
511             return this;
512         }
513 
image(Path image)514         public JImageTask image(Path image) {
515             this.image = image;
516             return this;
517         }
518 
dir(Path dir)519         public JImageTask dir(Path dir) {
520             this.dir = dir;
521             return this;
522         }
523 
option(String o)524         public JImageTask option(String o) {
525             this.options.add(o);
526             return this;
527         }
528 
toPath(List<Path> paths)529         private String toPath(List<Path> paths) {
530             return paths.stream()
531                     .map(Path::toString)
532                     .collect(Collectors.joining(File.pathSeparator));
533         }
534 
optionsJImage(String cmd)535         private String[] optionsJImage(String cmd) {
536             List<String> options = new ArrayList<>();
537             options.add(cmd);
538             if (dir != null) {
539                 options.add("--dir");
540                 options.add(dir.toString());
541             }
542             if (!pluginModulePath.isEmpty()) {
543                 options.add(PLUGIN_MODULE_PATH);
544                 options.add(toPath(pluginModulePath));
545             }
546             options.addAll(this.options);
547             options.add(image.toString());
548             return options.toArray(new String[options.size()]);
549         }
550 
cmd(String cmd, Path returnPath)551         private Result cmd(String cmd, Path returnPath) {
552             String[] args = optionsJImage(cmd);
553             System.err.println("jimage options: " + optionsPrettyPrint(args));
554             StringWriter writer = new StringWriter();
555             int exitCode = jdk.tools.jimage.Main.run(args, new PrintWriter(writer));
556             return new Result(exitCode, writer.toString(), returnPath);
557         }
558 
extract()559         public Result extract() {
560             return cmd("extract", dir);
561         }
562     }
563 
564     public static class JLinkTask {
565         static final java.util.spi.ToolProvider JLINK_TOOL =
566             java.util.spi.ToolProvider.findFirst("jlink").orElseThrow(() ->
567                 new RuntimeException("jlink tool not found")
568             );
569 
570         private final List<Path> jars = new ArrayList<>();
571         private final List<Path> jmods = new ArrayList<>();
572         private final List<Path> pluginModulePath = new ArrayList<>();
573         private final List<String> addMods = new ArrayList<>();
574         private final List<String> limitMods = new ArrayList<>();
575         private final List<String> options = new ArrayList<>();
576         private String modulePath;
577         // if you want to specifiy repeated --module-path option
578         private String repeatedModulePath;
579         // if you want to specifiy repeated --limit-modules option
580         private String repeatedLimitMods;
581         private Path output;
582         private Path existing;
583         private String launcher; // optional
584 
modulePath(String modulePath)585         public JLinkTask modulePath(String modulePath) {
586             this.modulePath = modulePath;
587             return this;
588         }
589 
launcher(String cmd)590         public JLinkTask launcher(String cmd) {
591             launcher = Objects.requireNonNull(cmd);
592             return this;
593         }
594 
repeatedModulePath(String modulePath)595         public JLinkTask repeatedModulePath(String modulePath) {
596             this.repeatedModulePath = modulePath;
597             return this;
598         }
599 
addJars(Path jars)600         public JLinkTask addJars(Path jars) {
601             this.jars.add(jars);
602             return this;
603         }
604 
addJmods(Path jmods)605         public JLinkTask addJmods(Path jmods) {
606             this.jmods.add(jmods);
607             return this;
608         }
609 
pluginModulePath(Path p)610         public JLinkTask pluginModulePath(Path p) {
611             this.pluginModulePath.add(p);
612             return this;
613         }
614 
addMods(String moduleName)615         public JLinkTask addMods(String moduleName) {
616             this.addMods.add(moduleName);
617             return this;
618         }
619 
limitMods(String moduleName)620         public JLinkTask limitMods(String moduleName) {
621             this.limitMods.add(moduleName);
622             return this;
623         }
624 
repeatedLimitMods(String modules)625         public JLinkTask repeatedLimitMods(String modules) {
626             this.repeatedLimitMods = modules;
627             return this;
628         }
629 
output(Path output)630         public JLinkTask output(Path output) {
631             this.output = output;
632             return this;
633         }
634 
existing(Path existing)635         public JLinkTask existing(Path existing) {
636             this.existing = existing;
637             return this;
638         }
639 
option(String o)640         public JLinkTask option(String o) {
641             this.options.add(o);
642             return this;
643         }
644 
modulePath()645         private String modulePath() {
646             // This is expect FIRST jmods THEN jars, if you change this, some tests could fail
647             String jmods = toPath(this.jmods);
648             String jars = toPath(this.jars);
649             return jmods + File.pathSeparator + jars;
650         }
651 
toPath(List<Path> paths)652         private String toPath(List<Path> paths) {
653             return paths.stream()
654                     .map(Path::toString)
655                     .collect(Collectors.joining(File.pathSeparator));
656         }
657 
optionsJLink()658         private String[] optionsJLink() {
659             List<String> options = new ArrayList<>();
660             if (output != null) {
661                 options.add(OUTPUT_OPTION);
662                 options.add(output.toString());
663             }
664             if (!addMods.isEmpty()) {
665                 options.add(ADD_MODULES_OPTION);
666                 options.add(addMods.stream().collect(Collectors.joining(",")));
667             }
668             if (!limitMods.isEmpty()) {
669                 options.add(LIMIT_MODULES_OPTION);
670                 options.add(limitMods.stream().collect(Collectors.joining(",")));
671             }
672             if (repeatedLimitMods != null) {
673                 options.add(LIMIT_MODULES_OPTION);
674                 options.add(repeatedLimitMods);
675             }
676             if (!jars.isEmpty() || !jmods.isEmpty()) {
677                 options.add(MODULE_PATH_OPTION);
678                 options.add(modulePath());
679             }
680             if (modulePath != null) {
681                 options.add(MODULE_PATH_OPTION);
682                 options.add(modulePath);
683             }
684             if (repeatedModulePath != null) {
685                 options.add(MODULE_PATH_OPTION);
686                 options.add(repeatedModulePath);
687             }
688             if (!pluginModulePath.isEmpty()) {
689                 options.add(PLUGIN_MODULE_PATH);
690                 options.add(toPath(pluginModulePath));
691             }
692             if (launcher != null && !launcher.isEmpty()) {
693                 options.add(LAUNCHER);
694                 options.add(launcher);
695             }
696             options.addAll(this.options);
697             return options.toArray(new String[options.size()]);
698         }
699 
optionsPostProcessJLink()700         private String[] optionsPostProcessJLink() {
701             List<String> options = new ArrayList<>();
702             if (existing != null) {
703                 options.add(POST_PROCESS_OPTION);
704                 options.add(existing.toString());
705             }
706             options.addAll(this.options);
707             return options.toArray(new String[options.size()]);
708         }
709 
call()710         public Result call() {
711             String[] args = optionsJLink();
712             System.err.println("jlink options: " + optionsPrettyPrint(args));
713             StringWriter writer = new StringWriter();
714             PrintWriter pw = new PrintWriter(writer);
715             int exitCode = JLINK_TOOL.run(pw, pw, args);
716             return new Result(exitCode, writer.toString(), output);
717         }
718 
callPostProcess()719         public Result callPostProcess() {
720             String[] args = optionsPostProcessJLink();
721             System.err.println("jlink options: " + optionsPrettyPrint(args));
722             StringWriter writer = new StringWriter();
723             PrintWriter pw = new PrintWriter(writer);
724             int exitCode = JLINK_TOOL.run(pw, pw, args);
725             return new Result(exitCode, writer.toString(), output);
726         }
727     }
728 
729     public static class InMemorySourceFile {
730         public final String packageName;
731         public final String className;
732         public final String source;
733 
InMemorySourceFile(String packageName, String simpleName, String source)734         public InMemorySourceFile(String packageName, String simpleName, String source) {
735             this.packageName = packageName;
736             this.className = simpleName;
737             this.source = source;
738         }
739     }
740 
741     public static class InMemoryFile {
742         private final String path;
743         private final byte[] bytes;
744 
getPath()745         public String getPath() {
746             return path;
747         }
748 
getBytes()749         public InputStream getBytes() {
750             return new ByteArrayInputStream(bytes);
751         }
752 
InMemoryFile(String path, byte[] bytes)753         public InMemoryFile(String path, byte[] bytes) {
754             this.path = path;
755             this.bytes = bytes;
756         }
757 
InMemoryFile(String path, InputStream is)758         public InMemoryFile(String path, InputStream is) throws IOException {
759             this(path, readAllBytes(is));
760         }
761     }
762 
readAllBytes(InputStream is)763     public static byte[] readAllBytes(InputStream is) throws IOException {
764         ByteArrayOutputStream baos = new ByteArrayOutputStream();
765         byte[] buf = new byte[1024];
766         while (true) {
767             int n = is.read(buf);
768             if (n < 0) {
769                 break;
770             }
771             baos.write(buf, 0, n);
772         }
773         return baos.toByteArray();
774     }
775 }
776