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 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 jpackagePath = 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             runJPackage();
  90         } catch (Exception ex) {
  91             Log.error(ex.getMessage());
  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 jpackage option --runtime-image (using --jpackage-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 --jpackage-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 --jpackage-options");
 360             }
 361         }
 362     }
 363 
 364     private void displayLaunchArgs() {
 365         if (Log.isVerbose()) {
 366             System.out.println();
 367             System.out.println("jpackage 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.createAppImage()) {
 389             addLaunchArg("create-app-image", launchArgs);
 390         } else if (options.createInstaller()) {
 391             if (options.getInstallerType() == null) {
 392                 addLaunchArg("create-installer", launchArgs);
 393             } else {
 394                 addLaunchArg("create-installer", launchArgs);
 395                 if (options.getInstallerType() != null) {
 396                     addLaunchArg("--installer-type", options.getInstallerType(), launchArgs);
 397                 }
 398             }
 399         }
 400 
 401         // Set verbose for jpackage if it is set for us.
 402         if (options.verbose()) {
 403             addLaunchArg("--verbose", launchArgs);
 404         }
 405 
 406         addLaunchArg("--input", getJarDownloadFolder(), launchArgs);
 407         addLaunchArg("--output", options.getOutput(), launchArgs);
 408         addLaunchArg("--name", jnlpd.getName(), launchArgs);
 409         addLaunchArg("--app-version", jnlpd.getVersion(), launchArgs);
 410         addLaunchArg("--vendor", jnlpd.getVendor(), launchArgs);
 411         addLaunchArg("--description", jnlpd.getDescription(), launchArgs);
 412         addLaunchArg("--icon", jnlpd.getIconLocation(), launchArgs);
 413         addLaunchArg("--main-jar", jnlpd.getMainJar(), launchArgs);
 414         addLaunchArg("--main-class", jnlpd.getMainClass(), launchArgs);
 415 
 416         addArguments(launchArgs);
 417         addJVMArgs(launchArgs);
 418 
 419         if (options.createInstaller()) {
 420             if (jnlpd.isDesktopHint()) {
 421                 if (Platform.isWindows()) {
 422                     addLaunchArg("--win-shortcut", launchArgs);
 423                 } else {
 424                     Log.warning("Ignoring shortcut hint, since it is not supported on current platform.");
 425                 }
 426             }
 427 
 428             if (jnlpd.isMenuHint()) {
 429                 if (Platform.isWindows()) {
 430                     addLaunchArg("--win-menu", launchArgs);
 431                     addLaunchArg("--win-menu-group", jnlpd.getSubMenu(), launchArgs);
 432                 } else {
 433                     Log.warning("Ignoring menu hint, since it is not supported on current platform.");
 434                 }
 435             }
 436 
 437             AssociationDesc[] associations = jnlpd.getAssociations();
 438             if (associations != null) {
 439                 for (AssociationDesc association : associations) {
 440                     String file = getFileAssociationsFile();
 441                     markFileToDelete(file);
 442 
 443                     try (PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(file)))) {
 444                         if (association.getExtensions() != null && association.getMimeType() != null) {
 445                             out.println(FA_EXTENSIONS + "=" + quote(association.getExtensions()));
 446                             out.println(FA_CONTENT_TYPE + "=" + quote(association.getMimeType()));
 447 
 448                             if (association.getMimeDescription() != null) {
 449                                 out.println(FA_DESCRIPTION + "=" + association.getMimeDescription());
 450                             }
 451 
 452                             if (association.getIconLocalLocation() != null) {
 453                                 out.println(FA_ICON + "=" + quote(association.getIconLocalLocation()));
 454                             }
 455 
 456                             addLaunchArg("--file-associations", file, launchArgs);
 457                         }
 458                     } catch (Exception ex) {
 459                         Log.warning(ex.toString());
 460                         if (association.getExtensions() != null) {
 461                             Log.warning("File assoication for " + association.getExtensions() + " will be ignored due to exception above.");
 462                         }
 463                     }
 464                 }
 465             }
 466         }
 467 
 468         // Add options from --jpackage-options
 469         List<String> jpackageOptions = options.getJPackageOptions();
 470         jpackageOptions.forEach((option) -> {
 471             launchArgs.add(option);
 472         });
 473 
 474         displayLaunchArgs();
 475     }
 476 
 477     private String getCommandFileName() {
 478         Platform platform = Platform.getPlatform();
 479         switch (platform) {
 480             case WINDOWS:
 481                 return "run_jpackage.bat";
 482             case LINUX:
 483                 return "run_jpackage.sh";
 484             case MAC:
 485                 return "run_jpackage.sh";
 486             default:
 487                 Log.error("Cannot determine platform type.");
 488                 return "";
 489         }
 490     }
 491 
 492     private void saveLaunchArgs() {
 493         if (options.keep() != null) {
 494             File keepFolder = new File(options.keep());
 495             String cmdFile = keepFolder.getAbsolutePath() + File.separator + getCommandFileName();
 496             try (PrintWriter out = new PrintWriter(cmdFile)) {
 497                 out.print(getJPackagePath());
 498                 launchArgs.forEach((arg) -> {
 499                     out.print(" ");
 500 
 501                     if (arg.contains(" ")) {
 502                         int len = arg.length();
 503                         if (len >= 1) {
 504                             if (arg.charAt(0) != '"' && arg.charAt(len - 1) != '"') {
 505                                 out.print("\"" + arg + "\"");
 506                             } else {
 507                                 if (Platform.isWindows()) {
 508                                     out.print(arg);
 509                                 } else {
 510                                     arg = escapeQuote(arg);
 511                                     out.print("\"" + arg + "\"");
 512                                 }
 513                             }
 514                         }
 515                     } else {
 516                         out.print(arg);
 517                     }
 518                 });
 519             } catch (FileNotFoundException ex) {
 520                 Log.error("Cannot save file with command line: " + ex.getLocalizedMessage());
 521             }
 522         }
 523     }
 524 
 525     private void runJPackage() {
 526         List<String> command = new ArrayList<>();
 527         command.add(getJPackagePath());
 528         command.addAll(launchArgs);
 529 
 530         ProcessBuilder builder = new ProcessBuilder();
 531         builder.inheritIO();
 532         builder.command(command);
 533 
 534         try {
 535             Process process = builder.start();
 536             int exitCode = process.waitFor();
 537             if (exitCode != 0) {
 538                 Log.warning("jpackage retrun non zero code: " + exitCode);
 539             }
 540         } catch (IOException | InterruptedException ex) {
 541             Log.error(ex.getMessage());
 542         }
 543     }
 544 
 545     private void addArguments(List<String> launchArgs) {
 546         List<String> arguments = jnlpd.getArguments();
 547         if (arguments.isEmpty()) {
 548             return;
 549         }
 550 
 551         String argsStr = "";
 552         for (int i = 0; i < arguments.size(); i++) {
 553             String arg = arguments.get(i);
 554             argsStr += quote(arg);
 555             if ((i + 1) != arguments.size()) {
 556                 argsStr += " ";
 557             }
 558         }
 559 
 560         launchArgs.add("--arguments");
 561         if (Platform.isWindows()) {
 562             if (argsStr.contains(" ")) {
 563                 if (argsStr.contains("\"")) {
 564                     argsStr = escapeQuote(argsStr);
 565                 }
 566                 argsStr = "\"" + argsStr + "\"";
 567             }
 568         }
 569         launchArgs.add(argsStr);
 570     }
 571 
 572     private void addJVMArgs(List<String> launchArgs) {
 573         List<String> jvmArgs = jnlpd.getVMArgs();
 574         if (jvmArgs.isEmpty()) {
 575             return;
 576         }
 577 
 578         String jvmArgsStr = "";
 579         for (int i = 0; i < jvmArgs.size(); i++) {
 580             String arg = jvmArgs.get(i);
 581             jvmArgsStr += quote(arg);
 582             if ((i + 1) != jvmArgs.size()) {
 583                 jvmArgsStr += " ";
 584             }
 585         }
 586 
 587         launchArgs.add("--java-options");
 588         if (Platform.isWindows()) {
 589             if (jvmArgsStr.contains(" ")) {
 590                 if (jvmArgsStr.contains("\"")) {
 591                     jvmArgsStr = escapeQuote(jvmArgsStr);
 592                 }
 593                 jvmArgsStr = "\"" + jvmArgsStr + "\"";
 594             }
 595         }
 596         launchArgs.add(jvmArgsStr);
 597     }
 598 
 599     private String quote(String in) {
 600         if (in == null) {
 601             return null;
 602         }
 603 
 604         if (in.isEmpty()) {
 605             return "";
 606         }
 607 
 608         if (!in.contains("=")) {
 609             // Not a property
 610             if (in.contains(" ")) {
 611                 in = escapeQuote(in);
 612                 return "\"" + in + "\"";
 613             }
 614             return in;
 615         }
 616 
 617         if (!in.contains(" ")) {
 618             return in; // No need to quote
 619         }
 620 
 621         int paramIndex = in.indexOf("=");
 622         if (paramIndex <= 0) {
 623             return in; // Something wrong, just skip quoting
 624         }
 625 
 626         String param = in.substring(0, paramIndex);
 627         String value = in.substring(paramIndex + 1);
 628 
 629         if (value.length() == 0) {
 630             return in; // No need to quote
 631         }
 632 
 633         value = escapeQuote(value);
 634 
 635         return param + "=" + "\"" + value + "\"";
 636     }
 637 
 638     private String escapeQuote(String in) {
 639         if (in == null) {
 640             return null;
 641         }
 642 
 643         if (in.isEmpty()) {
 644             return "";
 645         }
 646 
 647         if (in.contains("\"")) {
 648             // Use code points to preserve non-ASCII chars
 649             StringBuilder sb = new StringBuilder();
 650             int codeLen = in.codePointCount(0, in.length());
 651             for (int i = 0; i < codeLen; i++) {
 652                 int code = in.codePointAt(i);
 653                 // Note: No need to escape '\' on Linux or OS X.
 654                 // jpackage expects us to pass arguments and properties with quotes and spaces as a map
 655                 // with quotes being escaped with additional \ for internal quotes.
 656                 // So if we want two properties below:
 657                 // -Djnlp.Prop1=Some "Value" 1
 658                 // -Djnlp.Prop2=Some Value 2
 659                 // jpackage will need:
 660                 // "-Djnlp.Prop1=\"Some \\"Value\\" 1\" -Djnlp.Prop2=\"Some Value 2\""
 661                 // but since we using ProcessBuilder to run jpackage we will need to escape
 662                 // our escape symbols as well, so we will need to pass string below to ProcessBuilder:
 663                 // "-Djnlp.Prop1=\\\"Some \\\\\\\"Value\\\\\\\" 1\\\" -Djnlp.Prop2=\\\"Some Value 2\\\""
 664                 switch (code) {
 665                     case '"':
 666                         // " -> \" -> \\\"
 667                         if (i == 0 || in.codePointAt(i - 1) != '\\') {
 668                                 sb.appendCodePoint('\\');
 669                             sb.appendCodePoint(code);
 670                         }
 671                         break;
 672                     case '\\':
 673                         // We need to escape already escaped symbols as well
 674                         if ((i + 1) < codeLen) {
 675                             int nextCode = in.codePointAt(i + 1);
 676                             if (nextCode == '"') {
 677                                 // \" -> \\\"
 678                                 sb.appendCodePoint('\\');
 679                                 sb.appendCodePoint('\\');
 680                                 sb.appendCodePoint('\\');
 681                                 sb.appendCodePoint(nextCode);
 682                             } else {
 683                                 sb.appendCodePoint('\\');
 684                                 sb.appendCodePoint(code);
 685                             }
 686                         } else {
 687                             sb.appendCodePoint(code);
 688                         }
 689                         break;
 690                     default:
 691                         sb.appendCodePoint(code);
 692                         break;
 693                 }
 694             }
 695             return sb.toString();
 696         }
 697 
 698         return in;
 699     }
 700 
 701     public synchronized String getDownloadFolder() {
 702         if (downloadFolder == null) {
 703             try {
 704                 File file;
 705                 if (options.keep() == null) {
 706                     Path path = Files.createTempDirectory("JNLPConverter");
 707                     file = path.toFile();
 708                     file.deleteOnExit();
 709                 } else {
 710                     file = new File(options.keep());
 711                     if (!file.exists()) {
 712                         file.mkdir();
 713                     }
 714                 }
 715 
 716                 downloadFolder = file.getAbsolutePath();
 717             } catch (IOException e) {
 718                 Log.error(e.getMessage());
 719             }
 720         }
 721 
 722         return downloadFolder;
 723     }
 724 
 725     public final synchronized String getJnlpDownloadFolder() {
 726         if (jnlpDownloadFolder == null) {
 727             File file = new File(getDownloadFolder() + File.separator + "jnlp");
 728             file.mkdir();
 729             markFileToDelete(getDownloadFolder() + File.separator + "jnlp");
 730             jnlpDownloadFolder = file.getAbsolutePath();
 731         }
 732 
 733         return jnlpDownloadFolder;
 734     }
 735 
 736     public static String getJnlpDownloadFolderStatic() {
 737         return jnlpDownloadFolderStatic;
 738     }
 739 
 740     public synchronized String getJarDownloadFolder() {
 741         if (jarDownloadFolder == null) {
 742             File file = new File(getDownloadFolder() + File.separator + "jar");
 743             file.mkdir();
 744             markFileToDelete(getDownloadFolder() + File.separator + "jar");
 745             jarDownloadFolder = file.getAbsolutePath();
 746         }
 747 
 748         return jarDownloadFolder;
 749     }
 750 
 751     public synchronized String getIconDownloadFolder() {
 752         if (iconDownloadFolder == null) {
 753             File file = new File(getDownloadFolder() + File.separator + "icon");
 754             file.mkdir();
 755             markFileToDelete(getDownloadFolder() + File.separator + "icon");
 756             iconDownloadFolder = file.getAbsolutePath();
 757         }
 758 
 759         return iconDownloadFolder;
 760     }
 761 
 762     public synchronized String getPropDownloadFolder() {
 763         if (propDownloadFolder == null) {
 764             File file = new File(getDownloadFolder() + File.separator + "prop");
 765             file.mkdir();
 766             markFileToDelete(getDownloadFolder() + File.separator + "prop");
 767             propDownloadFolder = file.getAbsolutePath();
 768         }
 769 
 770         return propDownloadFolder;
 771     }
 772 
 773     public synchronized static String getJPackagePath() {
 774         if (jpackagePath == null) {
 775             jpackagePath = System.getProperty("java.home");
 776             jpackagePath += File.separator;
 777             jpackagePath += "bin";
 778             jpackagePath += File.separator;
 779 
 780             Platform platform = Platform.getPlatform();
 781             switch (platform) {
 782                 case WINDOWS:
 783                     jpackagePath += "jpackage.exe";
 784                     break;
 785                 case LINUX:
 786                     jpackagePath += "jpackage";
 787                     break;
 788                 case MAC:
 789                     jpackagePath += "jpackage";
 790                     break;
 791                 default:
 792                     Log.error("Cannot determine platform type.");
 793                     break;
 794             }
 795 
 796             Log.verbose("jpackage: " + jpackagePath);
 797         }
 798 
 799         return jpackagePath;
 800     }
 801 
 802     public static String getIconFormat(String icon) {
 803         // GIF, JPEG, ICO, or PNG
 804         if (icon.toLowerCase().endsWith(".gif")) {
 805             return "GIF";
 806         } else if (icon.toLowerCase().endsWith(".jpg")) {
 807             return "JPEG";
 808         } else if (icon.toLowerCase().endsWith(".ico")) {
 809             return "ICO";
 810         } else if (icon.toLowerCase().endsWith(".png")) {
 811             return "PNG";
 812         }
 813 
 814         return "UNKNOWN";
 815     }
 816 
 817     public static boolean isIconSupported(String icon) {
 818         Platform platform = Platform.getPlatform();
 819         switch (platform) {
 820             case WINDOWS:
 821                 if (icon.endsWith(".ico")) {
 822                     return true;
 823                 } else {
 824                     Log.warning("Icon file format (" + getIconFormat(icon) + ") is not supported on Windows for file " + icon + ".");
 825                     return false;
 826                 }
 827             case LINUX:
 828                 if (icon.endsWith(".png")) {
 829                     return true;
 830                 } else {
 831                     Log.warning("Icon file format (" + getIconFormat(icon) + ") is not supported on Linux for file " + icon + ".");
 832                     return false;
 833                 }
 834             case MAC:
 835                 Log.warning("Icon file format (" + getIconFormat(icon) + ") is not supported on OS X for file " + icon + ".");
 836                 return false;
 837         }
 838 
 839         return false;
 840     }
 841 }