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 package com.oracle.tools.packager.mac; 26 27 import com.oracle.tools.packager.AbstractImageBundler; 28 import com.oracle.tools.packager.BundlerParamInfo; 29 import com.oracle.tools.packager.EnumeratedBundlerParam; 30 import com.oracle.tools.packager.JreUtils; 31 import com.oracle.tools.packager.JreUtils.Rule; 32 import com.oracle.tools.packager.StandardBundlerParam; 33 import com.oracle.tools.packager.Log; 34 import com.sun.javafx.tools.packager.bundlers.BundleParams; 35 import com.oracle.tools.packager.ConfigException; 36 import com.oracle.tools.packager.IOUtils; 37 import com.oracle.tools.packager.RelativeFileSet; 38 import com.oracle.tools.packager.UnsupportedPlatformException; 39 40 import java.io.*; 41 import java.math.BigInteger; 42 import java.net.MalformedURLException; 43 import java.net.URL; 44 import java.text.MessageFormat; 45 import java.util.*; 46 47 import static com.oracle.tools.packager.StandardBundlerParam.*; 48 import static com.oracle.tools.packager.mac.MacBaseInstallerBundler.SIGNING_KEYCHAIN; 49 import static com.oracle.tools.packager.mac.MacBaseInstallerBundler.SIGNING_KEY_USER; 50 import static com.oracle.tools.packager.mac.MacBaseInstallerBundler.getPredefinedImage; 51 52 public class MacAppBundler extends AbstractImageBundler { 53 54 private static final ResourceBundle I18N = 55 ResourceBundle.getBundle(MacAppBundler.class.getName()); 56 57 public final static String MAC_BUNDLER_PREFIX = 58 BUNDLER_PREFIX + "macosx" + File.separator; 59 60 private static final String EXECUTABLE_NAME = "JavaAppLauncher"; 61 private final static String LIBRARY_NAME = "libpackager.dylib"; 62 private static final String TEMPLATE_BUNDLE_ICON = "GenericApp.icns"; 63 private static final String OS_TYPE_CODE = "APPL"; 64 private static final String TEMPLATE_INFO_PLIST_LEGACY = "Info.plist.template"; 65 private static final String TEMPLATE_INFO_PLIST_LITE = "Info-lite.plist.template"; 66 67 private static Map<String, String> getMacCategories() { 68 Map<String, String> map = new HashMap<>(); 69 map.put("Business", "public.app-category.business"); 70 map.put("Developer Tools", "public.app-category.developer-tools"); 71 map.put("Education", "public.app-category.education"); 72 map.put("Entertainment", "public.app-category.entertainment"); 73 map.put("Finance", "public.app-category.finance"); 74 map.put("Games", "public.app-category.games"); 75 map.put("Graphics & Design", "public.app-category.graphics-design"); 76 map.put("Healthcare & Fitness", "public.app-category.healthcare-fitness"); 77 map.put("Lifestyle", "public.app-category.lifestyle"); 78 map.put("Medical", "public.app-category.medical"); 79 map.put("Music", "public.app-category.music"); 80 map.put("News", "public.app-category.news"); 81 map.put("Photography", "public.app-category.photography"); 82 map.put("Productivity", "public.app-category.productivity"); 83 map.put("Reference", "public.app-category.reference"); 84 map.put("Social Networking", "public.app-category.social-networking"); 85 map.put("Sports", "public.app-category.sports"); 86 map.put("Travel", "public.app-category.travel"); 87 map.put("Utilities", "public.app-category.utilities"); 88 map.put("Video", "public.app-category.video"); 89 map.put("Weather", "public.app-category.weather"); 90 91 map.put("Action Games", "public.app-category.action-games"); 92 map.put("Adventure Games", "public.app-category.adventure-games"); 93 map.put("Arcade Games", "public.app-category.arcade-games"); 94 map.put("Board Games", "public.app-category.board-games"); 95 map.put("Card Games", "public.app-category.card-games"); 96 map.put("Casino Games", "public.app-category.casino-games"); 97 map.put("Dice Games", "public.app-category.dice-games"); 98 map.put("Educational Games", "public.app-category.educational-games"); 99 map.put("Family Games", "public.app-category.family-games"); 100 map.put("Kids Games", "public.app-category.kids-games"); 101 map.put("Music Games", "public.app-category.music-games"); 102 map.put("Puzzle Games", "public.app-category.puzzle-games"); 103 map.put("Racing Games", "public.app-category.racing-games"); 104 map.put("Role Playing Games", "public.app-category.role-playing-games"); 105 map.put("Simulation Games", "public.app-category.simulation-games"); 106 map.put("Sports Games", "public.app-category.sports-games"); 107 map.put("Strategy Games", "public.app-category.strategy-games"); 108 map.put("Trivia Games", "public.app-category.trivia-games"); 109 map.put("Word Games", "public.app-category.word-games"); 110 111 return map; 112 } 113 114 public static final BundlerParamInfo<Boolean> MAC_CONFIGURE_LAUNCHER_IN_PLIST = 115 new StandardBundlerParam<>( 116 I18N.getString("param.configure-launcher-in-plist"), 117 I18N.getString("param.configure-launcher-in-plist.description"), 118 "mac.configure-launcher-in-plist", 119 Boolean.class, 120 params -> Boolean.FALSE, 121 (s, p) -> Boolean.valueOf(s)); 122 123 public static final EnumeratedBundlerParam<String> MAC_CATEGORY = 124 new EnumeratedBundlerParam<>( 125 I18N.getString("param.category-name"), 126 I18N.getString("param.category-name.description"), 127 "mac.category", 128 String.class, 129 params -> params.containsKey(CATEGORY.getID()) 130 ? CATEGORY.fetchFrom(params) 131 : "Unknown", 132 (s, p) -> s, 133 getMacCategories(), 134 false //strict - for MacStoreBundler this should be strict 135 ); 136 137 public static final BundlerParamInfo<String> MAC_CF_BUNDLE_NAME = 138 new StandardBundlerParam<>( 139 I18N.getString("param.cfbundle-name.name"), 140 I18N.getString("param.cfbundle-name.description"), 141 "mac.CFBundleName", 142 String.class, 143 params -> null, 144 (s, p) -> s); 145 146 public static final BundlerParamInfo<String> MAC_CF_BUNDLE_IDENTIFIER = 147 new StandardBundlerParam<>( 148 I18N.getString("param.cfbundle-identifier.name"), 149 I18N.getString("param.cfbundle-identifier.description"), 150 "mac.CFBundleIdentifier", 151 String.class, 152 IDENTIFIER::fetchFrom, 153 (s, p) -> s); 154 155 public static final BundlerParamInfo<String> MAC_CF_BUNDLE_VERSION = 156 new StandardBundlerParam<>( 157 I18N.getString("param.cfbundle-version.name"), 158 I18N.getString("param.cfbundle-version.description"), 159 "mac.CFBundleVersion", 160 String.class, 161 p -> { 162 String s = VERSION.fetchFrom(p); 163 if (validCFBundleVersion(s)) { 164 return s; 165 } else { 166 return "100"; 167 } 168 }, 169 (s, p) -> s); 170 171 public static final BundlerParamInfo<File> CONFIG_ROOT = new StandardBundlerParam<>( 172 I18N.getString("param.config-root.name"), 173 I18N.getString("param.config-root.description"), 174 "configRoot", 175 File.class, 176 params -> { 177 File configRoot = new File(BUILD_ROOT.fetchFrom(params), "macosx"); 178 configRoot.mkdirs(); 179 return configRoot; 180 }, 181 (s, p) -> new File(s)); 182 183 public static final BundlerParamInfo<URL> RAW_EXECUTABLE_URL = new StandardBundlerParam<>( 184 I18N.getString("param.raw-executable-url.name"), 185 I18N.getString("param.raw-executable-url.description"), 186 "mac.launcher.url", 187 URL.class, 188 params -> MacResources.class.getResource(EXECUTABLE_NAME), 189 (s, p) -> { 190 try { 191 return new URL(s); 192 } catch (MalformedURLException e) { 193 Log.info(e.toString()); 194 return null; 195 } 196 }); 197 198 public static final BundlerParamInfo<String> DEFAULT_ICNS_ICON = new StandardBundlerParam<>( 199 I18N.getString("param.default-icon-icns"), 200 I18N.getString("param.default-icon-icns.description"), 201 ".mac.default.icns", 202 String.class, 203 params -> TEMPLATE_BUNDLE_ICON, 204 (s, p) -> s); 205 206 public static final BundlerParamInfo<Rule[]> MAC_RULES = new StandardBundlerParam<>( 207 "", 208 "", 209 ".mac.runtime.rules", 210 Rule[].class, 211 MacAppBundler::createMacRuntimeRules, 212 (s, p) -> null 213 ); 214 215 public static final BundlerParamInfo<RelativeFileSet> MAC_RUNTIME = new StandardBundlerParam<>( 216 I18N.getString("param.runtime.name"), 217 I18N.getString("param.runtime.description"), 218 BundleParams.PARAM_RUNTIME, 219 RelativeFileSet.class, 220 params -> extractMacRuntime(System.getProperty("java.home"), params), 221 MacAppBundler::extractMacRuntime 222 ); 223 224 public static final BundlerParamInfo<String> DEVELOPER_ID_APP_SIGNING_KEY = new StandardBundlerParam<>( 225 I18N.getString("param.signing-key-developer-id-app.name"), 226 I18N.getString("param.signing-key-developer-id-app.description"), 227 "mac.signing-key-developer-id-app", 228 String.class, 229 params -> MacBaseInstallerBundler.findKey("Developer ID Application: " + SIGNING_KEY_USER.fetchFrom(params), SIGNING_KEYCHAIN.fetchFrom(params), VERBOSE.fetchFrom(params)), 230 (s, p) -> s); 231 232 public static final BundlerParamInfo<String> BUNDLE_ID_SIGNING_PREFIX = new StandardBundlerParam<>( 233 I18N.getString("param.bundle-id-signing-prefix.name"), 234 I18N.getString("param.bundle-id-signing-prefix.description"), 235 "mac.bundle-id-signing-prefix", 236 String.class, 237 params -> IDENTIFIER.fetchFrom(params) + ".", 238 (s, p) -> s); 239 240 public static final BundlerParamInfo<File> ICON_ICNS = new StandardBundlerParam<>( 241 I18N.getString("param.icon-icns.name"), 242 I18N.getString("param.icon-icns.description"), 243 "icon.icns", 244 File.class, 245 params -> { 246 File f = ICON.fetchFrom(params); 247 if (f != null && !f.getName().toLowerCase().endsWith(".icns")) { 248 Log.info(MessageFormat.format(I18N.getString("message.icon-not-icns"), f)); 249 return null; 250 } 251 return f; 252 }, 253 (s, p) -> new File(s)); 254 255 public static RelativeFileSet extractMacRuntime(String base, Map<String, ? super Object> params) { 256 if (base.isEmpty()) { 257 return null; 258 } 259 260 File workingBase = new File(base); 261 workingBase = workingBase.getAbsoluteFile(); 262 try { 263 workingBase = workingBase.getCanonicalFile(); 264 } catch (IOException ignore) { 265 // we tried, workingBase will remain absolute and not canonical. 266 } 267 268 if (workingBase.getName().equals("jre")) { 269 workingBase = workingBase.getParentFile(); 270 } 271 if (workingBase.getName().equals("Home")) { 272 workingBase = workingBase.getParentFile(); 273 } 274 if (workingBase.getName().equals("Contents")) { 275 workingBase = workingBase.getParentFile(); 276 } 277 return JreUtils.extractJreAsRelativeFileSet(workingBase.toString(), 278 MAC_RULES.fetchFrom(params), true); 279 } 280 281 public MacAppBundler() { 282 super(); 283 baseResourceLoader = MacResources.class; 284 } 285 286 @Override 287 protected String getCacheLocation(Map<String, ? super Object> params) { 288 Boolean systemWide = SYSTEM_WIDE.fetchFrom(params); 289 if (systemWide == null || systemWide) { 290 return "/Library/Application Support/" + IDENTIFIER.fetchFrom(params) + "/cache/"; 291 } else { 292 return "$CACHEDIR/"; 293 } 294 } 295 296 297 public static boolean validCFBundleVersion(String v) { 298 // CFBundleVersion (String - iOS, OS X) specifies the build version 299 // number of the bundle, which identifies an iteration (released or 300 // unreleased) of the bundle. The build version number should be a 301 // string comprised of three non-negative, period-separated integers 302 // with the first integer being greater than zero. The string should 303 // only contain numeric (0-9) and period (.) characters. Leading zeros 304 // are truncated from each integer and will be ignored (that is, 305 // 1.02.3 is equivalent to 1.2.3). This key is not localizable. 306 307 if (v == null) { 308 return false; 309 } 310 311 String p[] = v.split("\\."); 312 if (p.length > 3 || p.length < 1) { 313 Log.verbose(I18N.getString("message.version-string-too-many-components")); 314 return false; 315 } 316 317 try { 318 BigInteger n = new BigInteger(p[0]); 319 if (BigInteger.ONE.compareTo(n) > 0) { 320 Log.verbose(I18N.getString("message.version-string-first-number-not-zero")); 321 return false; 322 } 323 if (p.length > 1) { 324 n = new BigInteger(p[1]); 325 if (BigInteger.ZERO.compareTo(n) > 0) { 326 Log.verbose(I18N.getString("message.version-string-no-negative-numbers")); 327 return false; 328 } 329 } 330 if (p.length > 2) { 331 n = new BigInteger(p[2]); 332 if (BigInteger.ZERO.compareTo(n) > 0) { 333 Log.verbose(I18N.getString("message.version-string-no-negative-numbers")); 334 return false; 335 } 336 } 337 } catch (NumberFormatException ne) { 338 Log.verbose(I18N.getString("message.version-string-numbers-only")); 339 Log.verbose(ne); 340 return false; 341 } 342 343 return true; 344 } 345 346 @Override 347 public boolean validate(Map<String, ? super Object> params) throws UnsupportedPlatformException, ConfigException { 348 try { 349 return doValidate(params); 350 } catch (RuntimeException re) { 351 if (re.getCause() instanceof ConfigException) { 352 throw (ConfigException) re.getCause(); 353 } else { 354 throw new ConfigException(re); 355 } 356 } 357 } 358 359 //to be used by chained bundlers, e.g. by EXE bundler to avoid 360 // skipping validation if p.type does not include "image" 361 public boolean doValidate(Map<String, ? super Object> p) throws UnsupportedPlatformException, ConfigException { 362 if (!System.getProperty("os.name").toLowerCase().contains("os x")) { 363 throw new UnsupportedPlatformException(); 364 } 365 366 StandardBundlerParam.validateMainClassInfoFromAppResources(p); 367 368 Map<String, String> userJvmOptions = USER_JVM_OPTIONS.fetchFrom(p); 369 if (userJvmOptions != null) { 370 for (Map.Entry<String, String> entry : userJvmOptions.entrySet()) { 371 if (entry.getValue() == null || entry.getValue().isEmpty()) { 372 throw new ConfigException( 373 MessageFormat.format(I18N.getString("error.empty-user-jvm-option-value"), entry.getKey()), 374 I18N.getString("error.empty-user-jvm-option-value.advice")); 375 } 376 } 377 } 378 379 if (getPredefinedImage(p) != null) { 380 return true; 381 } 382 383 if (MAIN_JAR.fetchFrom(p) == null) { 384 throw new ConfigException( 385 I18N.getString("error.no-application-jar"), 386 I18N.getString("error.no-application-jar.advice")); 387 } 388 389 //validate required inputs 390 testRuntime(MAC_RUNTIME.fetchFrom(p), new String[] { 391 "Contents/Home/(jre/)?lib/[^/]+/libjvm.dylib", // most reliable 392 "Contents/Home/(jre/)?lib/rt.jar", // fallback canary for JDK 8 393 }); 394 if (USE_FX_PACKAGING.fetchFrom(p)) { 395 testRuntime(MAC_RUNTIME.fetchFrom(p), new String[] {"Contents/Home/(jre/)?lib/ext/jfxrt.jar", "Contents/Home/(jre/)?lib/jfxrt.jar"}); 396 } 397 398 // validate short version 399 if (!validCFBundleVersion(MAC_CF_BUNDLE_VERSION.fetchFrom(p))) { 400 throw new ConfigException( 401 I18N.getString("error.invalid-cfbundle-version"), 402 I18N.getString("error.invalid-cfbundle-version.advice")); 403 } 404 405 // reject explicitly set sign to true and no valid signature key 406 if (Optional.ofNullable(SIGN_BUNDLE.fetchFrom(p)).orElse(Boolean.FALSE)) { 407 String signingIdentity = DEVELOPER_ID_APP_SIGNING_KEY.fetchFrom(p); 408 if (signingIdentity == null) { 409 throw new ConfigException( 410 I18N.getString("error.explicit-sign-no-cert"), 411 I18N.getString("error.explicit-sign-no-cert.advice")); 412 } 413 } 414 415 return true; 416 } 417 418 private File getConfig_InfoPlist(Map<String, ? super Object> params) { 419 return new File(CONFIG_ROOT.fetchFrom(params), "Info.plist"); 420 } 421 422 private File getConfig_Icon(Map<String, ? super Object> params) { 423 return new File(CONFIG_ROOT.fetchFrom(params), APP_NAME.fetchFrom(params) + ".icns"); 424 } 425 426 private void prepareConfigFiles(Map<String, ? super Object> params) throws IOException { 427 File infoPlistFile = getConfig_InfoPlist(params); 428 infoPlistFile.createNewFile(); 429 writeInfoPlist(infoPlistFile, params); 430 431 // Copy icon to Resources folder 432 prepareIcon(params); 433 } 434 435 public File doBundle(Map<String, ? super Object> p, File outputDirectory, boolean dependentTask) { 436 File rootDirectory = null; 437 Map<String, ? super Object> originalParams = new HashMap<>(p); 438 439 if (!outputDirectory.isDirectory() && !outputDirectory.mkdirs()) { 440 throw new RuntimeException(MessageFormat.format(I18N.getString("error.cannot-create-output-dir"), outputDirectory.getAbsolutePath())); 441 } 442 if (!outputDirectory.canWrite()) { 443 throw new RuntimeException(MessageFormat.format(I18N.getString("error.cannot-write-to-output-dir"), outputDirectory.getAbsolutePath())); 444 } 445 446 try { 447 final File predefinedImage = getPredefinedImage(p); 448 if (predefinedImage != null) { 449 return predefinedImage; 450 } 451 452 // side effect is temp dir is created if not specified 453 BUILD_ROOT.fetchFrom(p); 454 455 //prepare config resources (we will copy them to the bundle later) 456 // NB: explicitly saving them to simplify customization 457 prepareConfigFiles(p); 458 459 // Create directory structure 460 rootDirectory = new File(outputDirectory, APP_NAME.fetchFrom(p) + ".app"); 461 IOUtils.deleteRecursive(rootDirectory); 462 rootDirectory.mkdirs(); 463 464 if (!dependentTask) { 465 Log.info(MessageFormat.format(I18N.getString("message.creating-app-bundle"), rootDirectory.getAbsolutePath())); 466 } 467 468 File contentsDirectory = new File(rootDirectory, "Contents"); 469 contentsDirectory.mkdirs(); 470 471 File macOSDirectory = new File(contentsDirectory, "MacOS"); 472 macOSDirectory.mkdirs(); 473 474 File javaDirectory = new File(contentsDirectory, "Java"); 475 javaDirectory.mkdirs(); 476 477 File plugInsDirectory = new File(contentsDirectory, "PlugIns"); 478 479 File resourcesDirectory = new File(contentsDirectory, "Resources"); 480 resourcesDirectory.mkdirs(); 481 482 // Generate PkgInfo 483 File pkgInfoFile = new File(contentsDirectory, "PkgInfo"); 484 pkgInfoFile.createNewFile(); 485 writePkgInfo(pkgInfoFile); 486 487 // Copy executable to MacOS folder 488 File executableFile = new File(macOSDirectory, getLauncherName(p)); 489 IOUtils.copyFromURL( 490 RAW_EXECUTABLE_URL.fetchFrom(p), 491 executableFile); 492 493 // Copy library to the MacOS folder 494 IOUtils.copyFromURL( 495 MacResources.class.getResource(LIBRARY_NAME), 496 new File(macOSDirectory, LIBRARY_NAME)); 497 498 // maybe generate launcher config 499 if (!MAC_CONFIGURE_LAUNCHER_IN_PLIST.fetchFrom(p)) { 500 if (LAUNCHER_CFG_FORMAT.fetchFrom(p).equals(CFG_FORMAT_PROPERTIES)) { 501 writeCfgFile(p, rootDirectory); 502 } else { 503 writeCfgFile(p, new File(rootDirectory, getLauncherCfgName(p)), "$APPDIR/PlugIns/Java.runtime"); 504 } 505 } 506 507 executableFile.setExecutable(true, false); 508 509 // Copy runtime to PlugIns folder 510 copyRuntime(plugInsDirectory, p); 511 512 // Copy class path entries to Java folder 513 copyClassPathEntries(javaDirectory, p); 514 515 //TODO: Need to support adding native libraries. 516 // Copy library path entries to MacOS folder 517 //copyLibraryPathEntries(macOSDirectory); 518 519 /*********** Take care of "config" files *******/ 520 // Copy icon to Resources folder 521 IOUtils.copyFile(getConfig_Icon(p), 522 new File(resourcesDirectory, getConfig_Icon(p).getName())); 523 524 // copy file association icons 525 for (Map<String, ? super Object> fa : FILE_ASSOCIATIONS.fetchFrom(p)) { 526 File f = FA_ICON.fetchFrom(fa); 527 if (f != null && f.exists()) { 528 IOUtils.copyFile(f, 529 new File(resourcesDirectory, f.getName())); 530 } 531 } 532 533 // Generate Info.plist 534 IOUtils.copyFile(getConfig_InfoPlist(p), 535 new File(contentsDirectory, "Info.plist")); 536 537 // create the secondary launchers, if any 538 List<Map<String, ? super Object>> entryPoints = StandardBundlerParam.SECONDARY_LAUNCHERS.fetchFrom(p); 539 for (Map<String, ? super Object> entryPoint : entryPoints) { 540 Map<String, ? super Object> tmp = new HashMap<>(originalParams); 541 tmp.putAll(entryPoint); 542 createLauncherForEntryPoint(tmp, rootDirectory); 543 } 544 545 // maybe sign 546 if (Optional.ofNullable(SIGN_BUNDLE.fetchFrom(p)).orElse(Boolean.TRUE)) { 547 String signingIdentity = DEVELOPER_ID_APP_SIGNING_KEY.fetchFrom(p); 548 if (signingIdentity != null) { 549 MacBaseInstallerBundler.signAppBundle(p, rootDirectory, signingIdentity, BUNDLE_ID_SIGNING_PREFIX.fetchFrom(p)); 550 } 551 } 552 } catch (IOException ex) { 553 Log.info(ex.toString()); 554 Log.verbose(ex); 555 return null; 556 } finally { 557 if (!VERBOSE.fetchFrom(p)) { 558 //cleanup 559 cleanupConfigFiles(p); 560 } else { 561 Log.info(MessageFormat.format(I18N.getString("message.config-save-location"), CONFIG_ROOT.fetchFrom(p).getAbsolutePath())); 562 } 563 } 564 return rootDirectory; 565 } 566 567 public void cleanupConfigFiles(Map<String, ? super Object> params) { 568 //Since building the app can be bypassed, make sure configRoot was set 569 if (CONFIG_ROOT.fetchFrom(params) != null) { 570 getConfig_Icon(params).delete(); 571 getConfig_InfoPlist(params).delete(); 572 } 573 } 574 575 private void copyClassPathEntries(File javaDirectory, Map<String, ? super Object> params) throws IOException { 576 List<RelativeFileSet> resourcesList = APP_RESOURCES_LIST.fetchFrom(params); 577 if (resourcesList == null) { 578 throw new RuntimeException(I18N.getString("message.null-classpath")); 579 } 580 581 for (RelativeFileSet classPath : resourcesList) { 582 File srcdir = classPath.getBaseDirectory(); 583 for (String fname : classPath.getIncludedFiles()) { 584 IOUtils.copyFile( 585 new File(srcdir, fname), new File(javaDirectory, fname)); 586 } 587 } 588 } 589 590 private void copyRuntime(File plugInsDirectory, Map<String, ? super Object> params) throws IOException { 591 RelativeFileSet runTime = MAC_RUNTIME.fetchFrom(params); 592 if (runTime == null) { 593 //request to use system runtime => do not bundle 594 return; 595 } 596 plugInsDirectory.mkdirs(); 597 598 File srcdir = runTime.getBaseDirectory(); 599 // the name in .../Contents/PlugIns/ must have a dot to be verified 600 // properly by the Mac App Store. 601 File destDir = new File(plugInsDirectory, "Java.runtime"); 602 Set<String> filesToCopy = runTime.getIncludedFiles(); 603 604 for (String fname : filesToCopy) { 605 IOUtils.copyFile( 606 new File(srcdir, fname), new File(destDir, fname)); 607 } 608 } 609 610 private void prepareIcon(Map<String, ? super Object> params) throws IOException { 611 File icon = ICON_ICNS.fetchFrom(params); 612 if (icon == null || !icon.exists()) { 613 fetchResource(MAC_BUNDLER_PREFIX+ APP_NAME.fetchFrom(params) +".icns", 614 "icon", 615 DEFAULT_ICNS_ICON.fetchFrom(params), 616 getConfig_Icon(params), 617 VERBOSE.fetchFrom(params), 618 DROP_IN_RESOURCES_ROOT.fetchFrom(params)); 619 } else { 620 fetchResource(MAC_BUNDLER_PREFIX+ APP_NAME.fetchFrom(params) +".icns", 621 "icon", 622 icon, 623 getConfig_Icon(params), 624 VERBOSE.fetchFrom(params), 625 DROP_IN_RESOURCES_ROOT.fetchFrom(params)); 626 } 627 } 628 629 private String getLauncherName(Map<String, ? super Object> params) { 630 if (APP_NAME.fetchFrom(params) != null) { 631 return APP_NAME.fetchFrom(params); 632 } else { 633 return MAIN_CLASS.fetchFrom(params); 634 } 635 } 636 637 private String getBundleName(Map<String, ? super Object> params) { 638 //TODO: Check to see what rules/limits are in place for CFBundleName 639 if (MAC_CF_BUNDLE_NAME.fetchFrom(params) != null) { 640 String bn = MAC_CF_BUNDLE_NAME.fetchFrom(params); 641 if (bn.length() > 16) { 642 Log.info(MessageFormat.format(I18N.getString("message.bundle-name-too-long-warning"), MAC_CF_BUNDLE_NAME.getID(), bn)); 643 } 644 return MAC_CF_BUNDLE_NAME.fetchFrom(params); 645 } else if (APP_NAME.fetchFrom(params) != null) { 646 return APP_NAME.fetchFrom(params); 647 } else { 648 String nm = MAIN_CLASS.fetchFrom(params); 649 if (nm.length() > 16) { 650 nm = nm.substring(0, 16); 651 } 652 return nm; 653 } 654 } 655 656 private void writeInfoPlist(File file, Map<String, ? super Object> params) throws IOException { 657 Log.verbose(MessageFormat.format(I18N.getString("message.preparing-info-plist"), file.getAbsolutePath())); 658 659 //prepare config for exe 660 //Note: do not need CFBundleDisplayName if we do not support localization 661 Map<String, String> data = new HashMap<>(); 662 data.put("DEPLOY_ICON_FILE", getConfig_Icon(params).getName()); 663 data.put("DEPLOY_BUNDLE_IDENTIFIER", 664 MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params)); 665 data.put("DEPLOY_BUNDLE_NAME", 666 getBundleName(params)); 667 data.put("DEPLOY_BUNDLE_COPYRIGHT", 668 COPYRIGHT.fetchFrom(params) != null ? COPYRIGHT.fetchFrom(params) : "Unknown"); 669 data.put("DEPLOY_LAUNCHER_NAME", getLauncherName(params)); 670 if (MAC_RUNTIME.fetchFrom(params) != null) { 671 data.put("DEPLOY_JAVA_RUNTIME_NAME", "$APPDIR/PlugIns/Java.runtime"); 672 } else { 673 data.put("DEPLOY_JAVA_RUNTIME_NAME", ""); 674 } 675 data.put("DEPLOY_BUNDLE_SHORT_VERSION", 676 VERSION.fetchFrom(params) != null ? VERSION.fetchFrom(params) : "1.0.0"); 677 data.put("DEPLOY_BUNDLE_CFBUNDLE_VERSION", 678 MAC_CF_BUNDLE_VERSION.fetchFrom(params) != null ? MAC_CF_BUNDLE_VERSION.fetchFrom(params) : "100"); 679 data.put("DEPLOY_BUNDLE_CATEGORY", 680 //TODO parameters should provide set of values for IDEs 681 MAC_CATEGORY.validatedFetchFrom(params)); 682 683 data.put("DEPLOY_MAIN_JAR_NAME", MAIN_JAR.fetchFrom(params).getIncludedFiles().iterator().next()); 684 685 data.put("DEPLOY_PREFERENCES_ID", PREFERENCES_ID.fetchFrom(params).toLowerCase()); 686 687 StringBuilder sb = new StringBuilder(); 688 List<String> jvmOptions = JVM_OPTIONS.fetchFrom(params); 689 690 String newline = ""; //So we don't add unneccessary extra line after last append 691 for (String o : jvmOptions) { 692 sb.append(newline).append(" <string>").append(o).append("</string>"); 693 newline = "\n"; 694 } 695 696 Map<String, String> jvmProps = JVM_PROPERTIES.fetchFrom(params); 697 for (Map.Entry<String, String> entry : jvmProps.entrySet()) { 698 sb.append(newline) 699 .append(" <string>-D") 700 .append(entry.getKey()) 701 .append("=") 702 .append(entry.getValue()) 703 .append("</string>"); 704 newline = "\n"; 705 } 706 707 String preloader = PRELOADER_CLASS.fetchFrom(params); 708 if (preloader != null) { 709 sb.append(newline) 710 .append(" <string>-Djavafx.preloader=") 711 .append(preloader) 712 .append("</string>"); 713 //newline = "\n"; 714 } 715 716 data.put("DEPLOY_JVM_OPTIONS", sb.toString()); 717 718 sb = new StringBuilder(); 719 List<String> args = ARGUMENTS.fetchFrom(params); 720 newline = ""; //So we don't add unneccessary extra line after last append 721 for (String o : args) { 722 sb.append(newline).append(" <string>").append(o).append("</string>"); 723 newline = "\n"; 724 } 725 data.put("DEPLOY_ARGUMENTS", sb.toString()); 726 727 newline = ""; 728 sb = new StringBuilder(); 729 Map<String, String> overridableJVMOptions = USER_JVM_OPTIONS.fetchFrom(params); 730 for (Map.Entry<String, String> arg: overridableJVMOptions.entrySet()) { 731 sb.append(newline) 732 .append(" <key>").append(arg.getKey()).append("</key>\n") 733 .append(" <string>").append(arg.getValue()).append("</string>"); 734 newline = "\n"; 735 } 736 data.put("DEPLOY_JVM_USER_OPTIONS", sb.toString()); 737 738 739 data.put("DEPLOY_LAUNCHER_CLASS", MAIN_CLASS.fetchFrom(params)); 740 741 StringBuilder macroedPath = new StringBuilder(); 742 for (String s : CLASSPATH.fetchFrom(params).split("[ ;:]+")) { 743 macroedPath.append(s); 744 macroedPath.append(":"); 745 } 746 macroedPath.deleteCharAt(macroedPath.length() - 1); 747 748 data.put("DEPLOY_APP_CLASSPATH", macroedPath.toString()); 749 750 //TODO: Add remainder of the classpath 751 752 StringBuilder bundleDocumentTypes = new StringBuilder(); 753 StringBuilder exportedTypes = new StringBuilder(); 754 for (Map<String, ? super Object> fileAssociation : FILE_ASSOCIATIONS.fetchFrom(params)) { 755 756 List<String> extensions = FA_EXTENSIONS.fetchFrom(fileAssociation); 757 758 if (extensions == null) { 759 Log.info(I18N.getString("message.creating-association-with-null-extension")); 760 } 761 762 List<String> mimeTypes = FA_CONTENT_TYPE.fetchFrom(fileAssociation); 763 String itemContentType = MAC_CF_BUNDLE_IDENTIFIER.fetchFrom(params) + "." + ((extensions == null || extensions.isEmpty()) 764 ? "mime" 765 : extensions.get(0)); 766 String description = FA_DESCRIPTION.fetchFrom(fileAssociation); 767 File icon = FA_ICON.fetchFrom(fileAssociation); //TODO FA_ICON_ICNS 768 769 bundleDocumentTypes.append(" <dict>\n") 770 .append(" <key>LSItemContentTypes</key>\n") 771 .append(" <array>\n") 772 .append(" <string>") 773 .append(itemContentType) 774 .append("</string>\n") 775 .append(" </array>\n") 776 .append("\n") 777 .append(" <key>CFBundleTypeName</key>\n") 778 .append(" <string>") 779 .append(description) 780 .append("</string>\n") 781 .append("\n") 782 .append(" <key>LSHandlerRank</key>\n") 783 .append(" <string>Owner</string>\n") //TODO make a bundler arg 784 .append("\n") 785 .append(" <key>CFBundleTypeRole</key>\n") 786 .append(" <string>Editor</string>\n") // TODO make a bundler arg 787 .append("\n") 788 .append(" <key>LSIsAppleDefaultForType</key>\n") 789 .append(" <true/>\n") // TODO make a bundler arg 790 .append("\n"); 791 792 if (icon != null && icon.exists()) { 793 //? 794 bundleDocumentTypes.append(" <key>CFBundleTypeIconFile</key>\n") 795 .append(" <string>") 796 .append(icon.getName()) 797 .append("</string>\n"); 798 } 799 bundleDocumentTypes.append(" </dict>\n"); 800 801 exportedTypes.append(" <dict>\n") 802 .append(" <key>UTTypeIdentifier</key>\n") 803 .append(" <string>") 804 .append(itemContentType) 805 .append("</string>\n") 806 .append("\n") 807 .append(" <key>UTTypeDescription</key>\n") 808 .append(" <string>") 809 .append(description) 810 .append("</string>\n") 811 .append(" <key>UTTypeConformsTo</key>\n") 812 .append(" <array>\n") 813 .append(" <string>public.data</string>\n") //TODO expose this? 814 .append(" </array>\n") 815 .append("\n"); 816 817 if (icon != null && icon.exists()) { 818 exportedTypes.append(" <key>UTTypeIconFile</key>\n") 819 .append(" <string>") 820 .append(icon.getName()) 821 .append("</string>\n") 822 .append("\n"); 823 } 824 825 exportedTypes.append("\n") 826 .append(" <key>UTTypeTagSpecification</key>\n") 827 .append(" <dict>\n") 828 //TODO expose via param? .append(" <key>com.apple.ostype</key>\n"); 829 //TODO expose via param? .append(" <string>ABCD</string>\n") 830 .append("\n"); 831 832 if (extensions != null && !extensions.isEmpty()) { 833 exportedTypes.append(" <key>public.filename-extension</key>\n") 834 .append(" <array>\n"); 835 836 for (String ext : extensions) { 837 exportedTypes.append(" <string>") 838 .append(ext) 839 .append("</string>\n"); 840 } 841 exportedTypes.append(" </array>\n"); 842 } 843 if (mimeTypes != null && !mimeTypes.isEmpty()) { 844 exportedTypes.append(" <key>public.mime-type</key>\n") 845 .append(" <array>\n"); 846 847 for (String mime : mimeTypes) { 848 exportedTypes.append(" <string>") 849 .append(mime) 850 .append("</string>\n"); 851 } 852 exportedTypes.append(" </array>\n"); 853 } 854 exportedTypes.append(" </dict>\n") 855 .append(" </dict>\n"); 856 } 857 String associationData; 858 if (bundleDocumentTypes.length() > 0) { 859 associationData = "\n <key>CFBundleDocumentTypes</key>\n <array>\n" 860 + bundleDocumentTypes.toString() 861 + " </array>\n\n <key>UTExportedTypeDeclarations</key>\n <array>\n" 862 + exportedTypes.toString() 863 + " </array>\n"; 864 } else { 865 associationData = ""; 866 } 867 data.put("DEPLOY_FILE_ASSOCIATIONS", associationData); 868 869 870 Writer w = new BufferedWriter(new FileWriter(file)); 871 w.write(preprocessTextResource( 872 MAC_BUNDLER_PREFIX + getConfig_InfoPlist(params).getName(), 873 I18N.getString("resource.bundle-config-file"), 874 MAC_CONFIGURE_LAUNCHER_IN_PLIST.fetchFrom(params) 875 ? TEMPLATE_INFO_PLIST_LEGACY 876 : TEMPLATE_INFO_PLIST_LITE, 877 data, VERBOSE.fetchFrom(params), 878 DROP_IN_RESOURCES_ROOT.fetchFrom(params))); 879 w.close(); 880 881 } 882 883 private void writePkgInfo(File file) throws IOException { 884 885 //hardcoded as it does not seem we need to change it ever 886 String signature = "????"; 887 888 try (Writer out = new BufferedWriter(new FileWriter(file))) { 889 out.write(OS_TYPE_CODE + signature); 890 out.flush(); 891 } 892 } 893 894 public static Rule[] createMacRuntimeRules(Map<String, ? super Object> params) { 895 if (!System.getProperty("os.name").toLowerCase().contains("os x")) { 896 // we will never get a sensible answer unless we are running on OSX, 897 // so quit now and return null indicating 'no sensible value' 898 return null; 899 } 900 901 //Subsetting of JRE is restricted. 902 //JRE README defines what is allowed to strip: 903 // http://www.oracle.com/technetwork/java/javase/jre-8-readme-2095710.html 904 // 905 906 List<Rule> rules = new ArrayList<>(); 907 908 File baseDir; 909 910 if (params.containsKey(MAC_RUNTIME.getID())) { 911 Object o = params.get(MAC_RUNTIME.getID()); 912 if (o instanceof RelativeFileSet) { 913 914 baseDir = ((RelativeFileSet)o).getBaseDirectory(); 915 } else { 916 baseDir = new File(o.toString()); 917 } 918 } else { 919 baseDir = new File(System.getProperty("java.home")); 920 } 921 922 // we accept either pointing at the directories typically installed at: 923 // /Libraries/Java/JavaVirtualMachine/jdk1.8.0_40/ 924 // * . 925 // * Contents/Home 926 // * Contents/Home/jre 927 // /Library/Internet\ Plug-Ins/JavaAppletPlugin.plugin/ 928 // * . 929 // * /Contents/Home 930 // version may change, and if we don't detect any Contents/Home or Contents/Home/jre we will 931 // presume we are at a root. 932 933 if (!baseDir.exists()) { 934 throw new RuntimeException(I18N.getString("error.non-existent-runtime"), 935 new ConfigException(I18N.getString("error.non-existent-runtime"), 936 I18N.getString("error.non-existent-runtime.advice"))); 937 } 938 939 boolean isJRE; 940 boolean isJDK; 941 942 try { 943 String path = baseDir.getCanonicalPath(); 944 if (path.endsWith("/Contents/Home/jre")) { 945 baseDir = baseDir.getParentFile().getParentFile().getParentFile(); 946 } else if (path.endsWith("/Contents/Home")) { 947 baseDir = baseDir.getParentFile().getParentFile(); 948 } 949 950 isJRE = new File(baseDir, "Contents/Home/lib/jli/libjli.dylib").exists(); 951 isJDK = new File(baseDir, "Contents/Home/jre/lib/jli/libjli.dylib").exists(); 952 953 } catch (IOException e) { 954 throw new RuntimeException(e); 955 } 956 957 if (!(isJRE || isJDK)) { 958 throw new RuntimeException(I18N.getString("error.cannot-detect-runtime-in-directory"), 959 new ConfigException(I18N.getString("error.cannot-detect-runtime-in-directory"), 960 I18N.getString("error.cannot-detect-runtime-in-directory.advice"))); 961 } 962 963 // we need the Info.plist for signing 964 rules.add(Rule.suffix("/contents/info.plist")); 965 966 // Strip some JRE specific stuff 967 if (isJRE) { 968 rules.add(Rule.suffixNeg("/contents/disabled.plist")); 969 rules.add(Rule.suffixNeg("/contents/enabled.plist")); 970 rules.add(Rule.substrNeg("/contents/frameworks/")); 971 } 972 973 // strip out command line tools 974 rules.add(Rule.suffixNeg("home/bin")); 975 if (isJDK) { 976 rules.add(Rule.suffixNeg("home/jre/bin")); 977 } 978 979 // strip out JRE stuff 980 if (isJRE) { 981 // update helper 982 rules.add(Rule.suffixNeg("resources")); 983 // interfacebuilder files 984 rules.add(Rule.suffixNeg("lib/nibs")); 985 // browser integration 986 rules.add(Rule.suffixNeg("lib/libnpjp2.dylib")); 987 // java webstart 988 rules.add(Rule.suffixNeg("lib/security/javaws.policy")); 989 rules.add(Rule.suffixNeg("lib/shortcuts")); 990 991 // general deploy libraries 992 rules.add(Rule.suffixNeg("lib/deploy")); 993 rules.add(Rule.suffixNeg("lib/deploy.jar")); 994 rules.add(Rule.suffixNeg("lib/javaws.jar")); 995 rules.add(Rule.suffixNeg("lib/libdeploy.dylib")); 996 rules.add(Rule.suffixNeg("lib/plugin.jar")); 997 } 998 999 // strip out man pages 1000 rules.add(Rule.suffixNeg("home/man")); 1001 1002 // this is the build hashes, strip or keep? 1003 //rules.add(Rule.suffixNeg("home/release")); 1004 1005 // strip out JDK stuff like JavaDB, JNI Headers, etc 1006 if (isJDK) { 1007 rules.add(Rule.suffixNeg("home/db")); 1008 rules.add(Rule.suffixNeg("home/demo")); 1009 rules.add(Rule.suffixNeg("home/include")); 1010 rules.add(Rule.suffixNeg("home/lib")); 1011 rules.add(Rule.suffixNeg("home/sample")); 1012 rules.add(Rule.suffixNeg("home/src.zip")); 1013 rules.add(Rule.suffixNeg("home/javafx-src.zip")); 1014 } 1015 1016 //"home/rt" is not part of the official builds 1017 // but we may be creating this symlink to make older NB projects 1018 // happy. Make sure to not include it into final artifact 1019 rules.add(Rule.suffixNeg("home/rt")); 1020 1021 //rules.add(Rule.suffixNeg("jre/lib/ext")); //need some of jars there for https to work 1022 1023 // strip out flight recorder 1024 rules.add(Rule.suffixNeg("lib/jfr.jar")); 1025 1026 return rules.toArray(new Rule[rules.size()]); 1027 } 1028 1029 ////////////////////////////////////////////////////////////////////////////////// 1030 // Implement Bundler 1031 ////////////////////////////////////////////////////////////////////////////////// 1032 1033 @Override 1034 public String getName() { 1035 return I18N.getString("bundler.name"); 1036 } 1037 1038 @Override 1039 public String getDescription() { 1040 return I18N.getString("bundler.description"); 1041 } 1042 1043 @Override 1044 public String getID() { 1045 return "mac.app"; 1046 } 1047 1048 @Override 1049 public String getBundleType() { 1050 return "IMAGE"; 1051 } 1052 1053 @Override 1054 public Collection<BundlerParamInfo<?>> getBundleParameters() { 1055 return getAppBundleParameters(); 1056 } 1057 1058 public static Collection<BundlerParamInfo<?>> getAppBundleParameters() { 1059 return Arrays.asList( 1060 APP_NAME, 1061 APP_RESOURCES, 1062 // APP_RESOURCES_LIST, // ?? 1063 ARGUMENTS, 1064 BUNDLE_ID_SIGNING_PREFIX, 1065 CLASSPATH, 1066 DEVELOPER_ID_APP_SIGNING_KEY, 1067 ICON_ICNS, 1068 JVM_OPTIONS, 1069 JVM_PROPERTIES, 1070 MAC_CATEGORY, 1071 MAC_CF_BUNDLE_IDENTIFIER, 1072 MAC_CF_BUNDLE_NAME, 1073 MAC_CF_BUNDLE_VERSION, 1074 MAC_RUNTIME, 1075 MAIN_CLASS, 1076 MAIN_JAR, 1077 PREFERENCES_ID, 1078 PRELOADER_CLASS, 1079 SIGNING_KEYCHAIN, 1080 USER_JVM_OPTIONS, 1081 VERSION 1082 ); 1083 } 1084 1085 1086 @Override 1087 public File execute(Map<String, ? super Object> params, File outputParentDir) { 1088 return doBundle(params, outputParentDir, false); 1089 } 1090 1091 private void createLauncherForEntryPoint(Map<String, ? super Object> p, File rootDirectory) throws IOException { 1092 prepareConfigFiles(p); 1093 1094 if (LAUNCHER_CFG_FORMAT.fetchFrom(p).equals(CFG_FORMAT_PROPERTIES)) { 1095 writeCfgFile(p, rootDirectory); 1096 } else { 1097 writeCfgFile(p, new File(rootDirectory, getLauncherCfgName(p)), "$APPDIR/PlugIns/Java.runtime"); 1098 } 1099 1100 // Copy executable root folder 1101 File executableFile = new File(rootDirectory, "Contents/MacOS/" + getLauncherName(p)); 1102 IOUtils.copyFromURL( 1103 RAW_EXECUTABLE_URL.fetchFrom(p), 1104 executableFile); 1105 executableFile.setExecutable(true, false); 1106 1107 } 1108 1109 public static String getLauncherCfgName(Map<String, ? super Object> p) { 1110 return "Contents/Java/" + APP_NAME.fetchFrom(p) +".cfg"; 1111 } 1112 1113 private void writeCfgFile(Map<String, ? super Object> params, File rootDir) throws FileNotFoundException { 1114 File pkgInfoFile = new File(rootDir, getLauncherCfgName(params)); 1115 1116 pkgInfoFile.delete(); 1117 1118 PrintStream out = new PrintStream(pkgInfoFile); 1119 if (MAC_RUNTIME.fetchFrom(params) == null) { 1120 out.println("app.runtime="); 1121 } else { 1122 out.println("app.runtime=$APPDIR/PlugIns/Java.runtime"); 1123 } 1124 out.println("app.mainjar=" + MAIN_JAR.fetchFrom(params).getIncludedFiles().iterator().next()); 1125 out.println("app.version=" + VERSION.fetchFrom(params)); 1126 //for future AU support (to be able to find app in the registry) 1127 out.println("app.id=" + IDENTIFIER.fetchFrom(params)); 1128 out.println("app.preferences.id=" + PREFERENCES_ID.fetchFrom(params)); 1129 out.println("app.identifier=" + IDENTIFIER.fetchFrom(params)); 1130 1131 out.println("app.mainclass=" + 1132 MAIN_CLASS.fetchFrom(params).replaceAll("\\.", "/")); 1133 out.println("app.classpath=" + CLASSPATH.fetchFrom(params)); 1134 1135 List<String> jvmargs = JVM_OPTIONS.fetchFrom(params); 1136 int idx = 1; 1137 for (String a : jvmargs) { 1138 out.println("jvmarg."+idx+"="+a); 1139 idx++; 1140 } 1141 Map<String, String> jvmProps = JVM_PROPERTIES.fetchFrom(params); 1142 for (Map.Entry<String, String> entry : jvmProps.entrySet()) { 1143 out.println("jvmarg."+idx+"=-D"+entry.getKey()+"="+entry.getValue()); 1144 idx++; 1145 } 1146 1147 String preloader = PRELOADER_CLASS.fetchFrom(params); 1148 if (preloader != null) { 1149 out.println("jvmarg."+idx+"=-Djavafx.preloader="+preloader); 1150 } 1151 1152 Map<String, String> overridableJVMOptions = USER_JVM_OPTIONS.fetchFrom(params); 1153 idx = 1; 1154 for (Map.Entry<String, String> arg: overridableJVMOptions.entrySet()) { 1155 if (arg.getKey() == null || arg.getValue() == null) { 1156 Log.info(I18N.getString("message.jvm-user-arg-is-null")); 1157 } 1158 else { 1159 out.println("jvmuserarg."+idx+".name="+arg.getKey()); 1160 out.println("jvmuserarg."+idx+".value="+arg.getValue()); 1161 } 1162 idx++; 1163 } 1164 1165 // add command line args 1166 List<String> args = ARGUMENTS.fetchFrom(params); 1167 idx = 1; 1168 for (String a : args) { 1169 out.println("arg."+idx+"="+a); 1170 idx++; 1171 } 1172 1173 out.close(); 1174 } 1175 1176 }