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