1 /*
   2  * Copyright (c) 2014, 2016, 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.oracle.tools.packager;
  27 
  28 import jdk.packager.internal.JLinkBundlerHelper;
  29 
  30 import com.sun.javafx.tools.packager.bundlers.BundleParams;
  31 
  32 import java.io.File;
  33 import java.io.IOException;
  34 import java.io.StringReader;
  35 import java.nio.file.Files;
  36 import java.nio.file.Path;
  37 import java.nio.file.Paths;
  38 import java.text.MessageFormat;
  39 import java.util.ArrayList;
  40 import java.util.Arrays;
  41 import java.util.Collections;
  42 import java.util.Date;
  43 import java.util.HashMap;
  44 import java.util.HashSet;
  45 import java.util.LinkedHashSet;
  46 import java.util.List;
  47 import java.util.Map;
  48 import java.util.Optional;
  49 import java.util.Properties;
  50 import java.util.ResourceBundle;
  51 import java.util.Set;
  52 import java.util.function.BiFunction;
  53 import java.util.function.Function;
  54 import java.util.jar.Attributes;
  55 import java.util.jar.JarFile;
  56 import java.util.jar.Manifest;
  57 import java.util.regex.Pattern;
  58 import java.util.stream.Collectors;
  59 import static jdk.packager.internal.JLinkBundlerHelper.findPathOfModule;
  60 import static jdk.packager.internal.JLinkBundlerHelper.listOfPathToString;
  61 
  62 public class StandardBundlerParam<T> extends BundlerParamInfo<T> {
  63 
  64     public static final String MANIFEST_JAVAFX_MAIN ="JavaFX-Application-Class";
  65     public static final String MANIFEST_PRELOADER = "JavaFX-Preloader-Class";
  66 
  67     private static final ResourceBundle I18N =
  68             ResourceBundle.getBundle(StandardBundlerParam.class.getName());
  69 
  70     public StandardBundlerParam(String name, String description, String id,
  71                                 Class<T> valueType,
  72                                 Function<Map<String, ? super Object>, T> defaultValueFunction,
  73                                 BiFunction<String, Map<String, ? super Object>, T> stringConverter) {
  74         this.name = name;
  75         this.description = description;
  76         this.id = id;
  77         this.valueType = valueType;
  78         this.defaultValueFunction = defaultValueFunction;
  79         this.stringConverter = stringConverter;
  80     }
  81 
  82     public static final StandardBundlerParam<RelativeFileSet> APP_RESOURCES =
  83             new StandardBundlerParam<>(
  84                     I18N.getString("param.app-resources.name"),
  85                     I18N.getString("param.app-resource.description"),
  86                     BundleParams.PARAM_APP_RESOURCES,
  87                     RelativeFileSet.class,
  88                     null, // no default.  Required parameter
  89                     null // no string translation, tool must provide complex type
  90             );
  91 
  92     @SuppressWarnings("unchecked")
  93     public static final StandardBundlerParam<List<RelativeFileSet>> APP_RESOURCES_LIST =
  94             new StandardBundlerParam<>(
  95                     I18N.getString("param.app-resources-list.name"),
  96                     I18N.getString("param.app-resource-list.description"),
  97                     BundleParams.PARAM_APP_RESOURCES + "List",
  98                     (Class<List<RelativeFileSet>>) (Object) List.class,
  99                     p -> new ArrayList<>(Collections.singletonList(APP_RESOURCES.fetchFrom(p))), // Default is appResources, as a single item list
 100                     StandardBundlerParam::createAppResourcesListFromString
 101             );
 102 
 103     @SuppressWarnings("unchecked")
 104     public static final StandardBundlerParam<String> SOURCE_DIR =
 105             new StandardBundlerParam<>(
 106                     I18N.getString("param.source-dir.name"),
 107                     I18N.getString("param.source-dir.description"),
 108                     "srcdir",
 109                     String.class,
 110                     p -> null,
 111                     (s, p) -> {
 112                         String value = String.valueOf(s);
 113                         if (value.charAt(value.length() - 1) == File.separatorChar) {
 114                             return value.substring(0, value.length() - 1);
 115                         }
 116                         else {
 117                             return value;
 118                         }
 119                     }
 120             );
 121 
 122     // note that each bundler is likely to replace this one with their own converter
 123     public static final StandardBundlerParam<RelativeFileSet> MAIN_JAR =
 124             new StandardBundlerParam<>(
 125                     I18N.getString("param.main-jar.name"),
 126                     I18N.getString("param.main-jar.description"),
 127                     "mainJar",
 128                     RelativeFileSet.class,
 129                     params -> {
 130                         extractMainClassInfoFromAppResources(params);
 131                         return (RelativeFileSet) params.get("mainJar");
 132                     },
 133                     (s, p) -> getMainJar(s, p)
 134             );
 135 
 136     public static final StandardBundlerParam<String> CLASSPATH =
 137             new StandardBundlerParam<>(
 138                     I18N.getString("param.classpath.name"),
 139                     I18N.getString("param.classpath.description"),
 140                     "classpath",
 141                     String.class,
 142                     params -> {
 143                         extractMainClassInfoFromAppResources(params);
 144                         String cp = (String) params.get("classpath");
 145                         return cp == null ? "" : cp;
 146                     },
 147                     (s, p) -> s.replace(File.pathSeparator, " ")
 148             );
 149 
 150     public static final StandardBundlerParam<String> MAIN_CLASS =
 151             new StandardBundlerParam<>(
 152                     I18N.getString("param.main-class.name"),
 153                     I18N.getString("param.main-class.description"),
 154                     BundleParams.PARAM_APPLICATION_CLASS,
 155                     String.class,
 156                     params -> {
 157                         //FIXME sniff modules
 158                         extractMainClassInfoFromAppResources(params);
 159                         return (String) params.get(BundleParams.PARAM_APPLICATION_CLASS);
 160                     },
 161                     (s, p) -> s
 162             );
 163 
 164     public static final StandardBundlerParam<String> APP_NAME =
 165             new StandardBundlerParam<>(
 166                     I18N.getString("param.app-name.name"),
 167                     I18N.getString("param.app-name.description"),
 168                     BundleParams.PARAM_NAME,
 169                     String.class,
 170                     params -> {
 171                         String s = MAIN_CLASS.fetchFrom(params);
 172                         if (s == null) return null;
 173 
 174                         int idx = s.lastIndexOf(".");
 175                         if (idx >= 0) {
 176                             return s.substring(idx+1);
 177                         }
 178                         return s;
 179                     },
 180                     (s, p) -> s
 181             );
 182 
 183     private static Pattern TO_FS_NAME = Pattern.compile("\\s|[\\\\/?:*<>|]"); // keep out invalid/undesireable filename characters
 184 
 185     public static final StandardBundlerParam<String> APP_FS_NAME =
 186             new StandardBundlerParam<>(
 187                     I18N.getString("param.app-fs-name.name"),
 188                     I18N.getString("param.app-fs-name.description"),
 189                     "name.fs",
 190                     String.class,
 191                     params -> TO_FS_NAME.matcher(APP_NAME.fetchFrom(params)).replaceAll(""),
 192                     (s, p) -> s
 193             );
 194 
 195     public static final StandardBundlerParam<File> ICON =
 196             new StandardBundlerParam<>(
 197                     I18N.getString("param.icon-file.name"),
 198                     I18N.getString("param.icon-file.description"),
 199                     BundleParams.PARAM_ICON,
 200                     File.class,
 201                     params -> null,
 202                     (s, p) -> new File(s)
 203             );
 204 
 205     public static final StandardBundlerParam<String> VENDOR =
 206             new StandardBundlerParam<>(
 207                     I18N.getString("param.vendor.name"),
 208                     I18N.getString("param.vendor.description"),
 209                     BundleParams.PARAM_VENDOR,
 210                     String.class,
 211                     params -> I18N.getString("param.vendor.default"),
 212                     (s, p) -> s
 213             );
 214 
 215     public static final StandardBundlerParam<String> CATEGORY =
 216             new StandardBundlerParam<>(
 217                     I18N.getString("param.category.name"),
 218                     I18N.getString("param.category.description"),
 219                     BundleParams.PARAM_CATEGORY,
 220                     String.class,
 221                     params -> I18N.getString("param.category.default"),
 222                     (s, p) -> s
 223             );
 224 
 225     public static final StandardBundlerParam<String> DESCRIPTION =
 226             new StandardBundlerParam<>(
 227                     I18N.getString("param.description.name"),
 228                     I18N.getString("param.description.description"),
 229                     BundleParams.PARAM_DESCRIPTION,
 230                     String.class,
 231                     params -> params.containsKey(APP_NAME.getID())
 232                             ? APP_NAME.fetchFrom(params)
 233                             : I18N.getString("param.description.default"),
 234                     (s, p) -> s
 235             );
 236 
 237     public static final StandardBundlerParam<String> COPYRIGHT =
 238             new StandardBundlerParam<>(
 239                     I18N.getString("param.copyright.name"),
 240                     I18N.getString("param.copyright.description"),
 241                     BundleParams.PARAM_COPYRIGHT,
 242                     String.class,
 243                     params -> MessageFormat.format(I18N.getString("param.copyright.default"), new Date()),
 244                     (s, p) -> s
 245             );
 246 
 247     public static final StandardBundlerParam<Boolean> USE_FX_PACKAGING =
 248             new StandardBundlerParam<>(
 249                     I18N.getString("param.use-javafx-packaging.name"),
 250                     I18N.getString("param.use-javafx-packaging.description"),
 251                     "fxPackaging",
 252                     Boolean.class,
 253                     params -> {
 254                         extractMainClassInfoFromAppResources(params);
 255                         Boolean result = (Boolean) params.get("fxPackaging");
 256                         return (result == null) ? Boolean.FALSE : result;
 257                     },
 258                     (s, p) -> Boolean.valueOf(s)
 259             );
 260 
 261     @SuppressWarnings("unchecked")
 262     public static final StandardBundlerParam<List<String>> ARGUMENTS =
 263             new StandardBundlerParam<>(
 264                     I18N.getString("param.arguments.name"),
 265                     I18N.getString("param.arguments.description"),
 266                     "arguments",
 267                     (Class<List<String>>) (Object) List.class,
 268                     params -> Collections.emptyList(),
 269                     (s, p) -> splitStringWithEscapes(s)
 270             );
 271 
 272     @SuppressWarnings("unchecked")
 273     public static final StandardBundlerParam<List<String>> JVM_OPTIONS =
 274             new StandardBundlerParam<>(
 275                     I18N.getString("param.jvm-options.name"),
 276                     I18N.getString("param.jvm-options.description"),
 277                     "jvmOptions",
 278                     (Class<List<String>>) (Object) List.class,
 279                     params -> Collections.emptyList(),
 280                     (s, p) -> Arrays.asList(s.split("\\s+"))
 281             );
 282 
 283     @SuppressWarnings("unchecked")
 284     public static final StandardBundlerParam<Map<String, String>> JVM_PROPERTIES =
 285             new StandardBundlerParam<>(
 286                     I18N.getString("param.jvm-system-properties.name"),
 287                     I18N.getString("param.jvm-system-properties.description"),
 288                     "jvmProperties",
 289                     (Class<Map<String, String>>) (Object) Map.class,
 290                     params -> Collections.emptyMap(),
 291                     (s, params) -> {
 292                         Map<String, String> map = new HashMap<>();
 293                         try {
 294                             Properties p = new Properties();
 295                             p.load(new StringReader(s));
 296                             for (Map.Entry<Object, Object> entry : p.entrySet()) {
 297                                 map.put((String)entry.getKey(), (String)entry.getValue());
 298                             }
 299                         } catch (IOException e) {
 300                             e.printStackTrace();
 301                         }
 302                         return map;
 303                     }
 304             );
 305 
 306     @SuppressWarnings("unchecked")
 307     public static final StandardBundlerParam<Map<String, String>> USER_JVM_OPTIONS =
 308             new StandardBundlerParam<>(
 309                     I18N.getString("param.user-jvm-options.name"),
 310                     I18N.getString("param.user-jvm-options.description"),
 311                     "userJvmOptions",
 312                     (Class<Map<String, String>>) (Object) Map.class,
 313                     params -> Collections.emptyMap(),
 314                     (s, params) -> {
 315                         Map<String, String> map = new HashMap<>();
 316                         try {
 317                             Properties p = new Properties();
 318                             p.load(new StringReader(s));
 319                             for (Map.Entry<Object, Object> entry : p.entrySet()) {
 320                                 map.put((String)entry.getKey(), (String)entry.getValue());
 321                             }
 322                         } catch (IOException e) {
 323                             e.printStackTrace();
 324                         }
 325                         return map;
 326                     }
 327             );
 328 
 329     public static final StandardBundlerParam<String> TITLE =
 330             new StandardBundlerParam<>(
 331                     I18N.getString("param.title.name"),
 332                     I18N.getString("param.title.description"), //?? but what does it do?
 333                     BundleParams.PARAM_TITLE,
 334                     String.class,
 335                     APP_NAME::fetchFrom,
 336                     (s, p) -> s
 337             );
 338 
 339     // note that each bundler is likely to replace this one with their own converter
 340     public static final StandardBundlerParam<String> VERSION =
 341             new StandardBundlerParam<>(
 342                     I18N.getString("param.version.name"),
 343                     I18N.getString("param.version.description"),
 344                     BundleParams.PARAM_VERSION,
 345                     String.class,
 346                     params -> I18N.getString("param.version.default"),
 347                     (s, p) -> s
 348             );
 349 
 350     public static final StandardBundlerParam<Boolean> SYSTEM_WIDE =
 351             new StandardBundlerParam<>(
 352                     I18N.getString("param.system-wide.name"),
 353                     I18N.getString("param.system-wide.description"),
 354                     BundleParams.PARAM_SYSTEM_WIDE,
 355                     Boolean.class,
 356                     params -> null,
 357                     // valueOf(null) is false, and we actually do want null in some cases
 358                     (s, p) -> (s == null || "null".equalsIgnoreCase(s))? null : Boolean.valueOf(s)
 359             );
 360 
 361     public static final StandardBundlerParam<Boolean> SERVICE_HINT  =
 362             new StandardBundlerParam<>(
 363                     I18N.getString("param.service-hint.name"),
 364                     I18N.getString("param.service-hint.description"),
 365                     BundleParams.PARAM_SERVICE_HINT,
 366                     Boolean.class,
 367                     params -> false,
 368                     (s, p) -> (s == null || "null".equalsIgnoreCase(s))? false : Boolean.valueOf(s)
 369             );
 370 
 371     public static final StandardBundlerParam<Boolean> START_ON_INSTALL  =
 372             new StandardBundlerParam<>(
 373                     I18N.getString("param.start-on-install.name"),
 374                     I18N.getString("param.start-on-install.description"),
 375                     "startOnInstall",
 376                     Boolean.class,
 377                     params -> false,
 378                     (s, p) -> (s == null || "null".equalsIgnoreCase(s))? false : Boolean.valueOf(s)
 379             );
 380 
 381     public static final StandardBundlerParam<Boolean> STOP_ON_UNINSTALL  =
 382             new StandardBundlerParam<>(
 383                     I18N.getString("param.stop-on-uninstall.name"),
 384                     I18N.getString("param.stop-on-uninstall.description"),
 385                     "stopOnUninstall",
 386                     Boolean.class,
 387                     params -> true,
 388                     (s, p) -> (s == null || "null".equalsIgnoreCase(s))? true : Boolean.valueOf(s)
 389             );
 390 
 391     public static final StandardBundlerParam<Boolean> RUN_AT_STARTUP  =
 392             new StandardBundlerParam<>(
 393                     I18N.getString("param.run-at-startup.name"),
 394                     I18N.getString("param.run-at-startup.description"),
 395                     "runAtStartup",
 396                     Boolean.class,
 397                     params -> false,
 398                     (s, p) -> (s == null || "null".equalsIgnoreCase(s))? false : Boolean.valueOf(s)
 399             );
 400 
 401     public static final StandardBundlerParam<Boolean> SIGN_BUNDLE  =
 402             new StandardBundlerParam<>(
 403                     I18N.getString("param.sign-bundle.name"),
 404                     I18N.getString("param.sign-bundle.description"),
 405                     "signBundle",
 406                     Boolean.class,
 407                     params -> null,
 408                     // valueOf(null) is false, and we actually do want null in some cases
 409                     (s, p) -> (s == null || "null".equalsIgnoreCase(s))? null : Boolean.valueOf(s)
 410             );
 411 
 412     public static final StandardBundlerParam<Boolean> SHORTCUT_HINT =
 413             new StandardBundlerParam<>(
 414                     I18N.getString("param.desktop-shortcut-hint.name"),
 415                     I18N.getString("param.desktop-shortcut-hint.description"),
 416                     BundleParams.PARAM_SHORTCUT,
 417                     Boolean.class,
 418                     params -> false,
 419                     // valueOf(null) is false, and we actually do want null in some cases
 420                     (s, p) -> (s == null || "null".equalsIgnoreCase(s))? false : Boolean.valueOf(s)
 421             );
 422 
 423     public static final StandardBundlerParam<Boolean> MENU_HINT =
 424             new StandardBundlerParam<>(
 425                     I18N.getString("param.menu-shortcut-hint.name"),
 426                     I18N.getString("param.menu-shortcut-hint.description"),
 427                     BundleParams.PARAM_MENU,
 428                     Boolean.class,
 429                     params -> false,
 430                     // valueOf(null) is false, and we actually do want null in some cases
 431                     (s, p) -> (s == null || "null".equalsIgnoreCase(s))? true : Boolean.valueOf(s)
 432             );
 433 
 434     @SuppressWarnings("unchecked")
 435     public static final StandardBundlerParam<List<String>> LICENSE_FILE =
 436             new StandardBundlerParam<>(
 437                     I18N.getString("param.license-file.name"),
 438                     I18N.getString("param.license-file.description"),
 439                     BundleParams.PARAM_LICENSE_FILE,
 440                     (Class<List<String>>)(Object)List.class,
 441                     params -> Collections.<String>emptyList(),
 442                     (s, p) -> Arrays.asList(s.split(","))
 443             );
 444 
 445     public static final BundlerParamInfo<String> LICENSE_TYPE =
 446             new StandardBundlerParam<>(
 447                     I18N.getString("param.license-type.name"),
 448                     I18N.getString("param.license-type.description"),
 449                     BundleParams.PARAM_LICENSE_TYPE,
 450                     String.class,
 451                     params -> I18N.getString("param.license-type.default"),
 452                     (s, p) -> s
 453             );
 454 
 455     public static final StandardBundlerParam<File> BUILD_ROOT =
 456             new StandardBundlerParam<>(
 457                     I18N.getString("param.build-root.name"),
 458                     I18N.getString("param.build-root.description"),
 459                     "buildRoot",
 460                     File.class,
 461                     params -> {
 462                         try {
 463                             return Files.createTempDirectory("fxbundler").toFile();
 464                         } catch (IOException ioe) {
 465                             return null;
 466                         }
 467                     },
 468                     (s, p) -> new File(s)
 469             );
 470 
 471     public static final StandardBundlerParam<String> IDENTIFIER =
 472             new StandardBundlerParam<>(
 473                     I18N.getString("param.identifier.name"),
 474                     I18N.getString("param.identifier.description"),
 475                     BundleParams.PARAM_IDENTIFIER,
 476                     String.class,
 477                     params -> {
 478                         String s = MAIN_CLASS.fetchFrom(params);
 479                         if (s == null) return null;
 480 
 481                         int idx = s.lastIndexOf(".");
 482                         if (idx >= 1) {
 483                             return s.substring(0, idx);
 484                         }
 485                         return s;
 486                     },
 487                     (s, p) -> s
 488             );
 489 
 490     public static final StandardBundlerParam<String> PREFERENCES_ID =
 491             new StandardBundlerParam<>(
 492                     I18N.getString("param.preferences-id.name"),
 493                     I18N.getString("param.preferences-id.description"),
 494                     "preferencesID",
 495                     String.class,
 496                     p -> Optional.ofNullable(IDENTIFIER.fetchFrom(p)).orElse("").replace('.', '/'),
 497                     (s, p) -> s
 498             );
 499 
 500     public static final StandardBundlerParam<String> PRELOADER_CLASS =
 501             new StandardBundlerParam<>(
 502                     I18N.getString("param.preloader.name"),
 503                     I18N.getString("param.preloader.description"),
 504                     "preloader",
 505                     String.class,
 506                     p -> null,
 507                     null
 508             );
 509 
 510     public static final StandardBundlerParam<Boolean> VERBOSE  =
 511             new StandardBundlerParam<>(
 512                     I18N.getString("param.verbose.name"),
 513                     I18N.getString("param.verbose.description"),
 514                     "verbose",
 515                     Boolean.class,
 516                     params -> false,
 517                     // valueOf(null) is false, and we actually do want null in some cases
 518                     (s, p) -> (s == null || "null".equalsIgnoreCase(s))? true : Boolean.valueOf(s)
 519             );
 520 
 521     public static final StandardBundlerParam<File> DROP_IN_RESOURCES_ROOT =
 522             new StandardBundlerParam<>(
 523                     I18N.getString("param.drop-in-resources-root.name"),
 524                     I18N.getString("param.drop-in-resources-root.description"),
 525                     "dropinResourcesRoot",
 526                     File.class,
 527                     params -> null,
 528                     (s, p) -> new File(s)
 529             );
 530 
 531     @SuppressWarnings("unchecked")
 532     public static final StandardBundlerParam<List<Map<String, ? super Object>>> SECONDARY_LAUNCHERS =
 533             new StandardBundlerParam<>(
 534                     I18N.getString("param.secondary-launchers.name"),
 535                     I18N.getString("param.secondary-launchers.description"),
 536                     "secondaryLaunchers",
 537                     (Class<List<Map<String, ? super Object>>>) (Object) List.class,
 538                     params -> new ArrayList<>(1),
 539                     // valueOf(null) is false, and we actually do want null in some cases
 540                     (s, p) -> null
 541             );
 542 
 543     @SuppressWarnings("unchecked")
 544     public static final StandardBundlerParam<List<Map<String, ? super Object>>> FILE_ASSOCIATIONS =
 545             new StandardBundlerParam<>(
 546                     I18N.getString("param.file-associations.name"),
 547                     I18N.getString("param.file-associations.description"),
 548                     "fileAssociations",
 549                     (Class<List<Map<String, ? super Object>>>) (Object) List.class,
 550                     params -> new ArrayList<>(1),
 551                     // valueOf(null) is false, and we actually do want null in some cases
 552                     (s, p) -> null
 553             );
 554 
 555     @SuppressWarnings("unchecked")
 556     public static final StandardBundlerParam<List<String>> FA_EXTENSIONS =
 557             new StandardBundlerParam<>(
 558                     I18N.getString("param.fa-extension.name"),
 559                     I18N.getString("param.fa-extension.description"),
 560                     "fileAssociation.extension",
 561                     (Class<List<String>>) (Object) List.class,
 562                     params -> null, // null means not matched to an extension
 563                     (s, p) -> Arrays.asList(s.split("(,|\\s)+"))
 564             );
 565 
 566     @SuppressWarnings("unchecked")
 567     public static final StandardBundlerParam<List<String>> FA_CONTENT_TYPE =
 568             new StandardBundlerParam<>(
 569                     I18N.getString("param.fa-content-type.name"),
 570                     I18N.getString("param.fa-content-type.description"),
 571                     "fileAssociation.contentType",
 572                     (Class<List<String>>) (Object) List.class,
 573                     params -> null, // null means not matched to a content/mime type
 574                     (s, p) -> Arrays.asList(s.split("(,|\\s)+"))
 575             );
 576 
 577     public static final StandardBundlerParam<String> FA_DESCRIPTION =
 578             new StandardBundlerParam<>(
 579                     I18N.getString("param.fa-description.name"),
 580                     I18N.getString("param.fa-description.description"),
 581                     "fileAssociation.description",
 582                     String.class,
 583                     params -> APP_NAME.fetchFrom(params) + " File",
 584                     null
 585             );
 586 
 587     public static final StandardBundlerParam<File> FA_ICON =
 588             new StandardBundlerParam<>(
 589                     I18N.getString("param.fa-icon.name"),
 590                     I18N.getString("param.fa-icon.description"),
 591                     "fileAssociation.icon",
 592                     File.class,
 593                     ICON::fetchFrom,
 594                     (s, p) -> new File(s)
 595             );
 596 
 597     public static final StandardBundlerParam<Boolean> UNLOCK_COMMERCIAL_FEATURES =
 598             new StandardBundlerParam<>(
 599                     I18N.getString("param.commercial-features.name"),
 600                     I18N.getString("param.commercial-features.description"),
 601                     "commercialFeatures",
 602                     Boolean.class,
 603                     p -> false,
 604                     (s, p) -> Boolean.parseBoolean(s)
 605             );
 606 
 607     public static final StandardBundlerParam<Boolean> ENABLE_APP_CDS =
 608             new StandardBundlerParam<>(
 609                     I18N.getString("param.com-app-cds.name"),
 610                     I18N.getString("param.com-app-cds.description"),
 611                     "commercial.AppCDS",
 612                     Boolean.class,
 613                     p -> false,
 614                     (s, p) -> Boolean.parseBoolean(s)
 615             );
 616 
 617     public static final StandardBundlerParam<String> APP_CDS_CACHE_MODE =
 618             new StandardBundlerParam<>(
 619                     I18N.getString("param.com-app-cds-cache-mode.name"),
 620                     I18N.getString("param.com-app-cds-cache-mode.description"),
 621                     "commercial.AppCDS.cache",
 622                     String.class,
 623                     p -> "auto",
 624                     (s, p) -> s
 625             );
 626 
 627     @SuppressWarnings("unchecked")
 628     public static final StandardBundlerParam<List<String>> APP_CDS_CLASS_ROOTS =
 629             new StandardBundlerParam<>(
 630                     I18N.getString("param.com-app-cds-root.name"),
 631                     I18N.getString("param.com-app-cds-root.description"),
 632                     "commercial.AppCDS.classRoots",
 633                     (Class<List<String>>)((Object)List.class),
 634                     p -> Collections.singletonList(MAIN_CLASS.fetchFrom(p)),
 635                     (s, p) -> Arrays.asList(s.split("[ ,:]"))
 636             );
 637 
 638     @SuppressWarnings("unchecked")
 639     public static final BundlerParamInfo<List<Path>> MODULE_PATH =
 640             new StandardBundlerParam<>(
 641                     I18N.getString("param.module-path.name"),
 642                     I18N.getString("param.module-path.description"),
 643                     "module-path",
 644                     (Class<List<Path>>) (Object)List.class,
 645                     p -> { return getDefaultModulePath(); },
 646                     (s, p) -> {
 647                         List<Path> modulePath = Arrays.asList(s.split("[;:]")).stream()
 648                                                       .map(ss -> new File(ss).toPath())
 649                                                       .collect(Collectors.toList());
 650                         Path userDefinedJdkModulePath = null;
 651                         if (modulePath != null) {
 652                             userDefinedJdkModulePath = JLinkBundlerHelper.findPathOfModule(modulePath, "java.base.jmod");
 653                         }
 654                         else {
 655                             modulePath = new ArrayList();
 656                         }
 657 
 658                         // Add the default JDK module path to the module path.
 659                         if (userDefinedJdkModulePath == null) {
 660                             List<Path> jdkModulePath = getDefaultModulePath();
 661 
 662                             if (jdkModulePath != null) {
 663                                 modulePath.addAll(jdkModulePath);
 664                             }
 665                         }
 666 
 667                         Path javaBasePath = findPathOfModule(modulePath, "java.base.jmod");
 668 
 669                         if (javaBasePath == null || !javaBasePath.toFile().exists()) {
 670                             com.oracle.tools.packager.Log.info(String.format(I18N.getString("warning.no.jdk.modules.found")));
 671                         }
 672 
 673                         return modulePath;
 674                     });
 675 
 676     @SuppressWarnings("unchecked")
 677     public static final BundlerParamInfo<String> MODULE =
 678             new StandardBundlerParam<>(
 679                     I18N.getString("param.main.module.name"),
 680                     I18N.getString("param.main.module.description"),
 681                     "module",
 682                     String.class,
 683                     p -> null,
 684                     (s, p) -> {
 685                         return String.valueOf(s);
 686                     });
 687 
 688     @SuppressWarnings("unchecked")
 689     public static final BundlerParamInfo<Set<String>> ADD_MODULES =
 690             new StandardBundlerParam<>(
 691                     I18N.getString("param.add-modules.name"),
 692                     I18N.getString("param.add-modules.description"),
 693                     "add-modules",
 694                     (Class<Set<String>>) (Object) Set.class,
 695                     p -> new LinkedHashSet(),
 696                     (s, p) -> new LinkedHashSet<>(Arrays.asList(s.split("[,;: ]+")))
 697             );
 698 
 699     @SuppressWarnings("unchecked")
 700     public static final BundlerParamInfo<Set<String>> LIMIT_MODULES =
 701             new StandardBundlerParam<>(
 702                     I18N.getString("param.limit-modules.name"),
 703                     I18N.getString("param.limit-modules.description"),
 704                     "limit-modules",
 705                     (Class<Set<String>>) (Object) Set.class,
 706                     p -> new LinkedHashSet(),
 707                     (s, p) -> new LinkedHashSet<>(Arrays.asList(s.split("[,;: ]+")))
 708             );
 709 
 710     @SuppressWarnings("unchecked")
 711     public static final BundlerParamInfo<Boolean> STRIP_NATIVE_COMMANDS =
 712             new StandardBundlerParam<>(
 713                     I18N.getString("param.strip-executables.name"),
 714                     I18N.getString("param.strip-executables.description"),
 715                     "strip-native-commands",
 716                     Boolean.class,
 717                     p -> Boolean.TRUE,
 718                     (s, p) -> Boolean.valueOf(s)
 719             );
 720 
 721     public static void extractMainClassInfoFromAppResources(Map<String, ? super Object> params) {
 722         boolean hasMainClass = params.containsKey(MAIN_CLASS.getID());
 723         boolean hasMainJar = params.containsKey(MAIN_JAR.getID());
 724         boolean hasMainJarClassPath = params.containsKey(CLASSPATH.getID());
 725         boolean hasPreloader = params.containsKey(PRELOADER_CLASS.getID());
 726         boolean hasModule = params.containsKey(MODULE.getID());
 727 
 728         if (hasMainClass && hasMainJar && hasMainJarClassPath || hasModule) {
 729             return;
 730         }
 731 
 732         // it's a pair.  The [0] is the srcdir [1] is the file relative to sourcedir
 733         List<String[]> filesToCheck = new ArrayList<>();
 734 
 735         if (hasMainJar) {
 736             RelativeFileSet rfs = MAIN_JAR.fetchFrom(params);
 737             for (String s : rfs.getIncludedFiles()) {
 738                 filesToCheck.add(new String[]{rfs.getBaseDirectory().toString(), s});
 739             }
 740         } else if (hasMainJarClassPath) {
 741             for (String s : CLASSPATH.fetchFrom(params).split("\\s+")) {
 742                 if (APP_RESOURCES.fetchFrom(params) != null) {
 743                     filesToCheck.add(new String[] {APP_RESOURCES.fetchFrom(params).getBaseDirectory().toString(), s});
 744                 }
 745             }
 746         } else {
 747             List<RelativeFileSet> rfsl = APP_RESOURCES_LIST.fetchFrom(params);
 748             if (rfsl == null || rfsl.isEmpty()) {
 749                 return;
 750             }
 751             for (RelativeFileSet rfs : rfsl) {
 752                 if (rfs == null) continue;
 753 
 754                 for (String s : rfs.getIncludedFiles()) {
 755                     filesToCheck.add(new String[]{rfs.getBaseDirectory().toString(), s});
 756                 }
 757             }
 758         }
 759 
 760         String declaredMainClass = (String) params.get(MAIN_CLASS.getID());
 761 
 762         // presume the set iterates in-order
 763         for (String[] fnames : filesToCheck) {
 764             try {
 765                 // only sniff jars
 766                 if (!fnames[1].toLowerCase().endsWith(".jar")) continue;
 767 
 768                 File file = new File(fnames[0], fnames[1]);
 769                 // that actually exist
 770                 if (!file.exists()) continue;
 771 
 772                 try (JarFile jf = new JarFile(file)) {
 773                     Manifest m = jf.getManifest();
 774                     Attributes attrs = (m != null) ? m.getMainAttributes() : null;
 775 
 776                     if (attrs != null) {
 777                         String mainClass = attrs.getValue(Attributes.Name.MAIN_CLASS);
 778                         String fxMain = attrs.getValue(MANIFEST_JAVAFX_MAIN);
 779                         String preloaderClass = attrs.getValue(MANIFEST_PRELOADER);
 780                         if (hasMainClass) {
 781                             if (declaredMainClass.equals(fxMain)) {
 782                                 params.put(USE_FX_PACKAGING.getID(), true);
 783                             } else if (declaredMainClass.equals(mainClass)) {
 784                                 params.put(USE_FX_PACKAGING.getID(), false);
 785                             } else {
 786                                 if (fxMain != null) {
 787                                     Log.info(MessageFormat.format(I18N.getString("message.fx-app-does-not-match-specified-main"), fnames[1], fxMain, declaredMainClass));
 788                                 }
 789                                 if (mainClass != null) {
 790                                     Log.info(MessageFormat.format(I18N.getString("message.main-class-does-not-match-specified-main"), fnames[1], mainClass, declaredMainClass));
 791                                 }
 792                                 continue;
 793                             }
 794                         } else {
 795                             if (fxMain != null) {
 796                                 params.put(USE_FX_PACKAGING.getID(), true);
 797                                 params.put(MAIN_CLASS.getID(), fxMain);
 798                             } else if (mainClass != null) {
 799                                 params.put(USE_FX_PACKAGING.getID(), false);
 800                                 params.put(MAIN_CLASS.getID(), mainClass);
 801                             } else {
 802                                 continue;
 803                             }
 804                         }
 805                         if (!hasPreloader && preloaderClass != null) {
 806                             params.put(PRELOADER_CLASS.getID(), preloaderClass);
 807                         }
 808                         if (!hasMainJar) {
 809                             if (fnames[0] == null) {
 810                                 fnames[0] = file.getParentFile().toString();
 811                             }
 812                             params.put(MAIN_JAR.getID(), new RelativeFileSet(new File(fnames[0]), new LinkedHashSet<>(Collections.singletonList(file))));
 813                         }
 814                         if (!hasMainJarClassPath) {
 815                             String cp = attrs.getValue(Attributes.Name.CLASS_PATH);
 816                             params.put(CLASSPATH.getID(), cp == null ? "" : cp);
 817                         }
 818                         break;
 819                     }
 820                 }
 821             } catch (IOException ignore) {
 822                 ignore.printStackTrace();
 823             }
 824         }
 825     }
 826 
 827     public static void validateMainClassInfoFromAppResources(Map<String, ? super Object> params) throws ConfigException {
 828         boolean hasMainClass = params.containsKey(MAIN_CLASS.getID());
 829         boolean hasMainJar = params.containsKey(MAIN_JAR.getID());
 830         boolean hasMainJarClassPath = params.containsKey(CLASSPATH.getID());
 831         boolean hasModule = params.containsKey(MODULE.getID());
 832 
 833         if (hasMainClass && hasMainJar && hasMainJarClassPath || hasModule) {
 834             return;
 835         }
 836 
 837         extractMainClassInfoFromAppResources(params);
 838 
 839         if (!params.containsKey(MAIN_CLASS.getID())) {
 840             if (hasMainJar) {
 841                 throw new ConfigException(
 842                         MessageFormat.format(I18N.getString("error.no-main-class-with-main-jar"),
 843                                 MAIN_JAR.fetchFrom(params)),
 844                         MessageFormat.format(I18N.getString("error.no-main-class-with-main-jar.advice"),
 845                                 MAIN_JAR.fetchFrom(params)));
 846             } else if (hasMainJarClassPath) {
 847                 throw new ConfigException(
 848                         I18N.getString("error.no-main-class-with-classpath"),
 849                         I18N.getString("error.no-main-class-with-classpath.advice"));
 850             } else {
 851                 throw new ConfigException(
 852                         I18N.getString("error.no-main-class"),
 853                         I18N.getString("error.no-main-class.advice"));
 854             }
 855         }
 856     }
 857 
 858 
 859     private static List<String> splitStringWithEscapes(String s) {
 860         List<String> l = new ArrayList<>();
 861         StringBuilder current = new StringBuilder();
 862         boolean quoted = false;
 863         boolean escaped = false;
 864         for (char c : s.toCharArray()) {
 865             if (escaped) {
 866                 current.append(c);
 867             } else if ('"' == c) {
 868                 quoted = !quoted;
 869             } else if (!quoted && Character.isWhitespace(c)) {
 870                 l.add(current.toString());
 871                 current = new StringBuilder();
 872             } else {
 873                 current.append(c);
 874             }
 875         }
 876         l.add(current.toString());
 877         return l;
 878     }
 879 
 880     private static List<RelativeFileSet> createAppResourcesListFromString(String s, Map<String, ? super Object> objectObjectMap) {
 881         List<RelativeFileSet> result = new ArrayList<>();
 882         for (String path : s.split("[:;]")) {
 883             File f = new File(path);
 884             if (f.getName().equals("*") || path.endsWith("/") || path.endsWith("\\")) {
 885                 if (f.getName().equals("*")) {
 886                     f = f.getParentFile();
 887                 }
 888                 Set<File> theFiles = new HashSet<>();
 889                 try {
 890                     Files.walk(f.toPath())
 891                             .filter(Files::isRegularFile)
 892                             .forEach(p -> theFiles.add(p.toFile()));
 893                 } catch (IOException e) {
 894                     e.printStackTrace();
 895                 }
 896                 result.add(new RelativeFileSet(f, theFiles));
 897             } else {
 898                 result.add(new RelativeFileSet(f.getParentFile(), Collections.singleton(f)));
 899             }
 900         }
 901         return result;
 902     }
 903 
 904     private static RelativeFileSet getMainJar(String moduleName, Map<String, ? super Object> params) {
 905         for (RelativeFileSet rfs : APP_RESOURCES_LIST.fetchFrom(params)) {
 906             File appResourcesRoot = rfs.getBaseDirectory();
 907             File mainJarFile = new File(appResourcesRoot, moduleName);
 908 
 909             if (mainJarFile.exists()) {
 910                 return new RelativeFileSet(appResourcesRoot, new LinkedHashSet<>(Collections.singletonList(mainJarFile)));
 911             }
 912             else {
 913                 List<Path> modulePath = MODULE_PATH.fetchFrom(params);
 914                 Path modularJarPath = JLinkBundlerHelper.findPathOfModule(modulePath, moduleName);
 915 
 916                 if (modularJarPath != null && Files.exists(modularJarPath)) {
 917                     return new RelativeFileSet(appResourcesRoot, new LinkedHashSet<>(Collections.singletonList(modularJarPath.toFile())));
 918                 }
 919             }
 920         }
 921 
 922         throw new IllegalArgumentException(
 923                 new ConfigException(
 924                         MessageFormat.format(I18N.getString("error.main-jar-does-not-exist"), moduleName),
 925                         I18N.getString("error.main-jar-does-not-exist.advice")));
 926     }
 927 
 928     public static List<Path> getDefaultModulePath() {
 929         List<Path> result = new ArrayList();
 930         Path jdkModulePath = Paths.get(System.getProperty("java.home"), "jmods").toAbsolutePath();
 931 
 932         if (jdkModulePath != null && Files.exists(jdkModulePath)) {
 933             result.add(jdkModulePath);
 934         }
 935         else {
 936             jdkModulePath = Paths.get(System.getProperty("java.home"), "../images/jmods").toAbsolutePath();
 937 
 938             if (jdkModulePath != null && Files.exists(jdkModulePath)) {
 939                 result.add(jdkModulePath);
 940             }
 941         }
 942 
 943         return result;
 944     }
 945 }