1 /*
   2  * Copyright (c) 2018, 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 package jnlp.converter;
  25 
  26 import java.io.BufferedWriter;
  27 import java.io.File;
  28 import java.io.FileNotFoundException;
  29 import java.io.FileOutputStream;
  30 import java.io.FileWriter;
  31 import java.io.IOException;
  32 import java.io.InputStream;
  33 import java.io.PrintWriter;
  34 import java.net.HttpURLConnection;
  35 import java.net.URI;
  36 import java.net.URL;
  37 import java.nio.file.Files;
  38 import java.nio.file.Path;
  39 import java.util.ArrayList;
  40 import java.util.Enumeration;
  41 import java.util.List;
  42 import java.util.jar.JarEntry;
  43 import java.util.jar.JarFile;
  44 import jnlp.converter.parser.JNLPDesc;
  45 import jnlp.converter.parser.JNLPDesc.AssociationDesc;
  46 import jnlp.converter.parser.JNLPDesc.IconDesc;
  47 import jnlp.converter.parser.ResourcesDesc.JARDesc;
  48 import jnlp.converter.parser.XMLFormat;
  49 
  50 public class JNLPConverter {
  51 
  52     private final Options options;
  53     private JNLPDesc jnlpd = null;
  54     private final List<String> launchArgs = new ArrayList<>();
  55 
  56     private String downloadFolder = null;
  57     private String jnlpDownloadFolder = null;
  58     private static String jnlpDownloadFolderStatic;
  59     private String jarDownloadFolder = null;
  60     private String iconDownloadFolder = null;
  61     private String propDownloadFolder = null;
  62 
  63     private static String jpackagerPath = null;
  64 
  65     private static boolean markFileToDelete = false;
  66 
  67     private static final String FA_EXTENSIONS = "extension";
  68     private static final String FA_CONTENT_TYPE = "mime-type";
  69     private static final String FA_DESCRIPTION = "description";
  70     private static final String FA_ICON = "icon";
  71 
  72     public JNLPConverter(Options options) {
  73         this.options = options;
  74         jnlpDownloadFolderStatic = getJnlpDownloadFolder();
  75         markFileToDelete = (options.keep() == null);
  76     }
  77 
  78     public String [] getLaunchArgs() {
  79         return launchArgs.toArray(new String[0]);
  80     }
  81 
  82     public void convert() {
  83         try {
  84             loadJNLPDesc();
  85             downloadResources();
  86             validate();
  87             buildLaunchArgs();
  88             saveLaunchArgs();
  89             runJPackager();
  90         } catch (Exception ex) {
  91             Log.error(ex.getLocalizedMessage());
  92         }
  93     }
  94 
  95     private JNLPDesc getJNLPD(String jnlp) throws Exception {
  96         URL codebase = getCodeBase(jnlp);
  97         byte[] bits = HTTPHelper.getJNLPBits(jnlp, jnlp);
  98         return XMLFormat.parse(bits, codebase, jnlp);
  99     }
 100 
 101     private void loadJNLPDesc() throws Exception {
 102         String jnlp = options.getJNLP();
 103         jnlpd = getJNLPD(jnlp);
 104 
 105         // Check for required options in case of FX
 106         if (jnlpd.isFXApp()) {
 107             if (!options.isRuntimeImageSet()) {
 108                 throw new Exception("This is a JavaFX Web-Start application which requires a runtime image capable of running JavaFX applications, which can be specified by the jpackager option --runtime-image (using --jpackager-options).");
 109             }
 110         }
 111 
 112         // Check href. It can be same as URL we provided or new one
 113         // if JNLP has different href or codebase. We assume that
 114         // XMLFormat.parse() will handle any errors in href and codebase
 115         // correctly.
 116         String href = jnlpd.getHref();
 117         if (href != null && !href.equalsIgnoreCase(jnlp)) {
 118             if (href.startsWith("file:")) {
 119                 URI hrefURI = new URI(href);
 120                 URI jnlpURI = new URI(jnlp);
 121 
 122                 String hrefPath = hrefURI.getPath();
 123                 String jnlpPath = jnlpURI.getPath();
 124 
 125                 if (!hrefPath.equalsIgnoreCase(jnlpPath)) {
 126                     jnlp = href;
 127                     jnlpd = getJNLPD(jnlp);
 128                 }
 129             } else {
 130                 jnlp = href;
 131                 jnlpd = getJNLPD(jnlp);
 132             }
 133         }
 134 
 135         if (jnlpd.getName() == null) {
 136             jnlpd.setName(getNameFromURL(jnlp));
 137         }
 138     }
 139 
 140     private static String getNameFromURL(String url) throws IOException {
 141         int index;
 142         int index1 = url.lastIndexOf('/');
 143         int index2 = url.lastIndexOf('\\');
 144 
 145         if (index1 >= index2) {
 146             index = index1;
 147         } else {
 148             index = index2;
 149         }
 150 
 151         if (index != -1) {
 152             String name = url.substring(index + 1, url.length());
 153             if (name.endsWith(".jnlp")) {
 154                 return name.substring(0, name.length() - 5);
 155             }
 156         }
 157 
 158         return null;
 159     }
 160 
 161     private URL getCodeBase(String jnlp) throws Exception {
 162         int index = jnlp.lastIndexOf('/');
 163         if (index != -1) {
 164             if (HTTPHelper.isHTTPUrl(jnlp)) {
 165                 return new URL(jnlp.substring(0, index + 1));
 166             } else {
 167                 String codeBasePath = jnlp.substring(0, index);
 168                 if (!codeBasePath.endsWith("/")) {
 169                     codeBasePath += "/";
 170                 }
 171                 return new URI(codeBasePath).toURL();
 172             }
 173         }
 174 
 175         return null;
 176     }
 177 
 178     public static void markFileToDelete(String file) {
 179         if (file == null || file.isEmpty()) {
 180             return;
 181         }
 182 
 183         if (markFileToDelete) {
 184             try {
 185                 File f = new File(file);
 186                 f.deleteOnExit();
 187             } catch (Exception e) {
 188                 // Print exception, but do not fail conversion.
 189                 Log.warning(e.getLocalizedMessage());
 190             }
 191         }
 192     }
 193 
 194     public static void deleteFile(String file) {
 195         try {
 196             File f = new File(file);
 197             f.delete();
 198         } catch (Exception e) {
 199             Log.warning(e.getLocalizedMessage());
 200         }
 201     }
 202 
 203     private void downloadResources() throws Exception {
 204         List<JARDesc> jars = jnlpd.getResources();
 205         for (JARDesc jar : jars) {
 206             if (jar.getVersion() != null) {
 207                 if (!jnlpd.isVersionEnabled()) {
 208                     throw new Exception("Error: Version based download protocol is not supported without -Djnlp.versionEnabled=true.");
 209                 }
 210             }
 211 
 212             String destFile = null;
 213             if (HTTPHelper.isHTTPUrl(jar.getLocation().toString())) {
 214                 if (jar.getVersion() != null) {
 215                     try {
 216                         destFile = HTTPHelper.downloadFile(jar.getVersionLocation().toString(), getJarDownloadFolder(), HTTPHelper.getFileNameFromURL(jar.getLocation().toString()));
 217                     } catch (HTTPHelperException ex) {
 218                         if (ex.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) {
 219                             System.out.println("Error downloading versioned JAR from " + jar.getVersionLocation());
 220                             System.out.println(ex.getMessage());
 221                             System.out.println("Downloading " + jar.getLocation() + " instead.");
 222                             destFile = HTTPHelper.downloadFile(jar.getLocation().toString(), getJarDownloadFolder(), HTTPHelper.getFileNameFromURL(jar.getLocation().toString()));
 223                         } else {
 224                             throw ex;
 225                         }
 226                     }
 227                 } else {
 228                     destFile = HTTPHelper.downloadFile(jar.getLocation().toString(), getJarDownloadFolder(), HTTPHelper.getFileNameFromURL(jar.getLocation().toString()));
 229                 }
 230                 markFileToDelete(destFile);
 231             } else {
 232                 if (jar.getVersion() != null) {
 233                     try {
 234                         destFile = HTTPHelper.copyFile(jar.getVersionLocation().toString(), getJarDownloadFolder(), HTTPHelper.getFileNameFromURL(jar.getLocation().toString()));
 235                     } catch (FileNotFoundException ex) {
 236                         System.out.println("Error copying versioned JAR from " + jar.getVersionLocation());
 237                         System.out.println(ex.getMessage());
 238                         System.out.println("Copying " + jar.getLocation() + " instead.");
 239                         destFile = HTTPHelper.copyFile(jar.getLocation().toString(), getJarDownloadFolder(), HTTPHelper.getFileNameFromURL(jar.getLocation().toString()));
 240                     }
 241                 } else {
 242                     destFile = HTTPHelper.copyFile(jar.getLocation().toString(), getJarDownloadFolder(), HTTPHelper.getFileNameFromURL(jar.getLocation().toString()));
 243                 }
 244                 markFileToDelete(destFile);
 245             }
 246 
 247             if (jar.isNativeLib()) {
 248                 unpackNativeLib(destFile);
 249                 deleteFile(destFile);
 250             } else {
 251                 jnlpd.addFile(jar.getName());
 252             }
 253         }
 254 
 255         IconDesc icon = jnlpd.getIcon();
 256         if (icon != null) {
 257             String destFile;
 258 
 259             if (HTTPHelper.isHTTPUrl(icon.getLocation())) {
 260                 destFile = HTTPHelper.downloadFile(icon.getLocation(), getIconDownloadFolder(), HTTPHelper.getFileNameFromURL(icon.getLocation()));
 261             } else {
 262                 destFile = HTTPHelper.copyFile(icon.getLocation(), getIconDownloadFolder(), HTTPHelper.getFileNameFromURL(icon.getLocation()));
 263             }
 264 
 265             markFileToDelete(destFile);
 266             icon.setLocalLocation(destFile);
 267         }
 268 
 269         AssociationDesc [] associations = jnlpd.getAssociations();
 270         if (associations != null) {
 271             for (AssociationDesc association : associations) {
 272                 if (association.getIconUrl() != null) {
 273                     String destFile;
 274                     if (HTTPHelper.isHTTPUrl(association.getIconUrl())) {
 275                         destFile = HTTPHelper.downloadFile(association.getIconUrl(), getIconDownloadFolder(), HTTPHelper.getFileNameFromURL(association.getIconUrl()));
 276                     } else {
 277                         destFile = HTTPHelper.copyFile(association.getIconUrl(), getIconDownloadFolder(), HTTPHelper.getFileNameFromURL(association.getIconUrl()));
 278                     }
 279 
 280                     markFileToDelete(destFile);
 281                     association.setIconLocalLocation(destFile);
 282                 }
 283             }
 284         }
 285     }
 286 
 287     public void unpackNativeLib(String file) throws IOException {
 288         try (JarFile jarFile = new JarFile(file)) {
 289             Enumeration entries = jarFile.entries();
 290 
 291             while (entries.hasMoreElements()) {
 292                 JarEntry entry = (JarEntry) entries.nextElement();
 293 
 294                 // Skip directories
 295                 if (entry.isDirectory()) {
 296                     continue;
 297                 }
 298 
 299                 String entryName = entry.getName();
 300                 // Skip anything in sub-directories
 301                 if (entryName.contains("\\") || entryName.contains("/")) {
 302                     continue;
 303                 }
 304 
 305                 // Skip anything not ending with .dll, .dylib or .so
 306                 if (!entryName.endsWith(".dll") && !entryName.endsWith(".dylib") && !entryName.endsWith(".so")) {
 307                     continue;
 308                 }
 309 
 310                 File destFile = new File(getJarDownloadFolder(), entryName);
 311                 if (destFile.exists()) {
 312                     Log.warning(destFile.getAbsolutePath() + " already exist and will not be overwriten by native library from " + file + ".");
 313                     continue;
 314                 }
 315 
 316                 InputStream inputStream = jarFile.getInputStream(entry);
 317                 FileOutputStream outputStream = new FileOutputStream(destFile);
 318 
 319                 byte[] buffer = new byte[HTTPHelper.BUFFER_SIZE];
 320                 int length;
 321                 do {
 322                     length = inputStream.read(buffer);
 323                     if (length > 0) {
 324                         outputStream.write(buffer, 0, length);
 325                     }
 326                 } while (length > 0);
 327 
 328                 jnlpd.addFile(entryName);
 329             }
 330         }
 331     }
 332 
 333     private void validate() {
 334         if (jnlpd.getMainJar() == null) {
 335             Log.error("Cannot find main jar");
 336         }
 337 
 338         if (jnlpd.getMainClass() == null) {
 339             Log.error("Cannot find main class");
 340         }
 341     }
 342 
 343     private void addLaunchArg(String arg, List<String> launchArgs) {
 344         if (arg != null && !arg.isEmpty()) {
 345             if (!options.isOptionPresent(arg)){
 346                 launchArgs.add(arg);
 347             } else {
 348                 Log.info(arg + " generated by JNLPConverter is dropped, since it is overwriten via --jpackager-options");
 349             }
 350         }
 351     }
 352 
 353     private void addLaunchArg(String arg, String value, List<String> launchArgs) {
 354         if (arg != null && !arg.isEmpty() && value != null && !value.isEmpty()) {
 355             if (!options.isOptionPresent(arg)){
 356                 launchArgs.add(arg);
 357                 launchArgs.add(value);
 358             } else {
 359                 Log.info(arg + "=" + value +" generated by JNLPConverter is dropped, since it is overwriten via --jpackager-options");
 360             }
 361         }
 362     }
 363 
 364     private void displayLaunchArgs() {
 365         if (Log.isVerbose()) {
 366             System.out.println();
 367             System.out.println("jpackager launch arguments (each argument starts on new line):");
 368             launchArgs.forEach((arg) -> {
 369                 System.out.println(arg);
 370             });
 371         }
 372     }
 373 
 374     private static int fileAssociationsCount = 0;
 375     private String getFileAssociationsFile() {
 376         String file = getPropDownloadFolder();
 377         file += File.separator;
 378         file += "fileAssociation";
 379         file += String.valueOf(fileAssociationsCount);
 380         file += ".properties";
 381 
 382         fileAssociationsCount++;
 383 
 384         return file;
 385     }
 386 
 387     private void buildLaunchArgs() {
 388         if (options.createImage()) {
 389             addLaunchArg("create-image", launchArgs);
 390         } else if (options.createInstaller()) {
 391             if (options.getInstallerType() == null) {
 392                 addLaunchArg("create-installer", launchArgs);
 393             } else {
 394                 addLaunchArg("create-installer", options.getInstallerType(), launchArgs);
 395             }
 396         }
 397 
 398         // Set verbose for jpackager if it is set for us.
 399         if (options.verbose()) {
 400             addLaunchArg("--verbose", launchArgs);
 401         }
 402 
 403         addLaunchArg("--input", getJarDownloadFolder(), launchArgs);
 404         addLaunchArg("--output", options.getOutput(), launchArgs);
 405         addLaunchArg("--name", jnlpd.getName(), launchArgs);
 406         addLaunchArg("--version", jnlpd.getVersion(), launchArgs);
 407         addLaunchArg("--vendor", jnlpd.getVendor(), launchArgs);
 408         addLaunchArg("--description", jnlpd.getDescription(), launchArgs);
 409         addLaunchArg("--icon", jnlpd.getIconLocation(), launchArgs);
 410         addLaunchArg("--main-jar", jnlpd.getMainJar(), launchArgs);
 411         addLaunchArg("--class", jnlpd.getMainClass(), launchArgs);
 412 
 413         addFiles(launchArgs);
 414         addArguments(launchArgs);
 415         addJVMArgs(launchArgs);
 416 
 417         if (jnlpd.isDesktopHint()) {
 418             if (Platform.isWindows()) {
 419                 addLaunchArg("--win-shortcut", launchArgs);
 420             } else {
 421                 Log.warning("Ignoring shortcut hint, since it is not supported on current platform.");
 422             }
 423         }
 424 
 425         if (jnlpd.isMenuHint()) {
 426             if (Platform.isWindows()) {
 427                 addLaunchArg("--win-menu", launchArgs);
 428                 addLaunchArg("--win-menu-group", jnlpd.getSubMenu(), launchArgs);
 429             } else {
 430                 Log.warning("Ignoring menu hint, since it is not supported on current platform.");
 431             }
 432         }
 433 
 434         AssociationDesc [] associations = jnlpd.getAssociations();
 435         if (associations != null) {
 436             for (AssociationDesc association : associations) {
 437                 String file = getFileAssociationsFile();
 438                 markFileToDelete(file);
 439 
 440                 try (PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(file)))) {
 441                     if (association.getExtensions() != null && association.getMimeType() != null) {
 442                         out.println(FA_EXTENSIONS + "=" + quote(association.getExtensions()));
 443                         out.println(FA_CONTENT_TYPE + "=" + quote(association.getMimeType()));
 444 
 445                         if (association.getMimeDescription() != null) {
 446                             out.println(FA_DESCRIPTION + "=" + association.getMimeDescription());
 447                         }
 448 
 449                         if (association.getIconLocalLocation() != null) {
 450                             out.println(FA_ICON + "=" + quote(association.getIconLocalLocation()));
 451                         }
 452 
 453                         addLaunchArg("--file-associations", file, launchArgs);
 454                     }
 455                 } catch (Exception ex) {
 456                     Log.warning(ex.toString());
 457                     if (association.getExtensions() != null) {
 458                         Log.warning("File assoication for " + association.getExtensions() + " will be ignored due to exception above.");
 459                     }
 460                 }
 461             }
 462         }
 463 
 464         // Add options from --jpackager-options
 465         List<String> jpackagerOptions = options.getJPackagerOptions();
 466         jpackagerOptions.forEach((option) -> {
 467             launchArgs.add(option);
 468         });
 469 
 470         displayLaunchArgs();
 471     }
 472 
 473     private String getCommandFileName() {
 474         Platform platform = Platform.getPlatform();
 475         switch (platform) {
 476             case WINDOWS:
 477                 return "run_jpackager.bat";
 478             case LINUX:
 479                 return "run_jpackager.sh";
 480             case MAC:
 481                 return "run_jpackager.sh";
 482             default:
 483                 Log.error("Cannot determine platform type.");
 484                 return "";
 485         }
 486     }
 487 
 488     private void saveLaunchArgs() {
 489         if (options.keep() != null) {
 490             File keepFolder = new File(options.keep());
 491             String cmdFile = keepFolder.getAbsolutePath() + File.separator + getCommandFileName();
 492             try (PrintWriter out = new PrintWriter(cmdFile)) {
 493                 out.print(getJPackagerPath());
 494                 launchArgs.forEach((arg) -> {
 495                     out.print(" ");
 496 
 497                     if (arg.contains(" ")) {
 498                         int len = arg.length();
 499                         if (len >= 1) {
 500                             if (arg.charAt(0) != '"' && arg.charAt(len - 1) != '"') {
 501                                 out.print("\"" + arg + "\"");
 502                             } else {
 503                                 if (Platform.isWindows()) {
 504                                     out.print(arg);
 505                                 } else {
 506                                     arg = escapeQuote(arg);
 507                                     out.print("\"" + arg + "\"");
 508                                 }
 509                             }
 510                         }
 511                     } else {
 512                         out.print(arg);
 513                     }
 514                 });
 515             } catch (FileNotFoundException ex) {
 516                 Log.error("Cannot save file with command line: " + ex.getLocalizedMessage());
 517             }
 518         }
 519     }
 520 
 521     private void runJPackager() {
 522         List<String> command = new ArrayList<>();
 523         command.add(getJPackagerPath());
 524         command.addAll(launchArgs);
 525 
 526         ProcessBuilder builder = new ProcessBuilder();
 527         builder.inheritIO();
 528         builder.command(command);
 529 
 530         try {
 531             Process process = builder.start();
 532             int exitCode = process.waitFor();
 533             if (exitCode != 0) {
 534                 Log.warning("jpackager retrun non zero code: " + exitCode);
 535             }
 536         } catch (IOException | InterruptedException ex) {
 537             Log.error(ex.getMessage());
 538         }
 539     }
 540 
 541     private void addFileList(String arg, List<String> filesToAdd, List<String> launchArgs) {
 542         if (filesToAdd.isEmpty()) {
 543             return;
 544         }
 545 
 546         String filesArg = "";
 547         for (int i = 0; i < filesToAdd.size(); i++) {
 548             filesArg += quote(filesToAdd.get(i));
 549             if ((i + 1) != filesToAdd.size()) {
 550                 filesArg += File.pathSeparator;
 551             }
 552         }
 553 
 554         launchArgs.add(arg);
 555         launchArgs.add(filesArg);
 556     }
 557 
 558     private void addFiles(List<String> launchArgs) {
 559         addFileList("--files", jnlpd.getFiles(), launchArgs);
 560     }
 561 
 562     private void addArguments(List<String> launchArgs) {
 563         List<String> arguments = jnlpd.getArguments();
 564         if (arguments.isEmpty()) {
 565             return;
 566         }
 567 
 568         String argsStr = "";
 569         for (int i = 0; i < arguments.size(); i++) {
 570             String arg = arguments.get(i);
 571             argsStr += quote(arg);
 572             if ((i + 1) != arguments.size()) {
 573                 argsStr += " ";
 574             }
 575         }
 576 
 577         launchArgs.add("--arguments");
 578         if (Platform.isWindows()) {
 579             if (argsStr.contains(" ")) {
 580                 if (argsStr.contains("\"")) {
 581                     argsStr = escapeQuote(argsStr);
 582                 }
 583                 argsStr = "\"" + argsStr + "\"";
 584             }
 585         }
 586         launchArgs.add(argsStr);
 587     }
 588 
 589     private void addJVMArgs(List<String> launchArgs) {
 590         List<String> jvmArgs = jnlpd.getVMArgs();
 591         if (jvmArgs.isEmpty()) {
 592             return;
 593         }
 594 
 595         String jvmArgsStr = "";
 596         for (int i = 0; i < jvmArgs.size(); i++) {
 597             String arg = jvmArgs.get(i);
 598             jvmArgsStr += quote(arg);
 599             if ((i + 1) != jvmArgs.size()) {
 600                 jvmArgsStr += " ";
 601             }
 602         }
 603 
 604         launchArgs.add("--jvm-args");
 605         if (Platform.isWindows()) {
 606             if (jvmArgsStr.contains(" ")) {
 607                 if (jvmArgsStr.contains("\"")) {
 608                     jvmArgsStr = escapeQuote(jvmArgsStr);
 609                 }
 610                 jvmArgsStr = "\"" + jvmArgsStr + "\"";
 611             }
 612         }
 613         launchArgs.add(jvmArgsStr);
 614     }
 615 
 616     private String quote(String in) {
 617         if (in == null) {
 618             return null;
 619         }
 620 
 621         if (in.isEmpty()) {
 622             return "";
 623         }
 624 
 625         if (!in.contains("=")) {
 626             // Not a property
 627             if (in.contains(" ")) {
 628                 in = escapeQuote(in);
 629                 return "\"" + in + "\"";
 630             }
 631             return in;
 632         }
 633 
 634         if (!in.contains(" ")) {
 635             return in; // No need to quote
 636         }
 637 
 638         int paramIndex = in.indexOf("=");
 639         if (paramIndex <= 0) {
 640             return in; // Something wrong, just skip quoting
 641         }
 642 
 643         String param = in.substring(0, paramIndex);
 644         String value = in.substring(paramIndex + 1);
 645 
 646         if (value.length() == 0) {
 647             return in; // No need to quote
 648         }
 649 
 650         value = escapeQuote(value);
 651 
 652         return param + "=" + "\"" + value + "\"";
 653     }
 654 
 655     private String escapeQuote(String in) {
 656         if (in == null) {
 657             return null;
 658         }
 659 
 660         if (in.isEmpty()) {
 661             return "";
 662         }
 663 
 664         if (in.contains("\"")) {
 665             // Use code points to preserve non-ASCII chars
 666             StringBuilder sb = new StringBuilder();
 667             int codeLen = in.codePointCount(0, in.length());
 668             for (int i = 0; i < codeLen; i++) {
 669                 int code = in.codePointAt(i);
 670                 // Note: No need to escape '\' on Linux or OS X.
 671                 // jpackager expects us to pass arguments and properties with quotes and spaces as a map
 672                 // with quotes being escaped with additional \ for internal quotes.
 673                 // So if we want two properties below:
 674                 // -Djnlp.Prop1=Some "Value" 1
 675                 // -Djnlp.Prop2=Some Value 2
 676                 // jpackager will need:
 677                 // "-Djnlp.Prop1=\"Some \\"Value\\" 1\" -Djnlp.Prop2=\"Some Value 2\""
 678                 // but since we using ProcessBuilder to run jpackager we will need to escape
 679                 // our escape symbols as well, so we will need to pass string below to ProcessBuilder:
 680                 // "-Djnlp.Prop1=\\\"Some \\\\\\\"Value\\\\\\\" 1\\\" -Djnlp.Prop2=\\\"Some Value 2\\\""
 681                 switch (code) {
 682                     case '"':
 683                         // " -> \" -> \\\"
 684                         if (i == 0 || in.codePointAt(i - 1) != '\\') {
 685                             if (Platform.isWindows()) {
 686                                 sb.appendCodePoint('\\');
 687                                 sb.appendCodePoint('\\');
 688                             }
 689                             sb.appendCodePoint('\\');
 690                             sb.appendCodePoint(code);
 691                         }
 692                         break;
 693                     case '\\':
 694                         // We need to escape already escaped symbols as well
 695                         if ((i + 1) < codeLen) {
 696                             int nextCode = in.codePointAt(i + 1);
 697                             if (nextCode == '"') {
 698                                 // \" -> \\\"
 699                                 sb.appendCodePoint('\\');
 700                                 sb.appendCodePoint('\\');
 701                                 sb.appendCodePoint('\\');
 702                                 sb.appendCodePoint(nextCode);
 703                             } else {
 704                                 sb.appendCodePoint('\\');
 705                                 sb.appendCodePoint(code);
 706                             }
 707                         } else {
 708                             if (Platform.isWindows()) {
 709                                 sb.appendCodePoint('\\');
 710                             }
 711                             sb.appendCodePoint(code);
 712                         }
 713                         break;
 714                     default:
 715                         sb.appendCodePoint(code);
 716                         break;
 717                 }
 718             }
 719             return sb.toString();
 720         }
 721 
 722         return in;
 723     }
 724 
 725     public synchronized String getDownloadFolder() {
 726         if (downloadFolder == null) {
 727             try {
 728                 File file;
 729                 if (options.keep() == null) {
 730                     Path path = Files.createTempDirectory("JNLPConverter");
 731                     file = path.toFile();
 732                     file.deleteOnExit();
 733                 } else {
 734                     file = new File(options.keep());
 735                     if (!file.exists()) {
 736                         file.mkdir();
 737                     }
 738                 }
 739 
 740                 downloadFolder = file.getAbsolutePath();
 741             } catch (IOException e) {
 742                 Log.error(e.getLocalizedMessage());
 743             }
 744         }
 745 
 746         return downloadFolder;
 747     }
 748 
 749     public final synchronized String getJnlpDownloadFolder() {
 750         if (jnlpDownloadFolder == null) {
 751             File file = new File(getDownloadFolder() + File.separator + "jnlp");
 752             file.mkdir();
 753             markFileToDelete(getDownloadFolder() + File.separator + "jnlp");
 754             jnlpDownloadFolder = file.getAbsolutePath();
 755         }
 756 
 757         return jnlpDownloadFolder;
 758     }
 759 
 760     public static String getJnlpDownloadFolderStatic() {
 761         return jnlpDownloadFolderStatic;
 762     }
 763 
 764     public synchronized String getJarDownloadFolder() {
 765         if (jarDownloadFolder == null) {
 766             File file = new File(getDownloadFolder() + File.separator + "jar");
 767             file.mkdir();
 768             markFileToDelete(getDownloadFolder() + File.separator + "jar");
 769             jarDownloadFolder = file.getAbsolutePath();
 770         }
 771 
 772         return jarDownloadFolder;
 773     }
 774 
 775     public synchronized String getIconDownloadFolder() {
 776         if (iconDownloadFolder == null) {
 777             File file = new File(getDownloadFolder() + File.separator + "icon");
 778             file.mkdir();
 779             markFileToDelete(getDownloadFolder() + File.separator + "icon");
 780             iconDownloadFolder = file.getAbsolutePath();
 781         }
 782 
 783         return iconDownloadFolder;
 784     }
 785 
 786     public synchronized String getPropDownloadFolder() {
 787         if (propDownloadFolder == null) {
 788             File file = new File(getDownloadFolder() + File.separator + "prop");
 789             file.mkdir();
 790             markFileToDelete(getDownloadFolder() + File.separator + "prop");
 791             propDownloadFolder = file.getAbsolutePath();
 792         }
 793 
 794         return propDownloadFolder;
 795     }
 796 
 797     public synchronized static String getJPackagerPath() {
 798         if (jpackagerPath == null) {
 799             jpackagerPath = System.getProperty("java.home");
 800             jpackagerPath += File.separator;
 801             jpackagerPath += "bin";
 802             jpackagerPath += File.separator;
 803 
 804             Platform platform = Platform.getPlatform();
 805             switch (platform) {
 806                 case WINDOWS:
 807                     jpackagerPath += "jpackager.exe";
 808                     break;
 809                 case LINUX:
 810                     jpackagerPath += "jpackager";
 811                     break;
 812                 case MAC:
 813                     jpackagerPath += "jpackager";
 814                     break;
 815                 default:
 816                     Log.error("Cannot determine platform type.");
 817                     break;
 818             }
 819 
 820             Log.verbose("jpackager: " + jpackagerPath);
 821         }
 822 
 823         return jpackagerPath;
 824     }
 825 
 826     public static String getIconFormat(String icon) {
 827         // GIF, JPEG, ICO, or PNG
 828         if (icon.toLowerCase().endsWith(".gif")) {
 829             return "GIF";
 830         } else if (icon.toLowerCase().endsWith(".jpg")) {
 831             return "JPEG";
 832         } else if (icon.toLowerCase().endsWith(".ico")) {
 833             return "ICO";
 834         } else if (icon.toLowerCase().endsWith(".png")) {
 835             return "PNG";
 836         }
 837 
 838         return "UNKNOWN";
 839     }
 840 
 841     public static boolean isIconSupported(String icon) {
 842         Platform platform = Platform.getPlatform();
 843         switch (platform) {
 844             case WINDOWS:
 845                 if (icon.endsWith(".ico")) {
 846                     return true;
 847                 } else {
 848                     Log.warning("Icon file format (" + getIconFormat(icon) + ") is not supported on Windows for file " + icon + ".");
 849                     return false;
 850                 }
 851             case LINUX:
 852                 if (icon.endsWith(".png")) {
 853                     return true;
 854                 } else {
 855                     Log.warning("Icon file format (" + getIconFormat(icon) + ") is not supported on Linux for file " + icon + ".");
 856                     return false;
 857                 }
 858             case MAC:
 859                 Log.warning("Icon file format (" + getIconFormat(icon) + ") is not supported on OS X for file " + icon + ".");
 860                 return false;
 861         }
 862 
 863         return false;
 864     }
 865 }