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");
 351     }
 352 
 353     private static void createModule(String javaFile)
 354             throws Exception {
 355         int retVal;
 356 
 357         File input = new File("input");
 358         if (!input.exists()) {
 359             input.mkdir();
 360         }
 361 
 362         File module = new File("module" + File.separator + "com.hello");
 363         if (!module.exists()) {
 364             module.mkdirs();
 365         }
 366 
 367         File javacLog = new File("javac.log");
 368         try {
 369             List<String> args = new ArrayList<>();
 370             args.add(JAVAC.toString());
 371             args.add("-d");
 372             args.add("module" + File.separator + "com.hello");
 373             args.add(TEST_SRC_ROOT + File.separator + "apps" + File.separator + "com.hello"
 374                     + File.separator + "module-info.java");
 375             args.add(TEST_SRC_ROOT + File.separator + "apps" + File.separator + "com.hello"
 376                     + File.separator + "com" + File.separator + "hello" + File.separator
 377                     + javaFile);
 378             retVal = execute(javacLog, args.stream().toArray(String[]::new));
 379         } catch (Exception ex) {
 380             if (javacLog.exists()) {
 381                 System.err.println(Files.readString(javacLog.toPath()));
 382             }
 383             throw ex;
 384         }
 385 
 386         if (retVal != 0) {
 387             if (javacLog.exists()) {
 388                 System.err.println(Files.readString(javacLog.toPath()));
 389             }
 390             throw new AssertionError("javac exited with error: " + retVal);
 391         }
 392 
 393         File jarLog = new File("jar.log");
 394         try {
 395             List<String> args = new ArrayList<>();
 396             args.add(JAR.toString());
 397             args.add("--create");
 398             args.add("--file");
 399             args.add("input" + File.separator + "com.hello.jar");
 400             args.add("-C");
 401             args.add("module" + File.separator + "com.hello");
 402             args.add(".");
 403 
 404             retVal = execute(jarLog, args.stream().toArray(String[]::new));
 405         } catch (Exception ex) {
 406             if (jarLog.exists()) {
 407                 System.err.println(Files.readString(jarLog.toPath()));
 408             }
 409             throw ex;
 410         }
 411 
 412         if (retVal != 0) {
 413             if (jarLog.exists()) {
 414                 System.err.println(Files.readString(jarLog.toPath()));
 415             }
 416             throw new AssertionError("jar exited with error: " + retVal);
 417         }
 418     }
 419 
 420     public static void createRuntime() throws Exception {
 421         int retVal;
 422 
 423         File jlinkLog = new File("jlink.log");
 424         try {
 425             List<String> args = new ArrayList<>();
 426             args.add(JLINK.toString());
 427             args.add("--output");
 428             args.add("runtime");
 429             args.add("--add-modules");
 430             args.add("java.base");
 431             retVal = execute(jlinkLog, args.stream().toArray(String[]::new));
 432         } catch (Exception ex) {
 433             if (jlinkLog.exists()) {
 434                 System.err.println(Files.readString(jlinkLog.toPath()));
 435             }
 436             throw ex;
 437         }
 438 
 439         if (retVal != 0) {
 440             if (jlinkLog.exists()) {
 441                 System.err.println(Files.readString(jlinkLog.toPath()));
 442             }
 443             throw new AssertionError("jlink exited with error: " + retVal);
 444         }
 445     }
 446 
 447     public static String listToArgumentsMap(List<String> arguments, boolean toolProvider) {
 448         if (arguments.isEmpty()) {
 449             return "";
 450         }
 451 
 452         String argsStr = "";
 453         for (int i = 0; i < arguments.size(); i++) {
 454             String arg = arguments.get(i);
 455             argsStr += quote(arg, toolProvider);
 456             if ((i + 1) != arguments.size()) {
 457                 argsStr += " ";
 458             }
 459         }
 460 
 461         if (!toolProvider && isWindows()) {
 462             if (argsStr.contains(" ")) {
 463                 if (argsStr.contains("\"")) {
 464                     argsStr = escapeQuote(argsStr, toolProvider);
 465                 }
 466                 argsStr = "\"" + argsStr + "\"";
 467             }
 468         }
 469         return argsStr;
 470     }
 471 
 472     private static String quote(String in, boolean toolProvider) {
 473         if (in == null) {
 474             return null;
 475         }
 476 
 477         if (in.isEmpty()) {
 478             return "";
 479         }
 480 
 481         if (!in.contains("=")) {
 482             // Not a property
 483             if (in.contains(" ")) {
 484                 in = escapeQuote(in, toolProvider);
 485                 return "\"" + in + "\"";
 486             }
 487             return in;
 488         }
 489 
 490         if (!in.contains(" ")) {
 491             return in; // No need to quote
 492         }
 493 
 494         int paramIndex = in.indexOf("=");
 495         if (paramIndex <= 0) {
 496             return in; // Something wrong, just skip quoting
 497         }
 498 
 499         String param = in.substring(0, paramIndex);
 500         String value = in.substring(paramIndex + 1);
 501 
 502         if (value.length() == 0) {
 503             return in; // No need to quote
 504         }
 505 
 506         value = escapeQuote(value, toolProvider);
 507 
 508         return param + "=" + "\"" + value + "\"";
 509     }
 510 
 511     private static String escapeQuote(String in, boolean toolProvider) {
 512         if (in == null) {
 513             return null;
 514         }
 515 
 516         if (in.isEmpty()) {
 517             return "";
 518         }
 519 
 520         if (in.contains("\"")) {
 521             // Use code points to preserve non-ASCII chars
 522             StringBuilder sb = new StringBuilder();
 523             int codeLen = in.codePointCount(0, in.length());
 524             for (int i = 0; i < codeLen; i++) {
 525                 int code = in.codePointAt(i);
 526                 // Note: No need to escape '\' on Linux or OS X
 527                 // jpackage expects us to pass arguments and properties with
 528                 // quotes and spaces as a map
 529                 // with quotes being escaped with additional \ for
 530                 // internal quotes.
 531                 // So if we want two properties below:
 532                 // -Djnlp.Prop1=Some "Value" 1
 533                 // -Djnlp.Prop2=Some Value 2
 534                 // jpackage will need:
 535                 // "-Djnlp.Prop1=\"Some \\"Value\\" 1\" -Djnlp.Prop2=\"Some Value 2\""
 536                 // but since we using ProcessBuilder to run jpackage we will need to escape
 537                 // our escape symbols as well, so we will need to pass string below to ProcessBuilder:
 538                 // "-Djnlp.Prop1=\\\"Some \\\\\\\"Value\\\\\\\" 1\\\" -Djnlp.Prop2=\\\"Some Value 2\\\""
 539                 switch (code) {
 540                     case '"':
 541                         // " -> \" -> \\\"
 542                         if (i == 0 || in.codePointAt(i - 1) != '\\') {
 543                             sb.appendCodePoint('\\');
 544                             sb.appendCodePoint(code);
 545                         }
 546                         break;
 547                     case '\\':
 548                         // We need to escape already escaped symbols as well
 549                         if ((i + 1) < codeLen) {
 550                             int nextCode = in.codePointAt(i + 1);
 551                             if (nextCode == '"') {
 552                                 // \" -> \\\"
 553                                 sb.appendCodePoint('\\');
 554                                 sb.appendCodePoint('\\');
 555                                 sb.appendCodePoint('\\');
 556                                 sb.appendCodePoint(nextCode);
 557                             } else {
 558                                 sb.appendCodePoint('\\');
 559                                 sb.appendCodePoint(code);
 560                             }
 561                         } else {
 562                             sb.appendCodePoint(code);
 563                         }
 564                         break;
 565                     default:
 566                         sb.appendCodePoint(code);
 567                         break;
 568                 }
 569             }
 570             return sb.toString();
 571         }
 572 
 573         return in;
 574     }
 575 
 576 }