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