1 /*
   2  * Copyright (c) 2012, 2017, 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.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package com.sun.javafx.tools.packager;
  27 
  28 import com.oracle.tools.packager.Bundlers;
  29 import com.oracle.tools.packager.ConfigException;
  30 import com.oracle.tools.packager.Log;
  31 import com.oracle.tools.packager.Platform;
  32 import com.oracle.tools.packager.UnsupportedPlatformException;
  33 import com.sun.javafx.tools.packager.JarSignature.InputStreamSource;
  34 import com.sun.javafx.tools.packager.bundlers.BundleParams;
  35 import com.sun.javafx.tools.packager.bundlers.Bundler.BundleType;
  36 import com.sun.javafx.tools.resource.PackagerResource;
  37 
  38 import java.io.BufferedReader;
  39 import java.io.File;
  40 import java.io.FileInputStream;
  41 import java.io.FileNotFoundException;
  42 import java.io.FileOutputStream;
  43 import java.io.FileWriter;
  44 import java.io.IOException;
  45 import java.io.InputStream;
  46 import java.io.InputStreamReader;
  47 import java.io.OutputStream;
  48 import java.io.Writer;
  49 import java.lang.reflect.Method;
  50 import java.net.MalformedURLException;
  51 import java.net.URL;
  52 import java.net.URLClassLoader;
  53 import java.nio.file.Files;
  54 import java.nio.file.StandardCopyOption;
  55 import java.security.InvalidKeyException;
  56 import java.security.KeyStore;
  57 import java.security.KeyStoreException;
  58 import java.security.NoSuchAlgorithmException;
  59 import java.security.PrivateKey;
  60 import java.security.SignatureException;
  61 import java.security.UnrecoverableKeyException;
  62 import java.security.cert.Certificate;
  63 import java.security.cert.CertificateException;
  64 import java.security.cert.X509Certificate;
  65 import java.text.MessageFormat;
  66 import java.util.ArrayList;
  67 import java.util.Base64;
  68 import java.util.Enumeration;
  69 import java.util.HashMap;
  70 import java.util.HashSet;
  71 import java.util.List;
  72 import java.util.Map;
  73 import java.util.Map.Entry;
  74 import java.util.ResourceBundle;
  75 import java.util.Set;
  76 import java.util.jar.Attributes;
  77 import java.util.jar.JarEntry;
  78 import java.util.jar.JarFile;
  79 import java.util.jar.JarOutputStream;
  80 import java.util.jar.Manifest;
  81 import java.util.zip.ZipEntry;
  82 import java.util.zip.ZipOutputStream;
  83 
  84 public class PackagerLib {
  85     public static final String JAVAFX_VERSION = System.getProperty("java.version");
  86 
  87     private static final ResourceBundle bundle =
  88             ResourceBundle.getBundle("com/sun/javafx/tools/packager/Bundle");
  89 
  90     private CreateJarParams createJarParams;
  91     private CreateBSSParams createBssParams;
  92     private File bssTmpDir;
  93 
  94 
  95     private enum Filter {ALL, CLASSES_ONLY, RESOURCES}
  96 
  97     private ClassLoader classLoader;
  98 
  99     private ClassLoader getClassLoader() throws PackagerException {
 100         if (classLoader == null) {
 101             try {
 102                 URL[] urls = {new URL(getJfxrtPath())};
 103                 classLoader = URLClassLoader.newInstance(urls);
 104             } catch (MalformedURLException ex) {
 105                 throw new PackagerException(ex, "ERR_CantFindRuntime");
 106             }
 107         }
 108         return classLoader;
 109     }
 110 
 111     //  if set of input resources consist of SINGLE element and
 112     //   this element is jar file then we expect this to be request to
 113     //   "update" jar file
 114     //  Input jar file MUST be executable jar file
 115     //
 116     // Check if we are in "special case" scenario
 117     private File jarFileToUpdate(CreateJarParams params) {
 118         if (params.resources.size() == 1) {
 119             PackagerResource p = params.resources.get(0);
 120             File f = p.getFile();
 121             if (!f.isFile() || !f.getAbsolutePath().toLowerCase().endsWith(".jar")) {
 122                 return null;
 123             }
 124             try (JarFile jf = new JarFile(f)) {
 125                 jf.getManifest(); //try to read manifest to validate it is jar
 126                 return f;
 127             } catch (Exception e) {
 128                 //treat any exception as "not a special case" scenario
 129                 com.oracle.tools.packager.Log.verbose(e);
 130             }
 131         }
 132         return null;
 133     }
 134 
 135     public void packageAsJar(CreateJarParams createJarParams) throws PackagerException {
 136         if (createJarParams == null) {
 137             throw new IllegalArgumentException("Parameters must not be null");
 138         }
 139 
 140         if (createJarParams.outfile == null) {
 141             throw new IllegalArgumentException("Output file is not specified");
 142         }
 143 
 144         this.createJarParams = createJarParams;
 145 
 146         //Special case: could be request for "update jar file"
 147         File jarToUpdate = jarFileToUpdate(createJarParams);
 148         Manifest m = null;
 149 
 150         if (jarToUpdate != null) {
 151             com.oracle.tools.packager.Log.info(MessageFormat.format(bundle.getString("MSG_UpdatingJar"), jarToUpdate.getAbsolutePath()));
 152             try (JarFile jf = new JarFile(jarToUpdate)) {
 153                 //extract data we want to preserve
 154                 m = jf.getManifest();
 155                 if (m != null) {
 156                     Attributes attrs = m.getMainAttributes();
 157                     if (createJarParams.applicationClass == null) {
 158                         createJarParams.applicationClass =
 159                                 attrs.getValue(Attributes.Name.MAIN_CLASS);
 160                     }
 161                     if (createJarParams.classpath == null) {
 162                         createJarParams.classpath =
 163                                 attrs.getValue(Attributes.Name.CLASS_PATH);
 164                     }
 165                     if (createJarParams.codebase == null) {
 166                         createJarParams.codebase =
 167                                 attrs.getValue(new Attributes.Name("Codebase"));
 168                     }
 169                     if (createJarParams.allPermissions == null) {
 170                         String value  =
 171                                 attrs.getValue(new Attributes.Name("Permissions"));
 172                         if (value != null) {
 173                             createJarParams.allPermissions = Boolean.valueOf(value);
 174                         }
 175                     }
 176                 }
 177             } catch (IOException ex) {
 178                 throw new PackagerException(
 179                         ex, "ERR_FileReadFailed", jarToUpdate.getAbsolutePath());
 180             }
 181         }
 182 
 183         if (createJarParams.applicationClass == null) {
 184             throw new IllegalArgumentException(
 185                     "Main application class is not specified");
 186         }
 187 
 188         //NOTE: This should be a save-to-temp file, then rename operation
 189         File applicationJar = new File(createJarParams.outdir,
 190                 createJarParams.outfile.endsWith(".jar")
 191                         ? createJarParams.outfile
 192                         : createJarParams.outfile + ".jar");
 193 
 194         if (jarToUpdate != null &&
 195                 applicationJar.getAbsoluteFile().equals(jarToUpdate.getAbsoluteFile())) {
 196             try {
 197                 File newInputJar = File.createTempFile("tempcopy", ".jar");
 198                 Files.move(jarToUpdate.toPath(), newInputJar.toPath(),
 199                         StandardCopyOption.REPLACE_EXISTING);
 200                 jarToUpdate = newInputJar;
 201             } catch (IOException ioe) {
 202                 throw new PackagerException(
 203                         ioe, "ERR_FileCopyFailed", jarToUpdate.getAbsolutePath());
 204             }
 205         }
 206 
 207         File parentFile = applicationJar.getParentFile();
 208         if (parentFile != null) {
 209             parentFile.mkdirs();
 210         }
 211 
 212         if (m == null) {
 213             m = new Manifest();
 214         }
 215         Attributes attr = m.getMainAttributes();
 216         attr.put(Attributes.Name.MANIFEST_VERSION, "1.0");
 217         attr.put(new Attributes.Name("Created-By"), "JavaFX Packager");
 218 
 219         if (createJarParams.manifestAttrs != null) {
 220             for (Entry<String, String> e: createJarParams.manifestAttrs.entrySet()) {
 221                 attr.put(new Attributes.Name(e.getKey()), e.getValue());
 222             }
 223         }
 224 
 225         attr.put(Attributes.Name.MAIN_CLASS, createJarParams.applicationClass);
 226         if (createJarParams.classpath != null) {
 227             // Allow comma or semicolon as delimeter (turn them into spaces)
 228             String cp = createJarParams.classpath;
 229             cp = cp.replace(';', ' ').replace(',', ' ');
 230             attr.put(Attributes.Name.CLASS_PATH, cp);
 231         }
 232 
 233         String existingSetting = attr.getValue("Permissions");
 234         if (existingSetting == null) {
 235             attr.put(new Attributes.Name("Permissions"),
 236                     Boolean.TRUE.equals(createJarParams.allPermissions) ? "all-permissions" : "sandbox");
 237         } else if (createJarParams.allPermissions != null && !Boolean.valueOf(existingSetting).equals(createJarParams.allPermissions)) {
 238             throw new PackagerException(
 239                 "ERR_ContradictorySetting", "Permissions");
 240         }
 241 
 242         existingSetting = attr.getValue("Codebase");
 243         if (existingSetting == null) {
 244             if (createJarParams.codebase != null) {
 245                 attr.put(new Attributes.Name("Codebase"), createJarParams.codebase);
 246             }
 247         } else if (createJarParams.codebase != null && !existingSetting.equals(createJarParams.codebase)) {
 248             throw new PackagerException(
 249                     "ERR_ContradictorySetting", "Codebase");
 250         }
 251 
 252         attr.put(new Attributes.Name("JavaFX-Version"), createJarParams.fxVersion);
 253 
 254         if (createJarParams.preloader != null) {
 255             attr.put(new Attributes.Name("JavaFX-Preloader-Class"), createJarParams.preloader);
 256         }
 257 
 258 
 259         if (createJarParams.arguments != null) {
 260             int idx = 1;
 261             for (String arg: createJarParams.arguments) {
 262                 attr.put(new Attributes.Name("JavaFX-Argument-"+idx),
 263                         encodeAsBase64(arg.getBytes()));
 264                 idx++;
 265             }
 266         }
 267         if (createJarParams.params != null) {
 268             int idx = 1;
 269             for (Param p : createJarParams.params) {
 270                 if (p.name != null) { //otherwise it is something weird and we skip it
 271                     attr.put(new Attributes.Name("JavaFX-Parameter-Name-" + idx),
 272                             encodeAsBase64(p.name.getBytes()));
 273                     if (p.value != null) { //legal, means not value specified
 274                         attr.put(new Attributes.Name("JavaFX-Parameter-Value-" + idx),
 275                                 encodeAsBase64(p.value.getBytes()));
 276                     }
 277                     idx++;
 278                 }
 279             }
 280         }
 281 
 282 
 283         if (createJarParams.css2bin) {
 284             try {
 285                 bssTmpDir = File.createTempFile("bssfiles", "");
 286             } catch (IOException ex) {
 287                 throw new PackagerException(ex, "ERR_CreatingTempFileFailed");
 288             }
 289             bssTmpDir.delete();
 290         }
 291 
 292         if (applicationJar.exists() && !applicationJar.delete()) {
 293             throw new PackagerException(
 294                     "ERR_CantDeleteFile", createJarParams.outfile);
 295         }
 296         try {
 297             jar(m, createJarParams.resources, jarToUpdate,
 298                     new JarOutputStream(new FileOutputStream(applicationJar)),
 299                     Filter.ALL);
 300         } catch (IOException ex) {
 301             throw new PackagerException(
 302                     ex, "ERR_CreatingJarFailed", createJarParams.outfile);
 303         }
 304 
 305         // cleanup
 306         deleteDirectory(bssTmpDir);
 307         this.createJarParams = null;
 308     }
 309 
 310     public void generateDeploymentPackages(DeployParams deployParams) throws PackagerException {
 311         if (deployParams == null) {
 312             throw new IllegalArgumentException("Parameters must not be null.");
 313         }
 314 
 315         try {
 316             BundleParams bp = deployParams.getBundleParams();
 317 
 318             if (bp != null) {
 319                 switch(deployParams.getBundleType()) {
 320                     case NATIVE: {
 321                         // Generate disk images.
 322                         generateNativeBundles(deployParams.outdir,
 323                                               bp.getBundleParamsAsMap(),
 324                                               BundleType.IMAGE.toString(),
 325                                               deployParams.getTargetFormat());
 326 
 327                         // Generate installers.
 328                         generateNativeBundles(deployParams.outdir,
 329                                               bp.getBundleParamsAsMap(),
 330                                               BundleType.INSTALLER.toString(),
 331                                               deployParams.getTargetFormat());
 332                         break;
 333                     }
 334 
 335                     case JNLP:
 336                     case NONE: {
 337                         // Old school default. Just generate JNLP.
 338                         generateNativeBundles(deployParams.outdir,
 339                                               bp.getBundleParamsAsMap(),
 340                                               BundleType.JNLP.toString(),
 341                                               "jnlp");
 342                         break;
 343                     }
 344 
 345                     default: {
 346                         // A specefic output format, just generate that.
 347                         generateNativeBundles(deployParams.outdir,
 348                                               bp.getBundleParamsAsMap(),
 349                                               deployParams.getBundleType().toString(),
 350                                               deployParams.getTargetFormat());
 351                     }
 352                 }
 353             }
 354         } catch (PackagerException ex) {
 355             throw ex;
 356         } catch (Exception ex) {
 357             throw new PackagerException(ex, "ERR_DeployFailed", ex.getMessage());
 358         }
 359 
 360     }
 361 
 362     private void generateNativeBundles(File outdir, Map<String, ? super Object> params, String bundleType, String bundleFormat) throws PackagerException {
 363         for (com.oracle.tools.packager.Bundler bundler : Bundlers.createBundlersInstance().getBundlers(bundleType)) {
 364             // if they specify the bundle format, require we match the ID
 365             if (bundleFormat != null && !bundleFormat.equalsIgnoreCase(bundler.getID())) continue;
 366 
 367             Map<String, ? super Object> localParams = new HashMap<>(params);
 368             try {
 369                 if (bundler.validate(localParams)) {
 370                     File result = bundler.execute(localParams, outdir);
 371                     bundler.cleanup(localParams);
 372                     if (result == null) {
 373                         throw new PackagerException("MSG_BundlerFailed", bundler.getID(), bundler.getName());
 374                     }
 375                 }
 376             } catch (UnsupportedPlatformException e) {
 377                 com.oracle.tools.packager.Log.debug(MessageFormat.format(bundle.getString("MSG_BundlerPlatformException"), bundler.getName()));
 378             } catch (ConfigException e) {
 379                 com.oracle.tools.packager.Log.debug(e);
 380                 if (e.getAdvice() != null) {
 381                     com.oracle.tools.packager.Log.info(MessageFormat.format(bundle.getString("MSG_BundlerConfigException"), bundler.getName(), e.getMessage(), e.getAdvice()));
 382                 } else {
 383                     com.oracle.tools.packager.Log.info(MessageFormat.format(bundle.getString("MSG_BundlerConfigExceptionNoAdvice"), bundler.getName(), e.getMessage()));
 384                 }
 385             } catch (RuntimeException re) {
 386                 com.oracle.tools.packager.Log.info(MessageFormat.format(bundle.getString("MSG_BundlerRuntimeException"), bundler.getName(), re.toString()));
 387                 com.oracle.tools.packager.Log.debug(re);
 388             }
 389         }
 390     }
 391 
 392     public void generateBSS(CreateBSSParams params) throws PackagerException {
 393         if (params == null) {
 394             throw new IllegalArgumentException("Parameters must not be null.");
 395         }
 396         this.createBssParams = params;
 397         createBinaryCss(createBssParams.resources, createBssParams.outdir);
 398         this.createBssParams = null;
 399     }
 400 
 401     public void signJar(SignJarParams params) throws PackagerException {
 402         try {
 403             JarSignature signature = retrieveSignature(params);
 404 
 405             for (PackagerResource pr: params.resources) {
 406                 signFile(pr, signature, params.outdir, params.verbose);
 407             }
 408 
 409         } catch (Exception ex) {
 410             com.oracle.tools.packager.Log.verbose(ex);
 411             throw new PackagerException("ERR_SignFailed", ex);
 412         }
 413 
 414     }
 415 
 416 
 417     private JarSignature retrieveSignature(SignJarParams params) throws KeyStoreException,
 418             NoSuchAlgorithmException, UnrecoverableKeyException, IOException,
 419             CertificateException, InvalidKeyException {
 420         if (params.keyPass == null) {
 421             params.keyPass = params.storePass;
 422         }
 423 
 424         if (params.keyStore == null) {
 425             throw new IOException("No keystore specified");
 426         }
 427 
 428         if (params.storePass == null) {
 429             throw new IOException("No store password specified");
 430         }
 431 
 432         if (params.storeType == null) {
 433             throw new IOException("No store type is specified");
 434         }
 435 
 436         KeyStore store = KeyStore.getInstance(params.storeType);
 437         store.load(new FileInputStream(params.keyStore), params.storePass.toCharArray());
 438 
 439         Certificate[] chain = store.getCertificateChain(params.alias);
 440         X509Certificate certChain[] = new X509Certificate[chain.length];
 441         for (int i=0; i<chain.length; i++) {
 442             certChain[i] = (X509Certificate) chain[i];
 443         }
 444 
 445         PrivateKey privateKey = (PrivateKey)
 446                 store.getKey(params.alias, params.keyPass.toCharArray());
 447 
 448         return JarSignature.create(privateKey, certChain);
 449     }
 450 
 451     private void signFile(
 452             PackagerResource pr, JarSignature signature, File outdir, boolean verbose)
 453             throws NoSuchAlgorithmException, IOException, SignatureException {
 454         if (pr.getFile().isDirectory()) {
 455             File[] children = pr.getFile().listFiles();
 456             if (children != null) {
 457                 for (File innerFile : children) {
 458                     signFile(new PackagerResource(
 459                             pr.getBaseDir(), innerFile), signature, outdir, verbose);
 460                 }
 461             }
 462         } else {
 463             File jar = pr.getFile();
 464             File parent = jar.getParentFile();
 465             String name = "bsigned_" + jar.getName();
 466             File signedJar = new File(parent, name);
 467 
 468             System.out.println("Signing (BLOB) " + jar.getPath());
 469 
 470             signAsBLOB(jar, signedJar, signature);
 471 
 472             File destJar;
 473             if (outdir != null) {
 474                 destJar = new File(outdir, pr.getRelativePath());
 475             } else {
 476                 // in-place
 477                 jar.delete();
 478                 destJar = jar;
 479             }
 480             destJar.delete();
 481             destJar.getParentFile().mkdirs();
 482             signedJar.renameTo(destJar);
 483             if (verbose) {
 484                 System.out.println("Signed as " + destJar.getPath());
 485             }
 486         }
 487     }
 488 
 489     private void signAsBLOB(final File jar, File signedJar, JarSignature signature)
 490             throws IOException, NoSuchAlgorithmException, SignatureException
 491     {
 492         if (signature == null) {
 493             throw new IllegalStateException("Should retrieve signature first");
 494         }
 495 
 496         InputStreamSource in = () -> new FileInputStream(jar);
 497         if (!signedJar.isFile()) {
 498             signedJar.createNewFile();
 499         }
 500         FileOutputStream fos = new FileOutputStream(signedJar);
 501         signature.signJarAsBLOB(in, new ZipOutputStream(fos));
 502     }
 503 
 504     public void makeAll(MakeAllParams makeAllParams) throws PackagerException {
 505         final String exe = (Platform.getPlatform() == Platform.WINDOWS) ? ".exe" : "";
 506         String jHome = System.getenv("JAVA_HOME");
 507         if (jHome == null) {
 508             jHome = System.getProperty("java.home");
 509         }
 510         if (jHome == null) {
 511             throw new PackagerException("ERR_MissingJavaHome");
 512         }
 513 
 514         final File javac = new File(new File(jHome), "bin/javac" + exe);
 515 
 516         String jfxHome = System.getenv("JAVAFX_HOME");
 517         if (jfxHome == null) {
 518             jfxHome = System.getProperty("javafx.home");
 519         }
 520         if (jfxHome == null) {
 521             throw new PackagerException("ERR_MissingJavaFxHome");
 522         }
 523 
 524         final String srcDirName = "src";
 525         final String compiledDirName = "compiled";
 526         final String distDirName = "dist";
 527         final String outfileName = "dist";
 528         final String jarName = outfileName + ".jar";
 529 
 530         final File distDir = new File(distDirName);
 531 
 532         final File compiledDir = new File(compiledDirName);
 533         compiledDir.mkdir();
 534 
 535         try {
 536             final File tmpFile = File.createTempFile("javac", "sources", new File("."));
 537             tmpFile.deleteOnExit();
 538             try (FileWriter sources = new FileWriter(tmpFile)) {
 539                 scanAndCopy(new PackagerResource(new File(srcDirName), "."), sources, compiledDir);
 540             }
 541             String classpath = jfxHome + "/../lib/jfxrt.jar";
 542             if (makeAllParams.classpath != null) {
 543                 classpath += File.pathSeparator + makeAllParams.classpath;
 544             }
 545             if (makeAllParams.verbose) {
 546                 System.out.println("Executing javac:");
 547                 System.out.printf("%s %s %s %s %s %s%n",
 548                         javac.getAbsolutePath(),
 549                         "-d", compiledDirName,
 550                         "-cp", classpath,
 551                         "@" + tmpFile.getAbsolutePath());
 552             }
 553             int ret = execute(
 554                     javac.getAbsolutePath(),
 555                     "-d", compiledDirName,
 556                     "-cp", classpath,
 557                     "@" + tmpFile.getAbsolutePath());
 558             if (ret != 0) {
 559                 throw new PackagerException("ERR_JavacFailed", Integer.toString(ret));
 560             }
 561         } catch (PackagerException e) {
 562             throw e;
 563         } catch (Exception e) {
 564             throw new PackagerException(e, "ERR_MakeAllJavacFailed");
 565         }
 566 
 567         CreateJarParams cjp = new CreateJarParams();
 568         cjp.applicationClass = makeAllParams.appClass;
 569         cjp.preloader = makeAllParams.preloader;
 570         cjp.classpath = makeAllParams.classpath;
 571         cjp.css2bin = false;
 572         cjp.outdir = distDir;
 573         cjp.outfile = jarName;
 574         cjp.addResource(compiledDir, ".");
 575 
 576         packageAsJar(cjp);
 577 
 578         DeployParams dp = new DeployParams();
 579         dp.applicationClass = makeAllParams.appClass;
 580         dp.appName = makeAllParams.appName;
 581         dp.description = "Application description";
 582         dp.height = makeAllParams.height;
 583         dp.width = makeAllParams.width;
 584         dp.vendor = "Application vendor";
 585         dp.outdir = distDir;
 586         dp.outfile = outfileName;
 587         dp.addResource(distDir, jarName);
 588         //noinspection deprecation
 589         dp.setBundleType(BundleType.ALL);
 590 
 591         generateDeploymentPackages(dp);
 592 
 593         deleteDirectory(compiledDir);
 594     }
 595 
 596     @SuppressWarnings("unchecked")
 597     private static int execute(Object ... args) throws IOException, InterruptedException {
 598         final ArrayList<String> argsList = new ArrayList<>();
 599         for (Object a : args) {
 600             if (a instanceof List) {
 601                 argsList.addAll((List)a);
 602             } else if (a instanceof String) {
 603                 argsList.add((String)a);
 604             }
 605         }
 606         final Process p = Runtime.getRuntime().exec(argsList.toArray(new String[argsList.size()]));
 607         final BufferedReader in = new BufferedReader(new InputStreamReader(p.getInputStream()));
 608         Thread t = new Thread(() -> {
 609             try {
 610                 String line;
 611                 while ((line = in.readLine()) != null) {
 612                     System.out.println(line);
 613                 }
 614             } catch (IOException ioe) {
 615                 com.oracle.tools.packager.Log.verbose(ioe);
 616             }
 617         });
 618         t.setDaemon(true);
 619         t.start();
 620         final BufferedReader err = new BufferedReader(new InputStreamReader(p.getErrorStream()));
 621         t = new Thread(() -> {
 622             try {
 623                 String line;
 624                 while ((line = err.readLine()) != null) {
 625                     System.err.println(line);
 626                 }
 627             } catch (IOException ioe) {
 628                 Log.verbose(ioe);
 629             }
 630         });
 631         t.setDaemon(true);
 632         t.start();
 633         return p.waitFor();
 634     }
 635 
 636     private static void scanAndCopy(PackagerResource dir, Writer out, File outdir) throws PackagerException {
 637         if (!dir.getFile().exists()) {
 638             throw new PackagerException("ERR_MissingDirectory", dir.getFile().getName());
 639         }
 640         File[] dirFilesList = dir.getFile().listFiles();
 641         if ((dirFilesList == null) || (dirFilesList.length == 0)) {
 642             throw new PackagerException("ERR_EmptySourceDirectory", dir.getFile().getName());
 643         }
 644         try {
 645             for (File f : dirFilesList) {
 646                 if (f.isDirectory()) {
 647                     scanAndCopy(new PackagerResource(dir.getBaseDir(), f), out, outdir);
 648                 } else if (f.getName().endsWith(".java")) {
 649                     out.write('\'' + f.getAbsolutePath().replace('\\', '/') + "\'\n");
 650                 } else {
 651                     copyFileToOutDir(new FileInputStream(f),
 652                             new File(outdir.getPath() + File.separator
 653                                     + dir.getRelativePath() + File.separator
 654                                     + f.getName()));
 655                 }
 656             }
 657         } catch (IOException ex) {
 658             throw new PackagerException("ERR_FileCopyFailed", dir.getFile().getName());
 659         }
 660     }
 661 
 662     private String encodeAsBase64(byte inp[]) {
 663         return Base64.getEncoder().encodeToString(inp);
 664     }
 665 
 666     private static void copyFileToOutDir(
 667             InputStream isa, File fout) throws PackagerException {
 668 
 669         final File outDir = fout.getParentFile();
 670         if (!outDir.exists() && !outDir.mkdirs()) {
 671             throw new PackagerException("ERR_CreatingDirFailed", outDir.getPath());
 672         }
 673         try (InputStream is = isa; OutputStream out = new FileOutputStream(fout)) {
 674             byte[] buf = new byte[16384];
 675             int len;
 676             while ((len = is.read(buf)) > 0) {
 677                 out.write(buf, 0, len);
 678             }
 679         } catch (IOException ex) {
 680             throw new PackagerException(ex, "ERR_FileCopyFailed", outDir.getPath());
 681         }
 682     }
 683 
 684     private void jar(
 685             Manifest manifest, List<PackagerResource> files,
 686             File importJarFile, JarOutputStream jar, Filter filter)
 687             throws IOException, PackagerException {
 688         try {
 689             jar.putNextEntry(new ZipEntry("META-INF/"));
 690             jar.closeEntry();
 691             jar.putNextEntry(new ZipEntry(JarFile.MANIFEST_NAME));
 692             manifest.write(jar);
 693             jar.closeEntry();
 694 
 695             alreadyAddedEntries.add("META-INF/");
 696             if (importJarFile != null) { //updating jar file
 697                 copyFromOtherJar(jar, importJarFile);
 698             } else { //normal situation
 699                 for (PackagerResource pr : files) {
 700                     jar(pr.getFile(), jar, filter,
 701                             pr.getBaseDir().getAbsolutePath().length() + 1);
 702                 }
 703             }
 704         } finally {
 705             jar.close();
 706             alreadyAddedEntries.clear();
 707         }
 708     }
 709 
 710     private Set<String> alreadyAddedEntries = new HashSet<>();
 711     private void createParentEntries(String relativePath, JarOutputStream jar) throws IOException {
 712         String[] pathComponents = relativePath.split("/");
 713         StringBuilder pathSB = new StringBuilder();
 714         // iterating over directories only, the last component is the file
 715         // or will be created next time.
 716         for (int i = 0; i < pathComponents.length - 1; i++) {
 717             pathSB.append(pathComponents[i]).append("/");
 718             if (!alreadyAddedEntries.contains(pathSB.toString())) {
 719                 jar.putNextEntry(new ZipEntry(pathSB.toString()));
 720                 jar.closeEntry();
 721             }
 722             alreadyAddedEntries.add(pathSB.toString());
 723         }
 724     }
 725 
 726     //add everything but manifest from given jar file
 727     private void copyFromOtherJar(JarOutputStream jar, File inputFile) throws IOException {
 728         JarFile inJar = new JarFile(inputFile);
 729 
 730         Enumeration<JarEntry> all = inJar.entries();
 731         while (all.hasMoreElements()) {
 732             JarEntry je = all.nextElement();
 733 
 734             //skip manifest or root manifest dir entry (can not add duplicate)
 735             if ("META-INF/MANIFEST.MF".equals(je.getName().toUpperCase())
 736                     || "META-INF/".equals(je.getName().toUpperCase())) {
 737                 continue;
 738             }
 739 
 740             jar.putNextEntry(new JarEntry(je.getName()));
 741 
 742             byte b[] = new byte[65000];
 743             int i;
 744             try (InputStream in = inJar.getInputStream(je)) {
 745                 while ((i = in.read(b)) > 0) {
 746                     jar.write(b, 0, i);
 747                 }
 748             }
 749 
 750             jar.closeEntry();
 751         }
 752     }
 753 
 754     private void jar(File f, JarOutputStream jar, Filter filter, int cut)
 755             throws IOException, PackagerException {
 756         if (!f.exists()) {
 757             throw new FileNotFoundException("Input folder does not exist ["
 758                     +f.getAbsolutePath()+"]");
 759         }
 760 
 761         if (f.isDirectory()) {
 762             File[] children = f.listFiles();
 763             if (children != null) {
 764                 for (File innerFile : children) {
 765                     jar(innerFile, jar, filter, cut);
 766                 }
 767             }
 768         } else if (filter == Filter.ALL
 769                 || (filter == Filter.CLASSES_ONLY && f.getName().endsWith(".class"))
 770                 || (filter == Filter.RESOURCES && isResource(f.getAbsolutePath()))) {
 771             final String absPath = f.getAbsolutePath();
 772             if (absPath.endsWith("META-INF\\MANIFEST.MF")
 773                     || absPath.endsWith("META-INF/MANIFEST.MF")) {
 774                 return;
 775             }
 776             createParentEntries(absPath.substring(cut).replace('\\', '/'), jar);
 777             if (createJarParams.css2bin && f.getName().endsWith(".css")) {
 778                 // generate bss file into temporary directory
 779                 int startOfExt = absPath.lastIndexOf(".") + 1;
 780                 String bssFileName = absPath
 781                         .substring(cut, startOfExt)
 782                         .concat("bss");
 783 
 784                 File bssFile = new File(bssTmpDir, bssFileName);
 785                 bssFile.getParentFile().mkdirs();
 786 
 787                 createBinaryCss(absPath, bssFile.getAbsolutePath());
 788                 jar.putNextEntry(new ZipEntry(bssFileName.replace('\\', '/')));
 789                 f = bssFile;
 790             } else {
 791                 jar.putNextEntry(new ZipEntry(absPath.substring(cut).replace('\\', '/')));
 792             }
 793 
 794             byte b[] = new byte[65000];
 795             int i;
 796 
 797             try (FileInputStream in = new FileInputStream(f)) {
 798                 while ((i = in.read(b)) > 0) {
 799                     jar.write(b, 0, i);
 800                 }
 801             }
 802             jar.closeEntry();
 803         }
 804     }
 805 
 806     private void createBinaryCss(List<PackagerResource> cssResources, File outdir)
 807             throws PackagerException {
 808         for (PackagerResource cssRes: cssResources) {
 809             String relPath = cssRes.getRelativePath();
 810             createBinaryCss(cssRes.getFile(), outdir, relPath);
 811         }
 812     }
 813 
 814     private void createBinaryCss(File f, File outdir, String relPath)
 815             throws PackagerException {
 816         if (f.isDirectory()) {
 817             File[] children = f.listFiles();
 818             if (children != null) {
 819                 for (File innerFile : children) {
 820                     createBinaryCss(innerFile, outdir, relPath + '/' + innerFile.getName());
 821                 }
 822             }
 823         } else if (f.getName().endsWith(".css")) {
 824             String cssFileName = f.getAbsolutePath();
 825             String bssFileName = new File(outdir.getAbsolutePath(),
 826                     replaceExtensionByBSS(relPath))
 827                     .getAbsolutePath();
 828             createBinaryCss(cssFileName, bssFileName);
 829         }
 830     }
 831 
 832     // Returns path to jfxrt.jar relatively to jar containing PackagerLib.class
 833     private String getJfxrtPath() throws PackagerException {
 834         String theClassFile = "PackagerLib.class";
 835         Class theClass = PackagerLib.class;
 836         String classUrl = theClass.getResource(theClassFile).toString();
 837 
 838         if (!classUrl.startsWith("jar:file:") || !classUrl.contains("!")){
 839             throw new PackagerException("ERR_CantFindRuntime");
 840         }
 841 
 842         // Strip everything after and including the "!"
 843         classUrl = classUrl.substring(0, classUrl.lastIndexOf("!"));
 844         // Strip everything after the last "/" or "\" to get rid of the jar filename
 845         int lastIndexOfSlash = Math.max(classUrl.lastIndexOf("/"), classUrl.lastIndexOf("\\"));
 846 
 847         return classUrl.substring(0, lastIndexOfSlash)
 848                 + "/../lib/jfxrt.jar!/";
 849     }
 850 
 851     private Class loadClassFromRuntime(String className) throws PackagerException {
 852         try {
 853             ClassLoader cl = getClassLoader();
 854             return cl.loadClass(className);
 855         } catch (ClassNotFoundException ex) {
 856             throw new PackagerException(ex, "ERR_CantFindRuntime");
 857         }
 858     }
 859 
 860     private void createBinaryCss(String cssFile, String binCssFile) throws PackagerException {
 861         String ofname = (binCssFile != null)
 862                 ? binCssFile
 863                 : replaceExtensionByBSS(cssFile);
 864 
 865         // create parent directories
 866         File of = new File(ofname);
 867         File parentFile = of.getParentFile();
 868         if (parentFile != null) {
 869             parentFile.mkdirs();
 870         }
 871 
 872         // Using reflection because CSS parser is part of runtime
 873         // and we want to avoid dependency on jfxrt during build
 874         Class<?> clazz;
 875         try {
 876             clazz = Class.forName("com.sun.javafx.css.parser.Css2Bin");
 877         } catch (ClassNotFoundException e) {
 878             // class was not found with default class loader, trying to
 879             // locate it by loading from jfxrt.jar
 880             clazz = loadClassFromRuntime("com.sun.javafx.css.parser.Css2Bin");
 881         }
 882 
 883         try {
 884             Method m = clazz.getMethod("convertToBinary", String.class, String.class);
 885             m.invoke(null, cssFile, ofname);
 886         } catch (Exception ex) {
 887             Throwable causeEx = ex.getCause();
 888             String cause = (causeEx != null) ? causeEx.getMessage()
 889                     : bundle.getString("ERR_UnknownReason");
 890 
 891             throw new PackagerException(ex, "ERR_BSSConversionFailed", cssFile, cause);
 892         }
 893     }
 894 
 895     private static String replaceExtensionByBSS(String cssName) {
 896         return cssName.substring(0, cssName.lastIndexOf(".") + 1).concat("bss");
 897     }
 898 
 899 
 900     private boolean isResource(String name) {
 901         if (name.endsWith(".class")) {
 902             return false;
 903         }
 904         if (name.endsWith(".java")) {
 905             return false;
 906         }
 907         if (name.endsWith(".fx")) {
 908             return false;
 909         }
 910         if (name.endsWith(".cvsignore")) {
 911             return false;
 912         }
 913         if (name.endsWith(".hgignore")) {
 914             return false;
 915         }
 916         if (name.endsWith("vssver.scc")) {
 917             return false;
 918         }
 919         if (name.endsWith(".DS_Store")) {
 920             return false;
 921         }
 922         if (name.endsWith("~")) {
 923             return false;
 924         }
 925         name = name.replace('\\', '/');
 926         if (name.contains("/CVS/")) {
 927             return false;
 928         }
 929         if (name.contains("/.svn/")) {
 930             return false;
 931         }
 932         if (name.contains("/.hg/")) {
 933             return false;
 934         }
 935         if (name.contains("/.#")) {
 936             return false;
 937         }
 938         if (name.contains("/._")) {
 939             return false;
 940         }
 941         if (name.endsWith("#") && name.contains("/#")) {
 942             return false;
 943         }
 944         if (name.endsWith("%") && name.contains("/%")) {
 945             return false;
 946         }
 947         if (name.endsWith("MANIFEST.MF")) {
 948             return false;
 949         }
 950         return true;
 951     }
 952 
 953     private static boolean deleteDirectory(File dir) {
 954         if (dir == null || !dir.exists()) {
 955             return false;
 956         }
 957 
 958         if (dir.isDirectory()) {
 959             for (String file : dir.list()) {
 960                 deleteDirectory(new File(dir, file));
 961             }
 962         }
 963         return dir.delete();
 964     }
 965 }