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