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