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