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