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