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