1 /*
   2  * Copyright (c) 2012, 2014, 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 package com.sun.javafx.tools.packager.bundlers;
  26 
  27 import com.oracle.bundlers.*;
  28 import com.oracle.bundlers.JreUtils.Rule;
  29 import com.sun.javafx.tools.packager.Log;
  30 import com.sun.javafx.tools.resource.mac.MacResources;
  31 
  32 import java.io.*;
  33 import java.net.MalformedURLException;
  34 import java.net.URL;
  35 import java.text.MessageFormat;
  36 import java.util.*;
  37 
  38 import static com.oracle.bundlers.StandardBundlerParam.*;
  39 import static com.oracle.bundlers.mac.MacBaseInstallerBundler.getPredefinedImage;
  40 
  41 public class MacAppBundler extends AbstractBundler {
  42 
  43     private static final ResourceBundle I18N =
  44             ResourceBundle.getBundle("com.oracle.bundlers.mac.MacAppBundler");
  45 
  46     public final static String MAC_BUNDLER_PREFIX =
  47             BUNDLER_PREFIX + "macosx" + File.separator;
  48 
  49     private static final String EXECUTABLE_NAME      = "JavaAppLauncher";
  50     private static final String TEMPLATE_BUNDLE_ICON = "GenericApp.icns";
  51     private static final String OS_TYPE_CODE         = "APPL";
  52     private static final String TEMPLATE_INFO_PLIST  = "Info.plist.template";
  53 
  54     private static Map<String, String> getMacCategories() {
  55         Map<String, String> map = new HashMap<>();
  56         map.put("Business", "public.app-category.business");
  57         map.put("Developer Tools", "public.app-category.developer-tools");
  58         map.put("Education", "public.app-category.education");
  59         map.put("Entertainment", "public.app-category.entertainment");
  60         map.put("Finance", "public.app-category.finance");
  61         map.put("Games", "public.app-category.games");
  62         map.put("Graphics & Design", "public.app-category.graphics-design");
  63         map.put("Healthcare & Fitness", "public.app-category.healthcare-fitness");
  64         map.put("Lifestyle", "public.app-category.lifestyle");
  65         map.put("Medical", "public.app-category.medical");
  66         map.put("Music", "public.app-category.music");
  67         map.put("News", "public.app-category.news");
  68         map.put("Photography", "public.app-category.photography");
  69         map.put("Productivity", "public.app-category.productivity");
  70         map.put("Reference", "public.app-category.reference");
  71         map.put("Social Networking", "public.app-category.social-networking");
  72         map.put("Sports", "public.app-category.sports");
  73         map.put("Travel", "public.app-category.travel");
  74         map.put("Utilities", "public.app-category.utilities");
  75         map.put("Video", "public.app-category.video");
  76         map.put("Weather", "public.app-category.weather");
  77 
  78         map.put("Action Games", "public.app-category.action-games");
  79         map.put("Adventure Games", "public.app-category.adventure-games");
  80         map.put("Arcade Games", "public.app-category.arcade-games");
  81         map.put("Board Games", "public.app-category.board-games");
  82         map.put("Card Games", "public.app-category.card-games");
  83         map.put("Casino Games", "public.app-category.casino-games");
  84         map.put("Dice Games", "public.app-category.dice-games");
  85         map.put("Educational Games", "public.app-category.educational-games");
  86         map.put("Family Games", "public.app-category.family-games");
  87         map.put("Kids Games", "public.app-category.kids-games");
  88         map.put("Music Games", "public.app-category.music-games");
  89         map.put("Puzzle Games", "public.app-category.puzzle-games");
  90         map.put("Racing Games", "public.app-category.racing-games");
  91         map.put("Role Playing Games", "public.app-category.role-playing-games");
  92         map.put("Simulation Games", "public.app-category.simulation-games");
  93         map.put("Sports Games", "public.app-category.sports-games");
  94         map.put("Strategy Games", "public.app-category.strategy-games");
  95         map.put("Trivia Games", "public.app-category.trivia-games");
  96         map.put("Word Games", "public.app-category.word-games");
  97 
  98         return map;
  99     }
 100 
 101     public static final EnumeratedBundlerParam<String> MAC_CATEGORY =
 102             new EnumeratedBundlerParam<>(
 103                     "Category",
 104                     "Mac App Store Categories. Note that the key is the string to display to the user and the value is the id of the category",
 105                     "mac.category",
 106                     String.class,
 107                     new String[] {CATEGORY.getID()},
 108                     params -> "Unknown",
 109                     false,
 110                     (s, p) -> s,
 111                     getMacCategories(),
 112                     false //strict - for MacStoreBundler this should be strict
 113             );
 114 
 115     public static final BundlerParamInfo<String> MAC_CF_BUNDLE_NAME =
 116             new StandardBundlerParam<>(
 117                     "CFBundleName",
 118                     "The name of the app as it appears in the Menu Bar.  This can be different from the application name.  This name should be less than 16 characters long and be suitable for displaying in the menu bar and the app’s Info window.",
 119                     "mac.CFBundleName",
 120                     String.class,
 121                     null,
 122                     params -> null,
 123                     false,
 124                     (s, p) -> s);
 125 
 126     public static final BundlerParamInfo<File> CONFIG_ROOT = new StandardBundlerParam<>(
 127             I18N.getString("param.config-root.name"),
 128             I18N.getString("param.config-root.description"),
 129             "configRoot",
 130             File.class,
 131             null,
 132             params -> {
 133                 File configRoot = new File(BUILD_ROOT.fetchFrom(params), "macosx");
 134                 configRoot.mkdirs();
 135                 return configRoot;
 136             },
 137             false,
 138             (s, p) -> new File(s));
 139 
 140     public static final BundlerParamInfo<URL> RAW_EXECUTABLE_URL = new StandardBundlerParam<>(
 141             "Launcher URL",
 142             "Override the packager default launcher with a custom launcher.",
 143             "mac.launcher.url",
 144             URL.class,
 145             null,
 146             params -> MacResources.class.getResource(EXECUTABLE_NAME),
 147             false,
 148             (s, p) -> {
 149                 try {
 150                     return new URL(s);
 151                 } catch (MalformedURLException e) {
 152                     Log.info(e.toString());
 153                     return null;
 154                 }
 155             });
 156 
 157     public static final BundlerParamInfo<String> DEFAULT_ICNS_ICON = new StandardBundlerParam<>(
 158             "Default Icon",
 159             "The Default Icon for when a user does not specify an icns file.",
 160             ".mac.default.icns",
 161             String.class,
 162             null,
 163             params -> TEMPLATE_BUNDLE_ICON,
 164             false,
 165             (s, p) -> s);
 166 
 167     //Subsetting of JRE is restricted.
 168     //JRE README defines what is allowed to strip:
 169     //   http://www.oracle.com/technetwork/java/javase/jre-7-readme-430162.html //TODO update when 8 goes GA
 170     //
 171     public static final BundlerParamInfo<Rule[]> MAC_JDK_RULES = new StandardBundlerParam<>(
 172             "",
 173             "",
 174             ".mac-jdk.runtime.rules",
 175             Rule[].class,
 176             null,
 177             params -> new Rule[]{
 178                     Rule.suffixNeg("macos/libjli.dylib"),
 179                     Rule.suffixNeg("resources"),
 180                     Rule.suffixNeg("home/bin"),
 181                     Rule.suffixNeg("home/db"),
 182                     Rule.suffixNeg("home/demo"),
 183                     Rule.suffixNeg("home/include"),
 184                     Rule.suffixNeg("home/lib"),
 185                     Rule.suffixNeg("home/man"),
 186                     Rule.suffixNeg("home/release"),
 187                     Rule.suffixNeg("home/sample"),
 188                     Rule.suffixNeg("home/src.zip"),
 189                     //"home/rt" is not part of the official builds
 190                     // but we may be creating this symlink to make older NB projects
 191                     // happy. Make sure to not include it into final artifact
 192                     Rule.suffixNeg("home/rt"),
 193                     Rule.suffixNeg("jre/bin"),
 194                     Rule.suffixNeg("jre/bin/rmiregistry"),
 195                     Rule.suffixNeg("jre/bin/tnameserv"),
 196                     Rule.suffixNeg("jre/bin/keytool"),
 197                     Rule.suffixNeg("jre/bin/klist"),
 198                     Rule.suffixNeg("jre/bin/ktab"),
 199                     Rule.suffixNeg("jre/bin/policytool"),
 200                     Rule.suffixNeg("jre/bin/orbd"),
 201                     Rule.suffixNeg("jre/bin/servertool"),
 202                     Rule.suffixNeg("jre/bin/javaws"),
 203                     Rule.suffixNeg("jre/bin/java"),
 204                     //Rule.suffixNeg("jre/lib/ext"), //need some of jars there for https to work
 205                     Rule.suffixNeg("jre/lib/nibs"),
 206                     //keep core deploy APIs but strip plugin dll
 207                     //Rule.suffixNeg("jre/lib/deploy"),
 208                     //Rule.suffixNeg("jre/lib/deploy.jar"),
 209                     //Rule.suffixNeg("jre/lib/javaws.jar"),
 210                     //Rule.suffixNeg("jre/lib/libdeploy.dylib"),
 211                     //Rule.suffixNeg("jre/lib/plugin.jar"),
 212                     Rule.suffixNeg("jre/lib/libnpjp2.dylib"),
 213                     Rule.suffixNeg("jre/lib/security/javaws.policy"),
 214                     Rule.substrNeg("Contents/Info.plist")
 215             },
 216             false,
 217             (s, p) -> null
 218     );
 219 
 220     public static final BundlerParamInfo<RelativeFileSet> MAC_RUNTIME = new StandardBundlerParam<>(
 221             RUNTIME.getName(),
 222             RUNTIME.getDescription(),
 223             RUNTIME.getID(),
 224             RelativeFileSet.class,
 225             null,
 226             params -> extractMacRuntime(System.getProperty("java.home"), params),
 227             false,
 228             MacAppBundler::extractMacRuntime
 229     );
 230 
 231     public static RelativeFileSet extractMacRuntime(String base, Map<String, ? super Object> params) {
 232         if (base.endsWith("/Home")) {
 233             throw new IllegalArgumentException("Currently Macs require a JDK to package");
 234         } else if (base.endsWith("/Home/jre")) {
 235             File baseDir = new File(base).getParentFile().getParentFile().getParentFile();
 236             return JreUtils.extractJreAsRelativeFileSet(baseDir.toString(),
 237                     MAC_JDK_RULES.fetchFrom(params));
 238         } else {
 239             // for now presume we are pointed to the top of a JDK
 240             return JreUtils.extractJreAsRelativeFileSet(base,
 241                     MAC_JDK_RULES.fetchFrom(params));
 242         }
 243     }
 244 
 245     public MacAppBundler() {
 246         super();
 247         baseResourceLoader = MacResources.class;
 248     }
 249 
 250     @Override
 251     public boolean validate(Map<String, ? super Object> params) throws UnsupportedPlatformException, ConfigException {
 252         try {
 253             logParameters(params);
 254             return doValidate(params);
 255         } catch (RuntimeException re) {
 256             throw new ConfigException(re);
 257         }
 258     }
 259 
 260     //to be used by chained bundlers, e.g. by EXE bundler to avoid
 261     // skipping validation if p.type does not include "image"
 262     public boolean doValidate(Map<String, ? super Object> p) throws UnsupportedPlatformException, ConfigException {
 263         if (!System.getProperty("os.name").toLowerCase().contains("os x")) {
 264             throw new UnsupportedPlatformException();
 265         }
 266 
 267         if (getPredefinedImage(p) != null) {
 268             return true;
 269         }
 270 
 271         if (MAIN_JAR.fetchFrom(p) == null) {
 272             throw new ConfigException(
 273                     "Main application jar is missing.",
 274                     "Make sure to use fx:jar task to create main application jar.");
 275         }
 276 
 277         //validate required inputs
 278         if (USE_FX_PACKAGING.fetchFrom(p)) {
 279             testRuntime(p, new String[] {"Contents/Home/jre/lib/ext/jfxrt.jar", "Contents/Home/jre/lib/jfxrt.jar"});
 280         }
 281 
 282         return true;
 283     }
 284 
 285 
 286     private File getConfig_InfoPlist(Map<String, ? super Object> params) {
 287         return new File(CONFIG_ROOT.fetchFrom(params), "Info.plist");
 288     }
 289 
 290     private File getConfig_Icon(Map<String, ? super Object> params) {
 291         return new File(CONFIG_ROOT.fetchFrom(params), APP_NAME.fetchFrom(params) + ".icns");
 292     }
 293 
 294     private void prepareConfigFiles(Map<String, ? super Object> params) throws IOException {
 295         File infoPlistFile = getConfig_InfoPlist(params);
 296         infoPlistFile.createNewFile();
 297         writeInfoPlist(infoPlistFile, params);
 298 
 299         // Copy icon to Resources folder
 300         prepareIcon(params);
 301     }
 302 
 303     public File doBundle(Map<String, ? super Object> p, File outputDirectory, boolean dependentTask) {
 304         File rootDirectory = null;
 305         try {
 306             final File predefinedImage = getPredefinedImage(p);
 307             if (predefinedImage != null) {
 308                 return predefinedImage;
 309             }
 310 
 311             File file = BUILD_ROOT.fetchFrom(p);
 312 
 313             //prepare config resources (we will copy them to the bundle later)
 314             // NB: explicitly saving them to simplify customization
 315             prepareConfigFiles(p);
 316 
 317             // Create directory structure
 318             rootDirectory = new File(outputDirectory, APP_NAME.fetchFrom(p) + ".app");
 319             IOUtils.deleteRecursive(rootDirectory);
 320             rootDirectory.mkdirs();
 321 
 322             if (!dependentTask) {
 323                 Log.info("Creating app bundle: " + rootDirectory.getAbsolutePath());
 324             }
 325 
 326             File contentsDirectory = new File(rootDirectory, "Contents");
 327             contentsDirectory.mkdirs();
 328 
 329             File macOSDirectory = new File(contentsDirectory, "MacOS");
 330             macOSDirectory.mkdirs();
 331 
 332             File javaDirectory = new File(contentsDirectory, "Java");
 333             javaDirectory.mkdirs();
 334 
 335             File plugInsDirectory = new File(contentsDirectory, "PlugIns");
 336 
 337             File resourcesDirectory = new File(contentsDirectory, "Resources");
 338             resourcesDirectory.mkdirs();
 339 
 340             // Generate PkgInfo
 341             File pkgInfoFile = new File(contentsDirectory, "PkgInfo");
 342             pkgInfoFile.createNewFile();
 343             writePkgInfo(pkgInfoFile);
 344 
 345             // Copy executable to MacOS folder
 346             File executableFile = new File(macOSDirectory, getLauncherName(p));
 347             IOUtils.copyFromURL(
 348                     RAW_EXECUTABLE_URL.fetchFrom(p),
 349                     executableFile);
 350 
 351             executableFile.setExecutable(true, false);
 352 
 353             // Copy runtime to PlugIns folder
 354             copyRuntime(plugInsDirectory, p);
 355 
 356             // Copy class path entries to Java folder
 357             copyClassPathEntries(javaDirectory, p);
 358 
 359 //TODO: Need to support adding native libraries.
 360             // Copy library path entries to MacOS folder
 361             //copyLibraryPathEntries(macOSDirectory);
 362 
 363             /*********** Take care of "config" files *******/
 364             // Copy icon to Resources folder
 365             IOUtils.copyFile(getConfig_Icon(p),
 366                     new File(resourcesDirectory, getConfig_Icon(p).getName()));
 367             // Generate Info.plist
 368             IOUtils.copyFile(getConfig_InfoPlist(p),
 369                     new File(contentsDirectory, "Info.plist"));
 370         } catch (IOException ex) {
 371             Log.verbose(ex);
 372             return null;
 373         } finally {
 374             if (!VERBOSE.fetchFrom(p)) {
 375                 //cleanup
 376                 cleanupConfigFiles(p);
 377             } else {
 378                 Log.info("Config files are saved to " +
 379                         CONFIG_ROOT.fetchFrom(p).getAbsolutePath()  +
 380                         ". Use them to customize package.");
 381             }
 382         }
 383         return rootDirectory;
 384     }
 385 
 386     public String getAppName(Map<String, ? super Object> params) {
 387         return APP_NAME.fetchFrom(params) + ".app";
 388     }
 389 
 390     protected void cleanupConfigFiles(Map<String, ? super Object> params) {
 391         //Since building the app can be bypassed, make sure configRoot was set
 392         if (CONFIG_ROOT.fetchFrom(params) != null) {
 393             if (getConfig_Icon(params) != null) {
 394                 getConfig_Icon(params).delete();
 395             }
 396             if (getConfig_InfoPlist(params) != null) {
 397                 getConfig_InfoPlist(params).delete();
 398             }
 399         }
 400     }
 401 
 402 
 403     private void copyClassPathEntries(File javaDirectory, Map<String, ? super Object> params) throws IOException {
 404         RelativeFileSet classPath = APP_RESOURCES.fetchFrom(params);
 405         if (classPath == null) {
 406             throw new RuntimeException("Null app resources?");
 407         }
 408         File srcdir = classPath.getBaseDirectory();
 409         for (String fname : classPath.getIncludedFiles()) {
 410             IOUtils.copyFile(
 411                     new File(srcdir, fname), new File(javaDirectory, fname));
 412         }
 413     }
 414 
 415     private void copyRuntime(File plugInsDirectory, Map<String, ? super Object> params) throws IOException {
 416         RelativeFileSet runTime = MAC_RUNTIME.fetchFrom(params);
 417         if (runTime == null) {
 418             //request to use system runtime => do not bundle
 419             return;
 420         }
 421         plugInsDirectory.mkdirs();
 422 
 423         File srcdir = runTime.getBaseDirectory();
 424         File destDir = new File(plugInsDirectory, srcdir.getName());
 425         Set<String> filesToCopy = runTime.getIncludedFiles();
 426 
 427         for (String fname : filesToCopy) {
 428             IOUtils.copyFile(
 429                     new File(srcdir, fname), new File(destDir, fname));
 430         }
 431     }
 432 
 433     private void prepareIcon(Map<String, ? super Object> params) throws IOException {
 434         File icon = ICON.fetchFrom(params);
 435         if (icon == null || !icon.exists()) {
 436             fetchResource(MAC_BUNDLER_PREFIX+ APP_NAME.fetchFrom(params) +".icns",
 437                     "icon",
 438                     DEFAULT_ICNS_ICON.fetchFrom(params),
 439                     getConfig_Icon(params),
 440                     VERBOSE.fetchFrom(params));
 441         } else {
 442             fetchResource(MAC_BUNDLER_PREFIX+ APP_NAME.fetchFrom(params) +".icns",
 443                     "icon",
 444                     icon,
 445                     getConfig_Icon(params),
 446                     VERBOSE.fetchFrom(params));
 447         }
 448     }
 449 
 450     private String getLauncherName(Map<String, ? super Object> params) {
 451         if (APP_NAME.fetchFrom(params) != null) {
 452             return APP_NAME.fetchFrom(params);
 453         } else {
 454             return MAIN_CLASS.fetchFrom(params);
 455         }
 456     }
 457 
 458     private String getBundleName(Map<String, ? super Object> params) {
 459         //TODO: Check to see what rules/limits are in place for CFBundleName
 460         if (MAC_CF_BUNDLE_NAME.fetchFrom(params) != null) {
 461             String bn = MAC_CF_BUNDLE_NAME.fetchFrom(params);
 462             if (bn.length() > 16) {
 463                 Log.info(MessageFormat.format(I18N.getString("message.bundle-name-too-long-warning"), MAC_CF_BUNDLE_NAME.getID(), bn));
 464             }
 465             return MAC_CF_BUNDLE_NAME.fetchFrom(params);
 466         } else if (APP_NAME.fetchFrom(params) != null) {
 467             return APP_NAME.fetchFrom(params);
 468         } else {
 469             String nm = MAIN_CLASS.fetchFrom(params);
 470             if (nm.length() > 16) {
 471                 nm = nm.substring(0, 16);
 472             }
 473             return nm;
 474         }
 475     }
 476 
 477     private String getBundleIdentifier(Map<String, ? super Object> params) {
 478         //TODO: Check to see what rules/limits are in place for CFBundleIdentifier
 479         return  IDENTIFIER.fetchFrom(params);
 480     }
 481 
 482     private void writeInfoPlist(File file, Map<String, ? super Object> params) throws IOException {
 483         Log.verbose("Preparing Info.plist: "+file.getAbsolutePath());
 484 
 485         //prepare config for exe
 486         //Note: do not need CFBundleDisplayName if we do not support localization
 487         Map<String, String> data = new HashMap<>();
 488         data.put("DEPLOY_ICON_FILE", getConfig_Icon(params).getName());
 489         data.put("DEPLOY_BUNDLE_IDENTIFIER",
 490                 getBundleIdentifier(params));
 491         data.put("DEPLOY_BUNDLE_NAME",
 492                 getBundleName(params));
 493         data.put("DEPLOY_BUNDLE_COPYRIGHT",
 494                 COPYRIGHT.fetchFrom(params) != null ? COPYRIGHT.fetchFrom(params) : "Unknown");
 495         data.put("DEPLOY_LAUNCHER_NAME", getLauncherName(params));
 496         if (MAC_RUNTIME.fetchFrom(params) != null) {
 497             data.put("DEPLOY_JAVA_RUNTIME_NAME",
 498                     MAC_RUNTIME.fetchFrom(params).getBaseDirectory().getName());
 499         } else {
 500             data.put("DEPLOY_JAVA_RUNTIME_NAME", "");
 501         }
 502         data.put("DEPLOY_BUNDLE_SHORT_VERSION",
 503                 VERSION.fetchFrom(params) != null ? VERSION.fetchFrom(params) : "1.0.0");
 504         data.put("DEPLOY_BUNDLE_CATEGORY",
 505                 //TODO parameters should provide set of values for IDEs
 506                 MAC_CATEGORY.validatedFetchFrom(params));
 507 
 508         //TODO NOT THE WAY TODO THIS but good enough for first pass
 509         data.put("DEPLOY_MAIN_JAR_NAME", new BundleParams(params).getMainApplicationJar());
 510 //        data.put("DEPLOY_MAIN_JAR_NAME", MAIN_JAR.fetchFrom(params).toString());
 511 
 512         data.put("DEPLOY_PREFERENCES_ID", PREFERENCES_ID.fetchFrom(params).toLowerCase());
 513 
 514         StringBuilder sb = new StringBuilder();
 515         List<String> jvmOptions = JVM_OPTIONS.fetchFrom(params);
 516 
 517         String newline = ""; //So we don't add unneccessary extra line after last append
 518         for (String o : jvmOptions) {
 519             sb.append(newline).append("    <string>").append(o).append("</string>");
 520             newline = "\n";
 521         }
 522         data.put("DEPLOY_JVM_OPTIONS", sb.toString());
 523 
 524         newline = "";
 525         sb = new StringBuilder();
 526         Map<String, String> overridableJVMOptions = USER_JVM_OPTIONS.fetchFrom(params);
 527         for (Map.Entry<String, String> arg: overridableJVMOptions.entrySet()) {
 528             sb.append(newline);
 529             sb.append("      <key>").append(arg.getKey()).append("</key>\n");
 530             sb.append("      <string>").append(arg.getValue()).append("</string>");
 531             newline = "\n";
 532         }
 533         data.put("DEPLOY_JVM_USER_OPTIONS", sb.toString());
 534 
 535 
 536         //TODO UNLESS we are supporting building for jre7, this is unnecessary
 537 //        if (params.useJavaFXPackaging()) {
 538 //            data.put("DEPLOY_LAUNCHER_CLASS", JAVAFX_LAUNCHER_CLASS);
 539 //        } else {
 540         data.put("DEPLOY_LAUNCHER_CLASS", MAIN_CLASS.fetchFrom(params));
 541 //        }
 542         // This will be an empty string for correctly packaged JavaFX apps
 543         data.put("DEPLOY_APP_CLASSPATH", MAIN_JAR_CLASSPATH.fetchFrom(params));
 544 
 545         //TODO: Add remainder of the classpath
 546 
 547         Writer w = new BufferedWriter(new FileWriter(file));
 548         w.write(preprocessTextResource(
 549                 MAC_BUNDLER_PREFIX + getConfig_InfoPlist(params).getName(),
 550                 "Bundle config file", TEMPLATE_INFO_PLIST, data,
 551                 VERBOSE.fetchFrom(params)));
 552         w.close();
 553 
 554     }
 555 
 556     private void writePkgInfo(File file) throws IOException {
 557 
 558         //hardcoded as it does not seem we need to change it ever
 559         String signature = "????";
 560 
 561         try (Writer out = new BufferedWriter(new FileWriter(file))) {
 562             out.write(OS_TYPE_CODE + signature);
 563             out.flush();
 564         }
 565     }
 566 
 567     //////////////////////////////////////////////////////////////////////////////////
 568     // Implement Bundler
 569     //////////////////////////////////////////////////////////////////////////////////
 570 
 571     @Override
 572     public String getName() {
 573         return "Mac Application Image";
 574     }
 575 
 576     @Override
 577     public String getDescription() {
 578         return "A Directory based image of a mac Application with an optionally co-bundled JRE.  Used as a base for the Installer bundlers";
 579     }
 580 
 581     @Override
 582     public String getID() {
 583         return "mac.app";
 584     }
 585 
 586     @Override
 587     public BundleType getBundleType() {
 588         return BundleType.IMAGE;
 589     }
 590 
 591     @Override
 592     public Collection<BundlerParamInfo<?>> getBundleParameters() {
 593         return getAppBundleParameters();
 594     }
 595 
 596     public static Collection<BundlerParamInfo<?>> getAppBundleParameters() {
 597         return Arrays.asList(
 598                 APP_NAME,
 599                 APP_RESOURCES,
 600                 BUILD_ROOT,
 601                 JVM_OPTIONS,
 602                 MAIN_CLASS,
 603                 MAIN_JAR,
 604                 MAIN_JAR_CLASSPATH,
 605                 PREFERENCES_ID,
 606                 RAW_EXECUTABLE_URL,
 607                 MAC_RUNTIME,
 608                 USE_FX_PACKAGING,
 609                 USER_JVM_OPTIONS,
 610                 VERSION,
 611                 ICON,
 612                 MAC_CATEGORY
 613         );
 614     }
 615 
 616 
 617     @Override
 618     public File execute(Map<String, ? super Object> params, File outputParentDir) {
 619         return doBundle(params, outputParentDir, false);
 620     }
 621 }