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