1 /*
   2  * Copyright (c) 2018, 2019, 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 
  24 import java.io.File;
  25 import java.io.IOException;
  26 import java.io.PrintWriter;
  27 import java.io.StringWriter;
  28 import java.nio.file.FileVisitResult;
  29 
  30 import java.nio.file.Files;
  31 import java.nio.file.Path;
  32 import java.nio.file.SimpleFileVisitor;
  33 import java.nio.file.attribute.BasicFileAttributes;
  34 import java.util.ArrayList;
  35 import java.util.List;
  36 
  37 import java.util.spi.ToolProvider;
  38 
  39 public class JPackageHelper {
  40 
  41     private static final boolean VERBOSE = false;
  42     private static final String OS = System.getProperty("os.name").toLowerCase();
  43     private static final String JAVA_HOME = System.getProperty("java.home");
  44     public static final String TEST_SRC_ROOT;
  45     public static final String TEST_SRC;
  46     private static final Path BIN_DIR = Path.of(JAVA_HOME, "bin");
  47     private static final Path JPACKAGE;
  48     private static final Path JAVAC;
  49     private static final Path JAR;
  50     private static final Path JLINK;
  51 
  52     static {
  53         if (OS.startsWith("win")) {
  54             JPACKAGE = BIN_DIR.resolve("jpackage.exe");
  55             JAVAC = BIN_DIR.resolve("javac.exe");
  56             JAR = BIN_DIR.resolve("jar.exe");
  57             JLINK = BIN_DIR.resolve("jlink.exe");
  58         } else {
  59             JPACKAGE = BIN_DIR.resolve("jpackage");
  60             JAVAC = BIN_DIR.resolve("javac");
  61             JAR = BIN_DIR.resolve("jar");
  62             JLINK = BIN_DIR.resolve("jlink");
  63         }
  64 
  65         // Figure out test src based on where we called
  66         File testSrc = new File(System.getProperty("test.src") + File.separator + ".."
  67                 + File.separator + "apps");
  68         if (testSrc.exists()) {
  69             TEST_SRC_ROOT = System.getProperty("test.src") + File.separator + "..";
  70         } else {
  71             testSrc = new File(System.getProperty("test.src") + File.separator
  72                     + ".." + File.separator + ".." + File.separator + "apps");
  73             if (testSrc.exists()) {
  74                 TEST_SRC_ROOT = System.getProperty("test.src") + File.separator + ".."
  75                         + File.separator + "..";
  76             } else {
  77                 testSrc = new File(System.getProperty("test.src") + File.separator
  78                         + ".." + File.separator + ".."  + File.separator + ".."
  79                         + File.separator + "apps");
  80                 if (testSrc.exists()) {
  81                     TEST_SRC_ROOT = System.getProperty("test.src") + File.separator + ".."
  82                             + File.separator + ".." + File.separator + "..";
  83                 } else {
  84                     TEST_SRC_ROOT = System.getProperty("test.src");
  85                 }
  86             }
  87         }
  88 
  89         TEST_SRC = System.getProperty("test.src");
  90     }
  91 
  92     static final ToolProvider JPACKAGE_TOOL =
  93             ToolProvider.findFirst("jpackage").orElseThrow(
  94             () -> new RuntimeException("jpackage tool not found"));
  95 
  96     public static int execute(File out, String... command) throws Exception {
  97         if (VERBOSE) {
  98             System.out.print("Execute command: ");
  99             for (String c : command) {
 100                 System.out.print(c);
 101                 System.out.print(" ");
 102             }
 103             System.out.println();
 104         }
 105 
 106         ProcessBuilder builder = new ProcessBuilder(command);
 107         if (out != null) {
 108             builder.redirectErrorStream(true);
 109             builder.redirectOutput(out);
 110         }
 111 
 112         Process process = builder.start();
 113         return process.waitFor();
 114     }
 115 
 116     public static Process executeNoWait(File out, String... command) throws Exception {
 117         if (VERBOSE) {
 118             System.out.print("Execute command: ");
 119             for (String c : command) {
 120                 System.out.print(c);
 121                 System.out.print(" ");
 122             }
 123             System.out.println();
 124         }
 125 
 126         ProcessBuilder builder = new ProcessBuilder(command);
 127         if (out != null) {
 128             builder.redirectErrorStream(true);
 129             builder.redirectOutput(out);
 130         }
 131 
 132         return builder.start();
 133     }
 134 
 135     private static String[] getCommand(String... args) {
 136         String[] command;
 137         if (args == null) {
 138             command = new String[1];
 139         } else {
 140             command = new String[args.length + 1];
 141         }
 142 
 143         int index = 0;
 144         command[index] = JPACKAGE.toString();
 145 
 146         if (args != null) {
 147             for (String arg : args) {
 148                 index++;
 149                 command[index] = arg;
 150             }
 151         }
 152 
 153         return command;
 154     }
 155 
 156     public static void deleteRecursive(File path) throws IOException {
 157         if (!path.exists()) {
 158             return;
 159         }
 160 
 161         Path directory = path.toPath();
 162         Files.walkFileTree(directory, new SimpleFileVisitor<Path>() {
 163             @Override
 164             public FileVisitResult visitFile(Path file,
 165                     BasicFileAttributes attr) throws IOException {
 166                 if (OS.startsWith("win")) {
 167                     Files.setAttribute(file, "dos:readonly", false);
 168                 }
 169                 Files.delete(file);
 170                 return FileVisitResult.CONTINUE;
 171             }
 172 
 173             @Override
 174             public FileVisitResult preVisitDirectory(Path dir,
 175                     BasicFileAttributes attr) throws IOException {
 176                 if (OS.startsWith("win")) {
 177                     Files.setAttribute(dir, "dos:readonly", false);
 178                 }
 179                 return FileVisitResult.CONTINUE;
 180             }
 181 
 182             @Override
 183             public FileVisitResult postVisitDirectory(Path dir, IOException e)
 184                     throws IOException {
 185                 Files.delete(dir);
 186                 return FileVisitResult.CONTINUE;
 187             }
 188         });
 189     }
 190 
 191     public static void deleteOutputFolder(String output) throws IOException {
 192         File outputFolder = new File(output);
 193         System.out.println("AMDEBUG output: " + outputFolder.getAbsolutePath());
 194         try {
 195             deleteRecursive(outputFolder);
 196         } catch (IOException ioe) {
 197             System.out.println("IOException: " + ioe);
 198             ioe.printStackTrace();
 199             deleteRecursive(outputFolder);
 200         }
 201     }
 202 
 203     public static String executeCLI(boolean retValZero, String... args) throws Exception {
 204         int retVal;
 205         File outfile = new File("output.log");
 206         try {
 207             String[] command = getCommand(args);
 208             retVal = execute(outfile, command);
 209         } catch (Exception ex) {
 210             if (outfile.exists()) {
 211                 System.err.println(Files.readString(outfile.toPath()));
 212             }
 213             throw ex;
 214         }
 215 
 216         String output = Files.readString(outfile.toPath());
 217         if (retValZero) {
 218             if (retVal != 0) {
 219                 System.err.println(output);
 220                 throw new AssertionError("jpackage exited with error: " + retVal);
 221             }
 222         } else {
 223             if (retVal == 0) {
 224                 System.err.println(output);
 225                 throw new AssertionError("jpackage exited without error: " + retVal);
 226             }
 227         }
 228 
 229         if (VERBOSE) {
 230             System.out.println("output =");
 231             System.out.println(output);
 232         }
 233 
 234         return output;
 235     }
 236 
 237     public static String executeToolProvider(boolean retValZero, String... args) throws Exception {
 238         StringWriter writer = new StringWriter();
 239         PrintWriter pw = new PrintWriter(writer);
 240         int retVal = JPACKAGE_TOOL.run(pw, pw, args);
 241         String output = writer.toString();
 242 
 243         if (retValZero) {
 244             if (retVal != 0) {
 245                 System.err.println(output);
 246                 throw new AssertionError("jpackage exited with error: " + retVal);
 247             }
 248         } else {
 249             if (retVal == 0) {
 250                 System.err.println(output);
 251                 throw new AssertionError("jpackage exited without error");
 252             }
 253         }
 254 
 255         if (VERBOSE) {
 256             System.out.println("output =");
 257             System.out.println(output);
 258         }
 259 
 260         return output;
 261     }
 262 
 263     public static boolean isWindows() {
 264         return (OS.contains("win"));
 265     }
 266 
 267     public static boolean isOSX() {
 268         return (OS.contains("mac"));
 269     }
 270 
 271     public static boolean isLinux() {
 272         return ((OS.contains("nix") || OS.contains("nux")));
 273     }
 274 
 275     public static void createHelloImageJar() throws Exception {
 276         createJar(false, "Hello", "image");
 277     }
 278 
 279     public static void createHelloImageJarWithMainClass() throws Exception {
 280         createJar(true, "Hello", "image");
 281     }
 282 
 283     public static void createHelloInstallerJar() throws Exception {
 284         createJar(false, "Hello", "installer");
 285     }
 286 
 287     public static void createHelloInstallerJarWithMainClass() throws Exception {
 288         createJar(true, "Hello", "installer");
 289     }
 290 
 291     private static void createJar(boolean mainClassAttribute, String name,
 292                                   String testType) throws Exception {
 293         int retVal;
 294 
 295         File input = new File("input");
 296         if (!input.exists()) {
 297             input.mkdir();
 298         }
 299 
 300         Files.copy(Path.of(TEST_SRC_ROOT + File.separator + "apps" + File.separator
 301                 + testType + File.separator + name + ".java"), Path.of(name + ".java"));
 302 
 303         File javacLog = new File("javac.log");
 304         try {
 305             retVal = execute(javacLog, JAVAC.toString(), name + ".java");
 306         } catch (Exception ex) {
 307             if (javacLog.exists()) {
 308                 System.err.println(Files.readString(javacLog.toPath()));
 309             }
 310             throw ex;
 311         }
 312 
 313         if (retVal != 0) {
 314             if (javacLog.exists()) {
 315                 System.err.println(Files.readString(javacLog.toPath()));
 316             }
 317             throw new AssertionError("javac exited with error: " + retVal);
 318         }
 319 
 320         File jarLog = new File("jar.log");
 321         try {
 322             List<String> args = new ArrayList<>();
 323             args.add(JAR.toString());
 324             args.add("-c");
 325             args.add("-v");
 326             args.add("-f");
 327             args.add("input" + File.separator + name.toLowerCase() + ".jar");
 328             if (mainClassAttribute) {
 329                 args.add("-e");
 330                 args.add(name);
 331             }
 332             args.add(name + ".class");
 333             retVal = execute(jarLog, args.stream().toArray(String[]::new));
 334         } catch (Exception ex) {
 335             if (jarLog.exists()) {
 336                 System.err.println(Files.readString(jarLog.toPath()));
 337             }
 338             throw ex;
 339         }
 340 
 341         if (retVal != 0) {
 342             if (jarLog.exists()) {
 343                 System.err.println(Files.readString(jarLog.toPath()));
 344             }
 345             throw new AssertionError("jar exited with error: " + retVal);
 346         }
 347     }
 348 
 349     public static void createHelloModule() throws Exception {
 350         createModule("Hello.java", "input", "hello");
 351     }
 352 
 353     public static void createOtherModule() throws Exception {
 354         createModule("Other.java", "input-other", "other");
 355     }
 356 
 357     private static void createModule(String javaFile, String inputDir,
 358             String aName) throws Exception {
 359         int retVal;
 360 
 361         File input = new File(inputDir);
 362         if (!input.exists()) {
 363             input.mkdir();
 364         }
 365 
 366         File module = new File("module" + File.separator + "com." + aName);
 367         if (!module.exists()) {
 368             module.mkdirs();
 369         }
 370 
 371         File javacLog = new File("javac.log");
 372         try {
 373             List<String> args = new ArrayList<>();
 374             args.add(JAVAC.toString());
 375             args.add("-d");
 376             args.add("module" + File.separator + "com." + aName);
 377             args.add(TEST_SRC_ROOT + File.separator + "apps" + File.separator
 378                     + "com." + aName + File.separator + "module-info.java");
 379             args.add(TEST_SRC_ROOT + File.separator + "apps"
 380                     + File.separator + "com." + aName + File.separator + "com"
 381                     + File.separator + aName + File.separator + javaFile);
 382             retVal = execute(javacLog, args.stream().toArray(String[]::new));
 383         } catch (Exception ex) {
 384             if (javacLog.exists()) {
 385                 System.err.println(Files.readString(javacLog.toPath()));
 386             }
 387             throw ex;
 388         }
 389 
 390         if (retVal != 0) {
 391             if (javacLog.exists()) {
 392                 System.err.println(Files.readString(javacLog.toPath()));
 393             }
 394             throw new AssertionError("javac exited with error: " + retVal);
 395         }
 396 
 397         File jarLog = new File("jar.log");
 398         try {
 399             List<String> args = new ArrayList<>();
 400             args.add(JAR.toString());
 401             args.add("--create");
 402             args.add("--file");
 403             args.add(inputDir + File.separator + "com." + aName + ".jar");
 404             args.add("-C");
 405             args.add("module" + File.separator + "com." + aName);
 406             args.add(".");
 407 
 408             retVal = execute(jarLog, args.stream().toArray(String[]::new));
 409         } catch (Exception ex) {
 410             if (jarLog.exists()) {
 411                 System.err.println(Files.readString(jarLog.toPath()));
 412             }
 413             throw ex;
 414         }
 415 
 416         if (retVal != 0) {
 417             if (jarLog.exists()) {
 418                 System.err.println(Files.readString(jarLog.toPath()));
 419             }
 420             throw new AssertionError("jar exited with error: " + retVal);
 421         }
 422     }
 423 
 424     public static void createRuntime() throws Exception {
 425         int retVal;
 426 
 427         File jlinkLog = new File("jlink.log");
 428         try {
 429             List<String> args = new ArrayList<>();
 430             args.add(JLINK.toString());
 431             args.add("--output");
 432             args.add("runtime");
 433             args.add("--add-modules");
 434             args.add("java.base");
 435             retVal = execute(jlinkLog, args.stream().toArray(String[]::new));
 436         } catch (Exception ex) {
 437             if (jlinkLog.exists()) {
 438                 System.err.println(Files.readString(jlinkLog.toPath()));
 439             }
 440             throw ex;
 441         }
 442 
 443         if (retVal != 0) {
 444             if (jlinkLog.exists()) {
 445                 System.err.println(Files.readString(jlinkLog.toPath()));
 446             }
 447             throw new AssertionError("jlink exited with error: " + retVal);
 448         }
 449     }
 450 
 451     public static String listToArgumentsMap(List<String> arguments, boolean toolProvider) {
 452         if (arguments.isEmpty()) {
 453             return "";
 454         }
 455 
 456         String argsStr = "";
 457         for (int i = 0; i < arguments.size(); i++) {
 458             String arg = arguments.get(i);
 459             argsStr += quote(arg, toolProvider);
 460             if ((i + 1) != arguments.size()) {
 461                 argsStr += " ";
 462             }
 463         }
 464 
 465         if (!toolProvider && isWindows()) {
 466             if (argsStr.contains(" ")) {
 467                 if (argsStr.contains("\"")) {
 468                     argsStr = escapeQuote(argsStr, toolProvider);
 469                 }
 470                 argsStr = "\"" + argsStr + "\"";
 471             }
 472         }
 473         return argsStr;
 474     }
 475 
 476     private static String quote(String in, boolean toolProvider) {
 477         if (in == null) {
 478             return null;
 479         }
 480 
 481         if (in.isEmpty()) {
 482             return "";
 483         }
 484 
 485         if (!in.contains("=")) {
 486             // Not a property
 487             if (in.contains(" ")) {
 488                 in = escapeQuote(in, toolProvider);
 489                 return "\"" + in + "\"";
 490             }
 491             return in;
 492         }
 493 
 494         if (!in.contains(" ")) {
 495             return in; // No need to quote
 496         }
 497 
 498         int paramIndex = in.indexOf("=");
 499         if (paramIndex <= 0) {
 500             return in; // Something wrong, just skip quoting
 501         }
 502 
 503         String param = in.substring(0, paramIndex);
 504         String value = in.substring(paramIndex + 1);
 505 
 506         if (value.length() == 0) {
 507             return in; // No need to quote
 508         }
 509 
 510         value = escapeQuote(value, toolProvider);
 511 
 512         return param + "=" + "\"" + value + "\"";
 513     }
 514 
 515     private static String escapeQuote(String in, boolean toolProvider) {
 516         if (in == null) {
 517             return null;
 518         }
 519 
 520         if (in.isEmpty()) {
 521             return "";
 522         }
 523 
 524         if (in.contains("\"")) {
 525             // Use code points to preserve non-ASCII chars
 526             StringBuilder sb = new StringBuilder();
 527             int codeLen = in.codePointCount(0, in.length());
 528             for (int i = 0; i < codeLen; i++) {
 529                 int code = in.codePointAt(i);
 530                 // Note: No need to escape '\' on Linux or OS X
 531                 // jpackage expects us to pass arguments and properties with
 532                 // quotes and spaces as a map
 533                 // with quotes being escaped with additional \ for
 534                 // internal quotes.
 535                 // So if we want two properties below:
 536                 // -Djnlp.Prop1=Some "Value" 1
 537                 // -Djnlp.Prop2=Some Value 2
 538                 // jpackage will need:
 539                 // "-Djnlp.Prop1=\"Some \\"Value\\" 1\" -Djnlp.Prop2=\"Some Value 2\""
 540                 // but since we using ProcessBuilder to run jpackage we will need to escape
 541                 // our escape symbols as well, so we will need to pass string below to ProcessBuilder:
 542                 // "-Djnlp.Prop1=\\\"Some \\\\\\\"Value\\\\\\\" 1\\\" -Djnlp.Prop2=\\\"Some Value 2\\\""
 543                 switch (code) {
 544                     case '"':
 545                         // " -> \" -> \\\"
 546                         if (i == 0 || in.codePointAt(i - 1) != '\\') {
 547                             sb.appendCodePoint('\\');
 548                             sb.appendCodePoint(code);
 549                         }
 550                         break;
 551                     case '\\':
 552                         // We need to escape already escaped symbols as well
 553                         if ((i + 1) < codeLen) {
 554                             int nextCode = in.codePointAt(i + 1);
 555                             if (nextCode == '"') {
 556                                 // \" -> \\\"
 557                                 sb.appendCodePoint('\\');
 558                                 sb.appendCodePoint('\\');
 559                                 sb.appendCodePoint('\\');
 560                                 sb.appendCodePoint(nextCode);
 561                             } else {
 562                                 sb.appendCodePoint('\\');
 563                                 sb.appendCodePoint(code);
 564                             }
 565                         } else {
 566                             sb.appendCodePoint(code);
 567                         }
 568                         break;
 569                     default:
 570                         sb.appendCodePoint(code);
 571                         break;
 572                 }
 573             }
 574             return sb.toString();
 575         }
 576 
 577         return in;
 578     }
 579 
 580 }