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