1 /*
   2  * Copyright (c) 2014, 2015, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package com.oracle.tools.packager;
  27 
  28 import com.sun.javafx.tools.packager.bundlers.BundleParams;
  29 
  30 import java.io.File;
  31 import java.io.IOException;
  32 import java.io.StringReader;
  33 import java.nio.file.Files;
  34 import java.text.MessageFormat;
  35 import java.util.ArrayList;
  36 import java.util.Arrays;
  37 import java.util.Collections;
  38 import java.util.Date;
  39 import java.util.HashMap;
  40 import java.util.HashSet;
  41 import java.util.LinkedHashSet;
  42 import java.util.List;
  43 import java.util.Map;
  44 import java.util.Optional;
  45 import java.util.Properties;
  46 import java.util.ResourceBundle;
  47 import java.util.Set;
  48 import java.util.function.BiFunction;
  49 import java.util.function.Function;
  50 import java.util.jar.Attributes;
  51 import java.util.jar.JarFile;
  52 import java.util.jar.Manifest;
  53 import java.util.regex.Pattern;
  54 
  55 public class StandardBundlerParam<T> extends BundlerParamInfo<T> {
  56 
  57     public static final String MANIFEST_JAVAFX_MAIN ="JavaFX-Application-Class";
  58     public static final String MANIFEST_PRELOADER = "JavaFX-Preloader-Class";
  59 
  60     private static final ResourceBundle I18N =
  61             ResourceBundle.getBundle(StandardBundlerParam.class.getName());
  62 
  63     public StandardBundlerParam(String name, String description, String id,
  64                                 Class<T> valueType,
  65                                 Function<Map<String, ? super Object>, T> defaultValueFunction,
  66                                 BiFunction<String, Map<String, ? super Object>, T> stringConverter) {
  67         this.name = name;
  68         this.description = description;
  69         this.id = id;
  70         this.valueType = valueType;
  71         this.defaultValueFunction = defaultValueFunction;
  72         this.stringConverter = stringConverter;
  73     }
  74 
  75     public static final StandardBundlerParam<RelativeFileSet> APP_RESOURCES =
  76             new StandardBundlerParam<>(
  77                     I18N.getString("param.app-resources.name"),
  78                     I18N.getString("param.app-resource.description"),
  79                     BundleParams.PARAM_APP_RESOURCES,
  80                     RelativeFileSet.class,
  81                     null, // no default.  Required parameter
  82                     null // no string translation, tool must provide complex type
  83             );
  84 
  85     @SuppressWarnings("unchecked")
  86     public static final StandardBundlerParam<List<RelativeFileSet>> APP_RESOURCES_LIST =
  87             new StandardBundlerParam<>(
  88                     I18N.getString("param.app-resources-list.name"),
  89                     I18N.getString("param.app-resource-list.description"),
  90                     BundleParams.PARAM_APP_RESOURCES + "List",
  91                     (Class<List<RelativeFileSet>>) (Object) List.class,
  92                     p -> new ArrayList<>(Collections.singletonList(APP_RESOURCES.fetchFrom(p))), // Default is appResources, as a single item list
  93                     StandardBundlerParam::createAppResourcesListFromString
  94             );
  95 
  96     private static List<RelativeFileSet> createAppResourcesListFromString(String s, Map<String, ? super Object> objectObjectMap) {
  97         List<RelativeFileSet> result = new ArrayList<>();
  98         for (String path : s.split("[:;]")) {
  99             File f = new File(path);
 100             if (f.getName().equals("*") || path.endsWith("/") || path.endsWith("\\")) {
 101                 if (f.getName().equals("*")) {
 102                     f = f.getParentFile();
 103                 }
 104                 Set<File> theFiles = new HashSet<>();
 105                 try {
 106                     Files.walk(f.toPath())
 107                             .filter(Files::isRegularFile)
 108                             .forEach(p -> theFiles.add(p.toFile()));
 109                 } catch (IOException e) {
 110                     e.printStackTrace();
 111                 }
 112                 result.add(new RelativeFileSet(f, theFiles));
 113             } else {
 114                 result.add(new RelativeFileSet(f.getParentFile(), Collections.singleton(f)));
 115             }
 116         }
 117         return result;
 118     }
 119 
 120     public static final StandardBundlerParam<File> ICON =
 121             new StandardBundlerParam<>(
 122                     I18N.getString("param.icon-file.name"),
 123                     I18N.getString("param.icon-file.description"),
 124                     BundleParams.PARAM_ICON,
 125                     File.class,
 126                     params -> null,
 127                     (s, p) -> new File(s)
 128             );
 129 
 130 
 131     public static final StandardBundlerParam<String> MAIN_CLASS =
 132             new StandardBundlerParam<>(
 133                     I18N.getString("param.main-class.name"),
 134                     I18N.getString("param.main-class.description"),
 135                     BundleParams.PARAM_APPLICATION_CLASS,
 136                     String.class,
 137                     params -> {
 138                         //FIXME sniff modules
 139                         extractMainClassInfoFromAppResources(params);
 140                         return (String) params.get(BundleParams.PARAM_APPLICATION_CLASS);
 141                     },
 142                     (s, p) -> s
 143             );
 144 
 145     public static final StandardBundlerParam<String> APP_NAME =
 146             new StandardBundlerParam<>(
 147                     I18N.getString("param.app-name.name"),
 148                     I18N.getString("param.app-name.description"),
 149                     BundleParams.PARAM_NAME,
 150                     String.class,
 151                     params -> {
 152                         String s = MAIN_CLASS.fetchFrom(params);
 153                         if (s == null) return null;
 154 
 155                         int idx = s.lastIndexOf(".");
 156                         if (idx >= 0) {
 157                             return s.substring(idx+1);
 158                         }
 159                         return s;
 160                     },
 161                     (s, p) -> s
 162             );
 163 
 164     private static Pattern TO_FS_NAME = Pattern.compile("\\s|[\\\\/?:*<>|]"); // keep out invalid/undesireable filename characters
 165 
 166     public static final StandardBundlerParam<String> APP_FS_NAME =
 167             new StandardBundlerParam<>(
 168                     I18N.getString("param.app-fs-name.name"),
 169                     I18N.getString("param.app-fs-name.description"),
 170                     "name.fs",
 171                     String.class,
 172                     params -> TO_FS_NAME.matcher(APP_NAME.fetchFrom(params)).replaceAll(""),
 173                     (s, p) -> s
 174             );
 175 
 176 
 177     public static final StandardBundlerParam<String> VENDOR =
 178             new StandardBundlerParam<>(
 179                     I18N.getString("param.vendor.name"),
 180                     I18N.getString("param.vendor.description"),
 181                     BundleParams.PARAM_VENDOR,
 182                     String.class,
 183                     params -> I18N.getString("param.vendor.default"),
 184                     (s, p) -> s
 185             );
 186 
 187     public static final StandardBundlerParam<String> CATEGORY =
 188             new StandardBundlerParam<>(
 189                     I18N.getString("param.category.name"),
 190                     I18N.getString("param.category.description"),
 191                     BundleParams.PARAM_CATEGORY,
 192                     String.class,
 193                     params -> I18N.getString("param.category.default"),
 194                     (s, p) -> s
 195             );
 196 
 197     public static final StandardBundlerParam<String> DESCRIPTION =
 198             new StandardBundlerParam<>(
 199                     I18N.getString("param.description.name"),
 200                     I18N.getString("param.description.description"),
 201                     BundleParams.PARAM_DESCRIPTION,
 202                     String.class,
 203                     params -> params.containsKey(APP_NAME.getID())
 204                             ? APP_NAME.fetchFrom(params)
 205                             : I18N.getString("param.description.default"),
 206                     (s, p) -> s
 207             );
 208 
 209     public static final StandardBundlerParam<String> COPYRIGHT =
 210             new StandardBundlerParam<>(
 211                     I18N.getString("param.copyright.name"),
 212                     I18N.getString("param.copyright.description"),
 213                     BundleParams.PARAM_COPYRIGHT,
 214                     String.class,
 215                     params -> MessageFormat.format(I18N.getString("param.copyright.default"), new Date()),
 216                     (s, p) -> s
 217             );
 218 
 219     // note that each bundler is likely to replace this one with their own converter
 220     public static final StandardBundlerParam<RelativeFileSet> MAIN_JAR =
 221             new StandardBundlerParam<>(
 222                     I18N.getString("param.main-jar.name"),
 223                     I18N.getString("param.main-jar.description"),
 224                     "mainJar",
 225                     RelativeFileSet.class,
 226                     params -> {
 227                         extractMainClassInfoFromAppResources(params);
 228                         return (RelativeFileSet) params.get("mainJar");
 229                     },
 230                     (s, p) -> {
 231                         for (RelativeFileSet rfs : APP_RESOURCES_LIST.fetchFrom(p)) {
 232                             File appResourcesRoot = rfs.getBaseDirectory();
 233                             File f = new File(appResourcesRoot, s);
 234                             if (f.exists()) {
 235                                 return new RelativeFileSet(appResourcesRoot, new LinkedHashSet<>(Collections.singletonList(f)));
 236                             }
 237                         }
 238                         throw new IllegalArgumentException(
 239                                 new ConfigException(
 240                                         MessageFormat.format(I18N.getString("error.main-jar-does-not-exist"), s),
 241                                         I18N.getString("error.main-jar-does-not-exist.advice")));
 242                     }
 243             );
 244 
 245     public static final StandardBundlerParam<String> CLASSPATH =
 246             new StandardBundlerParam<>(
 247                     I18N.getString("param.classpath.name"),
 248                     I18N.getString("param.classpath.description"),
 249                     "classpath",
 250                     String.class,
 251                     params -> {
 252                         extractMainClassInfoFromAppResources(params);
 253                         String cp = (String) params.get("classpath");
 254                         return cp == null ? "" : cp;
 255                     },
 256                     (s, p) -> s.replace(File.pathSeparator, " ")
 257             );
 258 
 259     public static final StandardBundlerParam<Boolean> USE_FX_PACKAGING =
 260             new StandardBundlerParam<>(
 261                     I18N.getString("param.use-javafx-packaging.name"),
 262                     I18N.getString("param.use-javafx-packaging.description"),
 263                     "fxPackaging",
 264                     Boolean.class,
 265                     params -> {
 266                         extractMainClassInfoFromAppResources(params);
 267                         Boolean result = (Boolean) params.get("fxPackaging");
 268                         return (result == null) ? Boolean.FALSE : result;
 269                     },
 270                     (s, p) -> Boolean.valueOf(s)
 271             );
 272 
 273     @SuppressWarnings("unchecked")
 274     public static final StandardBundlerParam<List<String>> ARGUMENTS =
 275             new StandardBundlerParam<>(
 276                     I18N.getString("param.arguments.name"),
 277                     I18N.getString("param.arguments.description"),
 278                     "arguments",
 279                     (Class<List<String>>) (Object) List.class,
 280                     params -> Collections.emptyList(),
 281                     (s, p) -> splitStringWithEscapes(s)
 282             );
 283 
 284     @SuppressWarnings("unchecked")
 285     public static final StandardBundlerParam<List<String>> JVM_OPTIONS =
 286             new StandardBundlerParam<>(
 287                     I18N.getString("param.jvm-options.name"),
 288                     I18N.getString("param.jvm-options.description"),
 289                     "jvmOptions",
 290                     (Class<List<String>>) (Object) List.class,
 291                     params -> Collections.emptyList(),
 292                     (s, p) -> Arrays.asList(s.split("\\s+"))
 293             );
 294 
 295     @SuppressWarnings("unchecked")
 296     public static final StandardBundlerParam<Map<String, String>> JVM_PROPERTIES =
 297             new StandardBundlerParam<>(
 298                     I18N.getString("param.jvm-system-properties.name"),
 299                     I18N.getString("param.jvm-system-properties.description"),
 300                     "jvmProperties",
 301                     (Class<Map<String, String>>) (Object) Map.class,
 302                     params -> Collections.emptyMap(),
 303                     (s, params) -> {
 304                         Map<String, String> map = new HashMap<>();
 305                         try {
 306                             Properties p = new Properties();
 307                             p.load(new StringReader(s));
 308                             for (Map.Entry<Object, Object> entry : p.entrySet()) {
 309                                 map.put((String)entry.getKey(), (String)entry.getValue());
 310                             }
 311                         } catch (IOException e) {
 312                             e.printStackTrace();
 313                         }
 314                         return map;
 315                     }
 316             );
 317 
 318     @SuppressWarnings("unchecked")
 319     public static final StandardBundlerParam<Map<String, String>> USER_JVM_OPTIONS =
 320             new StandardBundlerParam<>(
 321                     I18N.getString("param.user-jvm-options.name"),
 322                     I18N.getString("param.user-jvm-options.description"),
 323                     "userJvmOptions",
 324                     (Class<Map<String, String>>) (Object) Map.class,
 325                     params -> Collections.emptyMap(),
 326                     (s, params) -> {
 327                         Map<String, String> map = new HashMap<>();
 328                         try {
 329                             Properties p = new Properties();
 330                             p.load(new StringReader(s));
 331                             for (Map.Entry<Object, Object> entry : p.entrySet()) {
 332                                 map.put((String)entry.getKey(), (String)entry.getValue());
 333                             }
 334                         } catch (IOException e) {
 335                             e.printStackTrace();
 336                         }
 337                         return map;
 338                     }
 339             );
 340 
 341     public static final StandardBundlerParam<String> TITLE =
 342             new StandardBundlerParam<>(
 343                     I18N.getString("param.title.name"),
 344                     I18N.getString("param.title.description"), //?? but what does it do?
 345                     BundleParams.PARAM_TITLE,
 346                     String.class,
 347                     APP_NAME::fetchFrom,
 348                     (s, p) -> s
 349             );
 350 
 351 
 352     // note that each bundler is likely to replace this one with their own converter
 353     public static final StandardBundlerParam<String> VERSION =
 354             new StandardBundlerParam<>(
 355                     I18N.getString("param.version.name"),
 356                     I18N.getString("param.version.description"),
 357                     BundleParams.PARAM_VERSION,
 358                     String.class,
 359                     params -> I18N.getString("param.version.default"),
 360                     (s, p) -> s
 361             );
 362 
 363     public static final StandardBundlerParam<Boolean> SYSTEM_WIDE =
 364             new StandardBundlerParam<>(
 365                     I18N.getString("param.system-wide.name"),
 366                     I18N.getString("param.system-wide.description"),
 367                     BundleParams.PARAM_SYSTEM_WIDE,
 368                     Boolean.class,
 369                     params -> null,
 370                     // valueOf(null) is false, and we actually do want null in some cases
 371                     (s, p) -> (s == null || "null".equalsIgnoreCase(s))? null : Boolean.valueOf(s)
 372             );
 373 
 374     public static final StandardBundlerParam<Boolean> SERVICE_HINT  =
 375             new StandardBundlerParam<>(
 376                     I18N.getString("param.service-hint.name"),
 377                     I18N.getString("param.service-hint.description"),
 378                     BundleParams.PARAM_SERVICE_HINT,
 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> START_ON_INSTALL  =
 385             new StandardBundlerParam<>(
 386                     I18N.getString("param.start-on-install.name"),
 387                     I18N.getString("param.start-on-install.description"),
 388                     "startOnInstall",
 389                     Boolean.class,
 390                     params -> false,
 391                     (s, p) -> (s == null || "null".equalsIgnoreCase(s))? false : Boolean.valueOf(s)
 392             );
 393 
 394     public static final StandardBundlerParam<Boolean> STOP_ON_UNINSTALL  =
 395             new StandardBundlerParam<>(
 396                     I18N.getString("param.stop-on-uninstall.name"),
 397                     I18N.getString("param.stop-on-uninstall.description"),
 398                     "stopOnUninstall",
 399                     Boolean.class,
 400                     params -> true,
 401                     (s, p) -> (s == null || "null".equalsIgnoreCase(s))? true : Boolean.valueOf(s)
 402             );
 403 
 404     public static final StandardBundlerParam<Boolean> RUN_AT_STARTUP  =
 405             new StandardBundlerParam<>(
 406                     I18N.getString("param.run-at-startup.name"),
 407                     I18N.getString("param.run-at-startup.description"),
 408                     "runAtStartup",
 409                     Boolean.class,
 410                     params -> false,
 411                     (s, p) -> (s == null || "null".equalsIgnoreCase(s))? false : Boolean.valueOf(s)
 412             );
 413 
 414     public static final StandardBundlerParam<Boolean> SIGN_BUNDLE  =
 415             new StandardBundlerParam<>(
 416                     I18N.getString("param.sign-bundle.name"),
 417                     I18N.getString("param.sign-bundle.description"),
 418                     "signBundle",
 419                     Boolean.class,
 420                     params -> null,
 421                     // valueOf(null) is false, and we actually do want null in some cases
 422                     (s, p) -> (s == null || "null".equalsIgnoreCase(s))? null : Boolean.valueOf(s)
 423             );
 424 
 425     public static final StandardBundlerParam<Boolean> SHORTCUT_HINT =
 426             new StandardBundlerParam<>(
 427                     I18N.getString("param.desktop-shortcut-hint.name"),
 428                     I18N.getString("param.desktop-shortcut-hint.description"),
 429                     BundleParams.PARAM_SHORTCUT,
 430                     Boolean.class,
 431                     params -> false,
 432                     // valueOf(null) is false, and we actually do want null in some cases
 433                     (s, p) -> (s == null || "null".equalsIgnoreCase(s))? false : Boolean.valueOf(s)
 434             );
 435 
 436     public static final StandardBundlerParam<Boolean> MENU_HINT =
 437             new StandardBundlerParam<>(
 438                     I18N.getString("param.menu-shortcut-hint.name"),
 439                     I18N.getString("param.menu-shortcut-hint.description"),
 440                     BundleParams.PARAM_MENU,
 441                     Boolean.class,
 442                     params -> true,
 443                     // valueOf(null) is false, and we actually do want null in some cases
 444                     (s, p) -> (s == null || "null".equalsIgnoreCase(s))? true : Boolean.valueOf(s)
 445             );
 446 
 447     @SuppressWarnings("unchecked")
 448     public static final StandardBundlerParam<List<String>> LICENSE_FILE =
 449             new StandardBundlerParam<>(
 450                     I18N.getString("param.license-file.name"),
 451                     I18N.getString("param.license-file.description"),
 452                     BundleParams.PARAM_LICENSE_FILE,
 453                     (Class<List<String>>)(Object)List.class,
 454                     params -> Collections.<String>emptyList(),
 455                     (s, p) -> Arrays.asList(s.split(","))
 456             );
 457 
 458     public static final BundlerParamInfo<String> LICENSE_TYPE =
 459             new StandardBundlerParam<>(
 460                     I18N.getString("param.license-type.name"),
 461                     I18N.getString("param.license-type.description"),
 462                     BundleParams.PARAM_LICENSE_TYPE,
 463                     String.class,
 464                     params -> I18N.getString("param.license-type.default"),
 465                     (s, p) -> s
 466             );
 467 
 468     public static final StandardBundlerParam<File> BUILD_ROOT =
 469             new StandardBundlerParam<>(
 470                     I18N.getString("param.build-root.name"),
 471                     I18N.getString("param.build-root.description"),
 472                     "buildRoot",
 473                     File.class,
 474                     params -> {
 475                         try {
 476                             return Files.createTempDirectory("fxbundler").toFile();
 477                         } catch (IOException ioe) {
 478                             return null;
 479                         }
 480                     },
 481                     (s, p) -> new File(s)
 482             );
 483 
 484     public static final StandardBundlerParam<String> IDENTIFIER =
 485             new StandardBundlerParam<>(
 486                     I18N.getString("param.identifier.name"),
 487                     I18N.getString("param.identifier.description"),
 488                     BundleParams.PARAM_IDENTIFIER,
 489                     String.class,
 490                     params -> {
 491                         String s = MAIN_CLASS.fetchFrom(params);
 492                         if (s == null) return null;
 493 
 494                         int idx = s.lastIndexOf(".");
 495                         if (idx >= 1) {
 496                             return s.substring(0, idx);
 497                         }
 498                         return s;
 499                     },
 500                     (s, p) -> s
 501             );
 502 
 503     public static final StandardBundlerParam<String> PREFERENCES_ID =
 504             new StandardBundlerParam<>(
 505                     I18N.getString("param.preferences-id.name"),
 506                     I18N.getString("param.preferences-id.description"),
 507                     "preferencesID",
 508                     String.class,
 509                     p -> Optional.ofNullable(IDENTIFIER.fetchFrom(p)).orElse("").replace('.', '/'),
 510                     (s, p) -> s
 511             );
 512 
 513     public static final StandardBundlerParam<String> PRELOADER_CLASS =
 514             new StandardBundlerParam<>(
 515                     I18N.getString("param.preloader.name"),
 516                     I18N.getString("param.preloader.description"),
 517                     "preloader",
 518                     String.class,
 519                     p -> null,
 520                     null
 521             );
 522 
 523     public static final StandardBundlerParam<Boolean> VERBOSE  =
 524             new StandardBundlerParam<>(
 525                     I18N.getString("param.verbose.name"),
 526                     I18N.getString("param.verbose.description"),
 527                     "verbose",
 528                     Boolean.class,
 529                     params -> false,
 530                     // valueOf(null) is false, and we actually do want null in some cases
 531                     (s, p) -> (s == null || "null".equalsIgnoreCase(s))? true : Boolean.valueOf(s)
 532             );
 533 
 534     public static final StandardBundlerParam<File> DROP_IN_RESOURCES_ROOT =
 535             new StandardBundlerParam<>(
 536                     I18N.getString("param.drop-in-resources-root.name"),
 537                     I18N.getString("param.drop-in-resources-root.description"),
 538                     "dropinResourcesRoot",
 539                     File.class,
 540                     params -> null,
 541                     (s, p) -> new File(s)
 542             );
 543 
 544     @SuppressWarnings("unchecked")
 545     public static final StandardBundlerParam<List<Map<String, ? super Object>>> SECONDARY_LAUNCHERS =
 546             new StandardBundlerParam<>(
 547                     I18N.getString("param.secondary-launchers.name"),
 548                     I18N.getString("param.secondary-launchers.description"),
 549                     "secondaryLaunchers",
 550                     (Class<List<Map<String, ? super Object>>>) (Object) List.class,
 551                     params -> new ArrayList<>(1),
 552                     // valueOf(null) is false, and we actually do want null in some cases
 553                     (s, p) -> null
 554             );
 555 
 556     @SuppressWarnings("unchecked")
 557     public static final StandardBundlerParam<List<Map<String, ? super Object>>> FILE_ASSOCIATIONS =
 558             new StandardBundlerParam<>(
 559                     I18N.getString("param.file-associations.name"),
 560                     I18N.getString("param.file-associations.description"),
 561                     "fileAssociations",
 562                     (Class<List<Map<String, ? super Object>>>) (Object) List.class,
 563                     params -> new ArrayList<>(1),
 564                     // valueOf(null) is false, and we actually do want null in some cases
 565                     (s, p) -> null
 566             );
 567 
 568     @SuppressWarnings("unchecked")
 569     public static final StandardBundlerParam<List<String>> FA_EXTENSIONS =
 570             new StandardBundlerParam<>(
 571                     I18N.getString("param.fa-extension.name"),
 572                     I18N.getString("param.fa-extension.description"),
 573                     "fileAssociation.extension",
 574                     (Class<List<String>>) (Object) List.class,
 575                     params -> null, // null means not matched to an extension
 576                     (s, p) -> Arrays.asList(s.split("(,|\\s)+"))
 577             );
 578 
 579     @SuppressWarnings("unchecked")
 580     public static final StandardBundlerParam<List<String>> FA_CONTENT_TYPE =
 581             new StandardBundlerParam<>(
 582                     I18N.getString("param.fa-content-type.name"),
 583                     I18N.getString("param.fa-content-type.description"),
 584                     "fileAssociation.contentType",
 585                     (Class<List<String>>) (Object) List.class,
 586                     params -> null, // null means not matched to a content/mime type
 587                     (s, p) -> Arrays.asList(s.split("(,|\\s)+"))
 588             );
 589 
 590     public static final StandardBundlerParam<String> FA_DESCRIPTION =
 591             new StandardBundlerParam<>(
 592                     I18N.getString("param.fa-description.name"),
 593                     I18N.getString("param.fa-description.description"),
 594                     "fileAssociation.description",
 595                     String.class,
 596                     params -> APP_NAME.fetchFrom(params) + " File",
 597                     null
 598             );
 599 
 600     public static final StandardBundlerParam<File> FA_ICON =
 601             new StandardBundlerParam<>(
 602                     I18N.getString("param.fa-icon.name"),
 603                     I18N.getString("param.fa-icon.description"),
 604                     "fileAssociation.icon",
 605                     File.class,
 606                     ICON::fetchFrom,
 607                     (s, p) -> new File(s)
 608             );
 609 
 610     public static final StandardBundlerParam<Boolean> UNLOCK_COMMERCIAL_FEATURES =
 611             new StandardBundlerParam<>(
 612                     I18N.getString("param.commercial-features.name"),
 613                     I18N.getString("param.commercial-features.description"),
 614                     "commercialFeatures",
 615                     Boolean.class,
 616                     p -> false,
 617                     (s, p) -> Boolean.parseBoolean(s)
 618             );
 619 
 620     public static final StandardBundlerParam<Boolean> ENABLE_APP_CDS =
 621             new StandardBundlerParam<>(
 622                     I18N.getString("param.com-app-cds.name"),
 623                     I18N.getString("param.com-app-cds.description"),
 624                     "commercial.AppCDS",
 625                     Boolean.class,
 626                     p -> false,
 627                     (s, p) -> Boolean.parseBoolean(s)
 628             );
 629 
 630     public static final StandardBundlerParam<String> APP_CDS_CACHE_MODE =
 631             new StandardBundlerParam<>(
 632                     I18N.getString("param.com-app-cds-cache-mode.name"),
 633                     I18N.getString("param.com-app-cds-cache-mode.description"),
 634                     "commercial.AppCDS.cache",
 635                     String.class,
 636                     p -> "auto",
 637                     (s, p) -> s
 638             );
 639 
 640     @SuppressWarnings("unchecked")
 641     public static final StandardBundlerParam<List<String>> APP_CDS_CLASS_ROOTS =
 642             new StandardBundlerParam<>(
 643                     I18N.getString("param.com-app-cds-root.name"),
 644                     I18N.getString("param.com-app-cds-root.description"),
 645                     "commercial.AppCDS.classRoots",
 646                     (Class<List<String>>)((Object)List.class),
 647                     p -> Collections.singletonList(MAIN_CLASS.fetchFrom(p)),
 648                     (s, p) -> Arrays.asList(s.split("[ ,:]"))
 649             );
 650 
 651     public static void extractMainClassInfoFromAppResources(Map<String, ? super Object> params) {
 652         boolean hasMainClass = params.containsKey(MAIN_CLASS.getID());
 653         boolean hasMainJar = params.containsKey(MAIN_JAR.getID());
 654         boolean hasMainJarClassPath = params.containsKey(CLASSPATH.getID());
 655         boolean hasPreloader = params.containsKey(PRELOADER_CLASS.getID());
 656 
 657         if (hasMainClass && hasMainJar && hasMainJarClassPath) {
 658             return;
 659         }
 660         // it's a pair.  The [0] is the srcdir [1] is the file relative to sourcedir
 661         List<String[]> filesToCheck = new ArrayList<>();
 662 
 663         if (hasMainJar) {
 664             RelativeFileSet rfs = MAIN_JAR.fetchFrom(params);
 665             for (String s : rfs.getIncludedFiles()) {
 666                 filesToCheck.add(new String[]{rfs.getBaseDirectory().toString(), s});
 667             }
 668         } else if (hasMainJarClassPath) {
 669             for (String s : CLASSPATH.fetchFrom(params).split("\\s+")) {
 670                 filesToCheck.add(new String[] {APP_RESOURCES.fetchFrom(params).getBaseDirectory().toString(), s});
 671             }
 672         } else {
 673             List<RelativeFileSet> rfsl = APP_RESOURCES_LIST.fetchFrom(params);
 674             if (rfsl == null || rfsl.isEmpty()) {
 675                 return;
 676             }
 677             for (RelativeFileSet rfs : rfsl) {
 678                 if (rfs == null) continue;
 679 
 680                 for (String s : rfs.getIncludedFiles()) {
 681                     filesToCheck.add(new String[]{rfs.getBaseDirectory().toString(), s});
 682                 }
 683             }
 684         }
 685 
 686         String declaredMainClass = (String) params.get(MAIN_CLASS.getID());
 687 
 688         // presume the set iterates in-order
 689         for (String[] fnames : filesToCheck) {
 690             try {
 691                 // only sniff jars
 692                 if (!fnames[1].toLowerCase().endsWith(".jar")) continue;
 693 
 694                 File file = new File(fnames[0], fnames[1]);
 695                 // that actually exist
 696                 if (!file.exists()) continue;
 697 
 698                 try (JarFile jf = new JarFile(file)) {
 699                     Manifest m = jf.getManifest();
 700                     Attributes attrs = (m != null) ? m.getMainAttributes() : null;
 701 
 702                     if (attrs != null) {
 703                         String mainClass = attrs.getValue(Attributes.Name.MAIN_CLASS);
 704                         String fxMain = attrs.getValue(MANIFEST_JAVAFX_MAIN);
 705                         String preloaderClass = attrs.getValue(MANIFEST_PRELOADER);
 706                         if (hasMainClass) {
 707                             if (declaredMainClass.equals(fxMain)) {
 708                                 params.put(USE_FX_PACKAGING.getID(), true);
 709                             } else if (declaredMainClass.equals(mainClass)) {
 710                                 params.put(USE_FX_PACKAGING.getID(), false);
 711                             } else {
 712                                 if (fxMain != null) {
 713                                     Log.info(MessageFormat.format(I18N.getString("message.fx-app-does-not-match-specified-main"), fnames[1], fxMain, declaredMainClass));
 714                                 }
 715                                 if (mainClass != null) {
 716                                     Log.info(MessageFormat.format(I18N.getString("message.main-class-does-not-match-specified-main"), fnames[1], mainClass, declaredMainClass));
 717                                 }
 718                                 continue;
 719                             }
 720                         } else {
 721                             if (fxMain != null) {
 722                                 params.put(USE_FX_PACKAGING.getID(), true);
 723                                 params.put(MAIN_CLASS.getID(), fxMain);
 724                             } else if (mainClass != null) {
 725                                 params.put(USE_FX_PACKAGING.getID(), false);
 726                                 params.put(MAIN_CLASS.getID(), mainClass);
 727                             } else {
 728                                 continue;
 729                             }
 730                         }
 731                         if (!hasPreloader && preloaderClass != null) {
 732                             params.put(PRELOADER_CLASS.getID(), preloaderClass);
 733                         }
 734                         if (!hasMainJar) {
 735                             if (fnames[0] == null) {
 736                                 fnames[0] = file.getParentFile().toString();
 737                             }
 738                             params.put(MAIN_JAR.getID(), new RelativeFileSet(new File(fnames[0]), new LinkedHashSet<>(Collections.singletonList(file))));
 739                         }
 740                         if (!hasMainJarClassPath) {
 741                             String cp = attrs.getValue(Attributes.Name.CLASS_PATH);
 742                             params.put(CLASSPATH.getID(), cp == null ? "" : cp);
 743                         }
 744                         break;
 745                     }
 746                 }
 747             } catch (IOException ignore) {
 748                 ignore.printStackTrace();
 749             }
 750         }
 751     }
 752 
 753     public static void validateMainClassInfoFromAppResources(Map<String, ? super Object> params) throws ConfigException {
 754         boolean hasMainClass = params.containsKey(MAIN_CLASS.getID());
 755         boolean hasMainJar = params.containsKey(MAIN_JAR.getID());
 756         boolean hasMainJarClassPath = params.containsKey(CLASSPATH.getID());
 757 
 758         if (hasMainClass && hasMainJar && hasMainJarClassPath) {
 759             return;
 760         }
 761 
 762         extractMainClassInfoFromAppResources(params);
 763         if (!params.containsKey(MAIN_CLASS.getID())) {
 764             if (hasMainJar) {
 765                 throw new ConfigException(
 766                         MessageFormat.format(I18N.getString("error.no-main-class-with-main-jar"),
 767                                 MAIN_JAR.fetchFrom(params)),
 768                         MessageFormat.format(I18N.getString("error.no-main-class-with-main-jar.advice"),
 769                                 MAIN_JAR.fetchFrom(params)));
 770             } else if (hasMainJarClassPath) {
 771                 throw new ConfigException(
 772                         I18N.getString("error.no-main-class-with-classpath"),
 773                         I18N.getString("error.no-main-class-with-classpath.advice"));
 774             } else {
 775                 throw new ConfigException(
 776                         I18N.getString("error.no-main-class"),
 777                         I18N.getString("error.no-main-class.advice"));
 778             }
 779         }
 780     }
 781 
 782 
 783     private static List<String> splitStringWithEscapes(String s) {
 784         List<String> l = new ArrayList<>();
 785         StringBuilder current = new StringBuilder();
 786         boolean quoted = false;
 787         boolean escaped = false;
 788         for (char c : s.toCharArray()) {
 789             if (escaped) {
 790                 current.append(c);
 791             } else if ('"' == c) {
 792                 quoted = !quoted;
 793             } else if (!quoted && Character.isWhitespace(c)) {
 794                 l.add(current.toString());
 795                 current = new StringBuilder();
 796             } else {
 797                 current.append(c);
 798             }
 799         }
 800         l.add(current.toString());
 801         return l;
 802     }
 803 }