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 java.io.File;
  29 import java.io.IOException;
  30 import java.io.StringReader;
  31 import java.nio.file.Files;
  32 import java.nio.file.Path;
  33 import java.nio.file.Paths;
  34 import java.text.MessageFormat;
  35 import java.util.ArrayList;
  36 import java.util.Arrays;
  37 import java.util.Collections;
  38 import java.util.Date;
  39 import java.util.HashMap;
  40 import java.util.HashSet;
  41 import java.util.LinkedHashSet;
  42 import java.util.List;
  43 import java.util.Map;
  44 import java.util.Optional;
  45 import java.util.Properties;
  46 import java.util.ResourceBundle;
  47 import java.util.Set;
  48 import java.util.HashSet;
  49 import java.util.function.BiFunction;
  50 import java.util.function.Function;
  51 import java.util.jar.Attributes;
  52 import java.util.jar.JarFile;
  53 import java.util.jar.Manifest;
  54 import java.util.regex.Pattern;
  55 import java.util.stream.Collectors;
  56 
  57 /**
  58  * StandardBundlerParam
  59  *
  60  * A parameter to a bundler.
  61  *
  62  * Also contains static definitions of all of the common bundler parameters.
  63  * (additional platform specific and mode specific bundler parameters
  64  * are defined in each of the specific bundlers)
  65  *
  66  * Also contains static methods that operate on maps of parameters.
  67  */
  68 class StandardBundlerParam<T> extends BundlerParamInfo<T> {
  69 
  70     private static final ResourceBundle I18N = ResourceBundle.getBundle(
  71             "jdk.jpackage.internal.resources.MainResources");
  72     private static final String JAVABASEJMOD = "java.base.jmod";
  73 
  74     StandardBundlerParam(String id, Class<T> valueType,
  75             Function<Map<String, ? super Object>, T> defaultValueFunction,
  76             BiFunction<String, Map<String, ? super Object>, T> stringConverter)
  77     {
  78         this.id = id;
  79         this.valueType = valueType;
  80         this.defaultValueFunction = defaultValueFunction;
  81         this.stringConverter = stringConverter;
  82     }
  83 
  84     static final StandardBundlerParam<RelativeFileSet> APP_RESOURCES =
  85             new StandardBundlerParam<>(
  86                     BundleParams.PARAM_APP_RESOURCES,
  87                     RelativeFileSet.class,
  88                     null, // no default.  Required parameter
  89                     null  // no string translation,
  90                           // tool must provide complex type
  91             );
  92 
  93     @SuppressWarnings("unchecked")
  94     static final
  95             StandardBundlerParam<List<RelativeFileSet>> APP_RESOURCES_LIST =
  96             new StandardBundlerParam<>(
  97                     BundleParams.PARAM_APP_RESOURCES + "List",
  98                     (Class<List<RelativeFileSet>>) (Object) List.class,
  99                     // Default is appResources, as a single item list
 100                     p -> new ArrayList<>(Collections.singletonList(
 101                             APP_RESOURCES.fetchFrom(p))),
 102                     StandardBundlerParam::createAppResourcesListFromString
 103             );
 104 
 105     static final StandardBundlerParam<String> SOURCE_DIR =
 106             new StandardBundlerParam<>(
 107                     Arguments.CLIOptions.INPUT.getId(),
 108                     String.class,
 109                     p -> null,
 110                     (s, p) -> {
 111                         String value = String.valueOf(s);
 112                         if (value.charAt(value.length() - 1) ==
 113                                 File.separatorChar) {
 114                             return value.substring(0, value.length() - 1);
 115                         }
 116                         else {
 117                             return value;
 118                         }
 119                     }
 120             );
 121 
 122     // note that each bundler is likely to replace this one with
 123     // their own converter
 124     static final StandardBundlerParam<RelativeFileSet> MAIN_JAR =
 125             new StandardBundlerParam<>(
 126                     Arguments.CLIOptions.MAIN_JAR.getId(),
 127                     RelativeFileSet.class,
 128                     params -> {
 129                         extractMainClassInfoFromAppResources(params);
 130                         return (RelativeFileSet) params.get("mainJar");
 131                     },
 132                     (s, p) -> getMainJar(s, p)
 133             );
 134 
 135     static final StandardBundlerParam<String> CLASSPATH =
 136             new StandardBundlerParam<>(
 137                     "classpath",
 138                     String.class,
 139                     params -> {
 140                         extractMainClassInfoFromAppResources(params);
 141                         String cp = (String) params.get("classpath");
 142                         return cp == null ? "" : cp;
 143                     },
 144                     (s, p) -> s
 145             );
 146 
 147     static final StandardBundlerParam<String> MAIN_CLASS =
 148             new StandardBundlerParam<>(
 149                     Arguments.CLIOptions.APPCLASS.getId(),
 150                     String.class,
 151                     params -> {
 152                         if (isRuntimeInstaller(params)) {
 153                             return null;
 154                         }
 155                         extractMainClassInfoFromAppResources(params);
 156                         String s = (String) params.get(
 157                                 BundleParams.PARAM_APPLICATION_CLASS);
 158                         if (s == null) {
 159                             s = JLinkBundlerHelper.getMainClass(params);
 160                             if (s.length() == 0) {
 161                                 s = null;
 162                             }
 163                         }
 164                         return s;
 165                     },
 166                     (s, p) -> s
 167             );
 168 
 169     static final StandardBundlerParam<File> PREDEFINED_RUNTIME_IMAGE =
 170             new StandardBundlerParam<>(
 171                     Arguments.CLIOptions.PREDEFINED_RUNTIME_IMAGE.getId(),
 172                     File.class,
 173                     params -> null,
 174                     (s, p) -> new File(s)
 175             );
 176 
 177     static final StandardBundlerParam<String> APP_NAME =
 178             new StandardBundlerParam<>(
 179                     Arguments.CLIOptions.NAME.getId(),
 180                     String.class,
 181                     params -> {
 182                         String s = MAIN_CLASS.fetchFrom(params);
 183                         if (s != null) {
 184                             int idx = s.lastIndexOf(".");
 185                             if (idx >= 0) {
 186                                 return s.substring(idx+1);
 187                             }
 188                             return s;
 189                         } else if (isRuntimeInstaller(params)) {
 190                             File f = PREDEFINED_RUNTIME_IMAGE.fetchFrom(params);
 191                             if (f != null) {
 192                                 return f.getName();
 193                             }
 194                         }
 195                         return null;
 196                     },
 197                     (s, p) -> s
 198             );
 199 
 200     static final StandardBundlerParam<File> ICON =
 201             new StandardBundlerParam<>(
 202                     Arguments.CLIOptions.ICON.getId(),
 203                     File.class,
 204                     params -> null,
 205                     (s, p) -> new File(s)
 206             );
 207 
 208     static final StandardBundlerParam<String> VENDOR =
 209             new StandardBundlerParam<>(
 210                     Arguments.CLIOptions.VENDOR.getId(),
 211                     String.class,
 212                     params -> I18N.getString("param.vendor.default"),
 213                     (s, p) -> s
 214             );
 215 
 216     static final StandardBundlerParam<String> DESCRIPTION =
 217             new StandardBundlerParam<>(
 218                     Arguments.CLIOptions.DESCRIPTION.getId(),
 219                     String.class,
 220                     params -> params.containsKey(APP_NAME.getID())
 221                             ? APP_NAME.fetchFrom(params)
 222                             : I18N.getString("param.description.default"),
 223                     (s, p) -> s
 224             );
 225 
 226     static final StandardBundlerParam<String> COPYRIGHT =
 227             new StandardBundlerParam<>(
 228                     Arguments.CLIOptions.COPYRIGHT.getId(),
 229                     String.class,
 230                     params -> MessageFormat.format(I18N.getString(
 231                             "param.copyright.default"), new Date()),
 232                     (s, p) -> s
 233             );
 234 
 235     @SuppressWarnings("unchecked")
 236     static final StandardBundlerParam<List<String>> ARGUMENTS =
 237             new StandardBundlerParam<>(
 238                     Arguments.CLIOptions.ARGUMENTS.getId(),
 239                     (Class<List<String>>) (Object) List.class,
 240                     params -> Collections.emptyList(),
 241                     (s, p) -> null
 242             );
 243 
 244     @SuppressWarnings("unchecked")
 245     static final StandardBundlerParam<List<String>> JAVA_OPTIONS =
 246             new StandardBundlerParam<>(
 247                     Arguments.CLIOptions.JAVA_OPTIONS.getId(),
 248                     (Class<List<String>>) (Object) List.class,
 249                     params -> Collections.emptyList(),
 250                     (s, p) -> Arrays.asList(s.split("\n\n"))
 251             );
 252 
 253     // note that each bundler is likely to replace this one with
 254     // their own converter
 255     static final StandardBundlerParam<String> VERSION =
 256             new StandardBundlerParam<>(
 257                     Arguments.CLIOptions.VERSION.getId(),
 258                     String.class,
 259                     params -> I18N.getString("param.version.default"),
 260                     (s, p) -> s
 261             );
 262 
 263     @SuppressWarnings("unchecked")
 264     public static final StandardBundlerParam<String> LICENSE_FILE =
 265             new StandardBundlerParam<>(
 266                     Arguments.CLIOptions.LICENSE_FILE.getId(),
 267                     String.class,
 268                     params -> null,
 269                     (s, p) -> s
 270             );
 271 
 272     static final StandardBundlerParam<File> TEMP_ROOT =
 273             new StandardBundlerParam<>(
 274                     Arguments.CLIOptions.TEMP_ROOT.getId(),
 275                     File.class,
 276                     params -> {
 277                         try {
 278                             return Files.createTempDirectory(
 279                                     "jdk.jpackage").toFile();
 280                         } catch (IOException ioe) {
 281                             return null;
 282                         }
 283                     },
 284                     (s, p) -> new File(s)
 285             );
 286 
 287     public static final StandardBundlerParam<File> CONFIG_ROOT =
 288             new StandardBundlerParam<>(
 289                 "configRoot",
 290                 File.class,
 291                 params -> {
 292                     File root =
 293                             new File(TEMP_ROOT.fetchFrom(params), "config");
 294                     root.mkdirs();
 295                     return root;
 296                 },
 297                 (s, p) -> null
 298             );
 299 
 300     static final StandardBundlerParam<String> IDENTIFIER =
 301             new StandardBundlerParam<>(
 302                     Arguments.CLIOptions.IDENTIFIER.getId(),
 303                     String.class,
 304                     params -> {
 305                         String s = MAIN_CLASS.fetchFrom(params);
 306                         if (s == null) return null;
 307 
 308                         int idx = s.lastIndexOf(".");
 309                         if (idx >= 1) {
 310                             return s.substring(0, idx);
 311                         }
 312                         return s;
 313                     },
 314                     (s, p) -> s
 315             );
 316 
 317     static final StandardBundlerParam<Boolean> VERBOSE  =
 318             new StandardBundlerParam<>(
 319                     Arguments.CLIOptions.VERBOSE.getId(),
 320                     Boolean.class,
 321                     params -> false,
 322                     // valueOf(null) is false, and we actually do want null
 323                     (s, p) -> (s == null || "null".equalsIgnoreCase(s)) ?
 324                             true : Boolean.valueOf(s)
 325             );
 326 
 327     static final StandardBundlerParam<File> RESOURCE_DIR =
 328             new StandardBundlerParam<>(
 329                     Arguments.CLIOptions.RESOURCE_DIR.getId(),
 330                     File.class,
 331                     params -> null,
 332                     (s, p) -> new File(s)
 333             );
 334 
 335     static final BundlerParamInfo<String> INSTALL_DIR =
 336             new StandardBundlerParam<>(
 337                     Arguments.CLIOptions.INSTALL_DIR.getId(),
 338                     String.class,
 339                      params -> null,
 340                     (s, p) -> s
 341     );
 342 
 343     static final StandardBundlerParam<File> PREDEFINED_APP_IMAGE =
 344             new StandardBundlerParam<>(
 345             Arguments.CLIOptions.PREDEFINED_APP_IMAGE.getId(),
 346             File.class,
 347             params -> null,
 348             (s, p) -> new File(s));
 349 
 350     @SuppressWarnings("unchecked")
 351     static final StandardBundlerParam<List<Map<String, ? super Object>>> ADD_LAUNCHERS =
 352             new StandardBundlerParam<>(
 353                     Arguments.CLIOptions.ADD_LAUNCHER.getId(),
 354                     (Class<List<Map<String, ? super Object>>>) (Object)
 355                             List.class,
 356                     params -> new ArrayList<>(1),
 357                     // valueOf(null) is false, and we actually do want null
 358                     (s, p) -> null
 359             );
 360 
 361     @SuppressWarnings("unchecked")
 362     static final StandardBundlerParam
 363             <List<Map<String, ? super Object>>> FILE_ASSOCIATIONS =
 364             new StandardBundlerParam<>(
 365                     Arguments.CLIOptions.FILE_ASSOCIATIONS.getId(),
 366                     (Class<List<Map<String, ? super Object>>>) (Object)
 367                             List.class,
 368                     params -> new ArrayList<>(1),
 369                     // valueOf(null) is false, and we actually do want null
 370                     (s, p) -> null
 371             );
 372 
 373     @SuppressWarnings("unchecked")
 374     static final StandardBundlerParam<List<String>> FA_EXTENSIONS =
 375             new StandardBundlerParam<>(
 376                     "fileAssociation.extension",
 377                     (Class<List<String>>) (Object) List.class,
 378                     params -> null, // null means not matched to an extension
 379                     (s, p) -> Arrays.asList(s.split("(,|\\s)+"))
 380             );
 381 
 382     @SuppressWarnings("unchecked")
 383     static final StandardBundlerParam<List<String>> FA_CONTENT_TYPE =
 384             new StandardBundlerParam<>(
 385                     "fileAssociation.contentType",
 386                     (Class<List<String>>) (Object) List.class,
 387                     params -> null,
 388                             // null means not matched to a content/mime type
 389                     (s, p) -> Arrays.asList(s.split("(,|\\s)+"))
 390             );
 391 
 392     static final StandardBundlerParam<String> FA_DESCRIPTION =
 393             new StandardBundlerParam<>(
 394                     "fileAssociation.description",
 395                     String.class,
 396                     params -> APP_NAME.fetchFrom(params) + " File",
 397                     null
 398             );
 399 
 400     static final StandardBundlerParam<File> FA_ICON =
 401             new StandardBundlerParam<>(
 402                     "fileAssociation.icon",
 403                     File.class,
 404                     ICON::fetchFrom,
 405                     (s, p) -> new File(s)
 406             );
 407 
 408     @SuppressWarnings("unchecked")
 409     static final BundlerParamInfo<List<Path>> MODULE_PATH =
 410             new StandardBundlerParam<>(
 411                     Arguments.CLIOptions.MODULE_PATH.getId(),
 412                     (Class<List<Path>>) (Object)List.class,
 413                     p -> { return getDefaultModulePath(); },
 414                     (s, p) -> {
 415                         List<Path> modulePath = Arrays.asList(s
 416                                 .split(File.pathSeparator)).stream()
 417                                 .map(ss -> new File(ss).toPath())
 418                                 .collect(Collectors.toList());
 419                         Path javaBasePath = null;
 420                         if (modulePath != null) {
 421                             javaBasePath = JLinkBundlerHelper
 422                                     .findPathOfModule(modulePath, JAVABASEJMOD);
 423                         } else {
 424                             modulePath = new ArrayList<Path>();
 425                         }
 426 
 427                         // Add the default JDK module path to the module path.
 428                         if (javaBasePath == null) {
 429                             List<Path> jdkModulePath = getDefaultModulePath();
 430 
 431                             if (jdkModulePath != null) {
 432                                 modulePath.addAll(jdkModulePath);
 433                                 javaBasePath =
 434                                         JLinkBundlerHelper.findPathOfModule(
 435                                         modulePath, JAVABASEJMOD);
 436                             }
 437                         }
 438 
 439                         if (javaBasePath == null ||
 440                                 !Files.exists(javaBasePath)) {
 441                             Log.error(String.format(I18N.getString(
 442                                     "warning.no.jdk.modules.found")));
 443                         }
 444 
 445                         return modulePath;
 446                     });
 447 
 448     static final BundlerParamInfo<String> MODULE =
 449             new StandardBundlerParam<>(
 450                     Arguments.CLIOptions.MODULE.getId(),
 451                     String.class,
 452                     p -> null,
 453                     (s, p) -> {
 454                         return String.valueOf(s);
 455                     });
 456 
 457     @SuppressWarnings("unchecked")
 458     static final BundlerParamInfo<Set<String>> ADD_MODULES =
 459             new StandardBundlerParam<>(
 460                     Arguments.CLIOptions.ADD_MODULES.getId(),
 461                     (Class<Set<String>>) (Object) Set.class,
 462                     p -> new LinkedHashSet<String>(),
 463                     (s, p) -> new LinkedHashSet<>(Arrays.asList(s.split(",")))
 464             );
 465 
 466     @SuppressWarnings("unchecked")
 467     static final BundlerParamInfo<Set<String>> LIMIT_MODULES =
 468             new StandardBundlerParam<>(
 469                     "limit-modules",
 470                     (Class<Set<String>>) (Object) Set.class,
 471                     p -> new LinkedHashSet<String>(),
 472                     (s, p) -> new LinkedHashSet<>(Arrays.asList(s.split(",")))
 473             );
 474 
 475     static boolean isRuntimeInstaller(Map<String, ? super Object> p) {
 476         if (p.containsKey(MODULE.getID()) ||
 477                 p.containsKey(MAIN_JAR.getID()) ||
 478                 p.containsKey(PREDEFINED_APP_IMAGE.getID())) {
 479             return false; // we are building or are given an application
 480         }
 481         // runtime installer requires --runtime-image, if this is false
 482         // here then we should have thrown error validating args.
 483         return p.containsKey(PREDEFINED_RUNTIME_IMAGE.getID());
 484     }
 485 
 486     static File getPredefinedAppImage(Map<String, ? super Object> p) {
 487         File applicationImage = null;
 488         if (PREDEFINED_APP_IMAGE.fetchFrom(p) != null) {
 489             applicationImage = PREDEFINED_APP_IMAGE.fetchFrom(p);
 490             Log.debug("Using App Image from " + applicationImage);
 491             if (!applicationImage.exists()) {
 492                 throw new RuntimeException(
 493                         MessageFormat.format(I18N.getString(
 494                                 "message.app-image-dir-does-not-exist"),
 495                                 PREDEFINED_APP_IMAGE.getID(),
 496                                 applicationImage.toString()));
 497             }
 498         }
 499         return applicationImage;
 500     }
 501 
 502     static void copyPredefinedRuntimeImage(
 503             Map<String, ? super Object> p,
 504             AbstractAppImageBuilder appBuilder)
 505             throws IOException , ConfigException {
 506         File image = PREDEFINED_RUNTIME_IMAGE.fetchFrom(p);
 507         if (!image.exists()) {
 508             throw new ConfigException(
 509                     MessageFormat.format(I18N.getString(
 510                     "message.runtime-image-dir-does-not-exist"),
 511                     PREDEFINED_RUNTIME_IMAGE.getID(),
 512                     image.toString()),
 513                     MessageFormat.format(I18N.getString(
 514                     "message.runtime-image-dir-does-not-exist.advice"),
 515                     PREDEFINED_RUNTIME_IMAGE.getID()));
 516         }
 517         // copy whole runtime, need to skip jmods and src.zip
 518         final List<String> excludes = Arrays.asList("jmods", "src.zip");
 519         IOUtils.copyRecursive(image.toPath(), appBuilder.getRoot(), excludes);
 520 
 521         // if module-path given - copy modules to appDir/mods
 522         List<Path> modulePath =
 523                 StandardBundlerParam.MODULE_PATH.fetchFrom(p);
 524         List<Path> defaultModulePath = getDefaultModulePath();
 525         Path dest = appBuilder.getAppModsDir();
 526 
 527         if (dest != null) {
 528             for (Path mp : modulePath) {
 529                 if (!defaultModulePath.contains(mp)) {
 530                     Files.createDirectories(dest);
 531                     IOUtils.copyRecursive(mp, dest);
 532                 }
 533             }
 534         }
 535 
 536         appBuilder.prepareApplicationFiles();
 537     }
 538 
 539     static void extractMainClassInfoFromAppResources(
 540             Map<String, ? super Object> params) {
 541         boolean hasMainClass = params.containsKey(MAIN_CLASS.getID());
 542         boolean hasMainJar = params.containsKey(MAIN_JAR.getID());
 543         boolean hasMainJarClassPath = params.containsKey(CLASSPATH.getID());
 544         boolean hasModule = params.containsKey(MODULE.getID());
 545 
 546         if (hasMainClass && hasMainJar && hasMainJarClassPath || hasModule ||
 547                 isRuntimeInstaller(params)) {
 548             return;
 549         }
 550 
 551         // it's a pair.
 552         // The [0] is the srcdir [1] is the file relative to sourcedir
 553         List<String[]> filesToCheck = new ArrayList<>();
 554 
 555         if (hasMainJar) {
 556             RelativeFileSet rfs = MAIN_JAR.fetchFrom(params);
 557             for (String s : rfs.getIncludedFiles()) {
 558                 filesToCheck.add(
 559                         new String[] {rfs.getBaseDirectory().toString(), s});
 560             }
 561         } else if (hasMainJarClassPath) {
 562             for (String s : CLASSPATH.fetchFrom(params).split("\\s+")) {
 563                 if (APP_RESOURCES.fetchFrom(params) != null) {
 564                     filesToCheck.add(
 565                             new String[] {APP_RESOURCES.fetchFrom(params)
 566                             .getBaseDirectory().toString(), s});
 567                 }
 568             }
 569         } else {
 570             List<RelativeFileSet> rfsl = APP_RESOURCES_LIST.fetchFrom(params);
 571             if (rfsl == null || rfsl.isEmpty()) {
 572                 return;
 573             }
 574             for (RelativeFileSet rfs : rfsl) {
 575                 if (rfs == null) continue;
 576 
 577                 for (String s : rfs.getIncludedFiles()) {
 578                     filesToCheck.add(
 579                             new String[]{rfs.getBaseDirectory().toString(), s});
 580                 }
 581             }
 582         }
 583 
 584         // presume the set iterates in-order
 585         for (String[] fnames : filesToCheck) {
 586             try {
 587                 // only sniff jars
 588                 if (!fnames[1].toLowerCase().endsWith(".jar")) continue;
 589 
 590                 File file = new File(fnames[0], fnames[1]);
 591                 // that actually exist
 592                 if (!file.exists()) continue;
 593 
 594                 try (JarFile jf = new JarFile(file)) {
 595                     Manifest m = jf.getManifest();
 596                     Attributes attrs = (m != null) ?
 597                             m.getMainAttributes() : null;
 598 
 599                     if (attrs != null) {
 600                         if (!hasMainJar) {
 601                             if (fnames[0] == null) {
 602                                 fnames[0] = file.getParentFile().toString();
 603                             }
 604                             params.put(MAIN_JAR.getID(), new RelativeFileSet(
 605                                     new File(fnames[0]),
 606                                     new LinkedHashSet<>(Collections
 607                                     .singletonList(file))));
 608                         }
 609                         if (!hasMainJarClassPath) {
 610                             String cp =
 611                                     attrs.getValue(Attributes.Name.CLASS_PATH);
 612                             params.put(CLASSPATH.getID(),
 613                                     cp == null ? "" : cp);
 614                         }
 615                         break;
 616                     }
 617                 }
 618             } catch (IOException ignore) {
 619                 ignore.printStackTrace();
 620             }
 621         }
 622     }
 623 
 624     static void validateMainClassInfoFromAppResources(
 625             Map<String, ? super Object> params) throws ConfigException {
 626         boolean hasMainClass = params.containsKey(MAIN_CLASS.getID());
 627         boolean hasMainJar = params.containsKey(MAIN_JAR.getID());
 628         boolean hasMainJarClassPath = params.containsKey(CLASSPATH.getID());
 629         boolean hasModule = params.containsKey(MODULE.getID());
 630         boolean hasAppImage = params.containsKey(PREDEFINED_APP_IMAGE.getID());
 631 
 632         if (hasMainClass && hasMainJar && hasMainJarClassPath ||
 633                hasModule || hasAppImage || isRuntimeInstaller(params)) {
 634             return;
 635         }
 636 
 637         extractMainClassInfoFromAppResources(params);
 638 
 639         if (!params.containsKey(MAIN_CLASS.getID())) {
 640             if (hasMainJar) {
 641                 throw new ConfigException(
 642                         MessageFormat.format(I18N.getString(
 643                         "error.no-main-class-with-main-jar"),
 644                         MAIN_JAR.fetchFrom(params)),
 645                         MessageFormat.format(I18N.getString(
 646                         "error.no-main-class-with-main-jar.advice"),
 647                         MAIN_JAR.fetchFrom(params)));
 648             } else {
 649                 throw new ConfigException(
 650                         I18N.getString("error.no-main-class"),
 651                         I18N.getString("error.no-main-class.advice"));
 652             }
 653         }
 654     }
 655 
 656     private static List<RelativeFileSet>
 657             createAppResourcesListFromString(String s,
 658             Map<String, ? super Object> objectObjectMap) {
 659         List<RelativeFileSet> result = new ArrayList<>();
 660         for (String path : s.split("[:;]")) {
 661             File f = new File(path);
 662             if (f.getName().equals("*") || path.endsWith("/") ||
 663                     path.endsWith("\\")) {
 664                 if (f.getName().equals("*")) {
 665                     f = f.getParentFile();
 666                 }
 667                 Set<File> theFiles = new HashSet<>();
 668                 try {
 669                     Files.walk(f.toPath())
 670                             .filter(Files::isRegularFile)
 671                             .forEach(p -> theFiles.add(p.toFile()));
 672                 } catch (IOException e) {
 673                     e.printStackTrace();
 674                 }
 675                 result.add(new RelativeFileSet(f, theFiles));
 676             } else {
 677                 result.add(new RelativeFileSet(f.getParentFile(),
 678                         Collections.singleton(f)));
 679             }
 680         }
 681         return result;
 682     }
 683 
 684     private static RelativeFileSet getMainJar(
 685             String mainJarValue, Map<String, ? super Object> params) {
 686         for (RelativeFileSet rfs : APP_RESOURCES_LIST.fetchFrom(params)) {
 687             File appResourcesRoot = rfs.getBaseDirectory();
 688             File mainJarFile = new File(appResourcesRoot, mainJarValue);
 689 
 690             if (mainJarFile.exists()) {
 691                 return new RelativeFileSet(appResourcesRoot,
 692                      new LinkedHashSet<>(Collections.singletonList(
 693                      mainJarFile)));
 694             }
 695             mainJarFile = new File(mainJarValue);
 696             if (mainJarFile.exists()) {
 697                 // absolute path for main-jar may fail is not legal
 698                 // below contains explicit error message.
 699             } else {
 700                 List<Path> modulePath = MODULE_PATH.fetchFrom(params);
 701                 modulePath.removeAll(getDefaultModulePath());
 702                 if (!modulePath.isEmpty()) {
 703                     Path modularJarPath = JLinkBundlerHelper.findPathOfModule(
 704                             modulePath, mainJarValue);
 705                     if (modularJarPath != null &&
 706                             Files.exists(modularJarPath)) {
 707                         return new RelativeFileSet(appResourcesRoot,
 708                                 new LinkedHashSet<>(Collections.singletonList(
 709                                 modularJarPath.toFile())));
 710                     }
 711                 }
 712             }
 713         }
 714 
 715         throw new IllegalArgumentException(
 716                 new ConfigException(MessageFormat.format(I18N.getString(
 717                         "error.main-jar-does-not-exist"),
 718                         mainJarValue), I18N.getString(
 719                         "error.main-jar-does-not-exist.advice")));
 720     }
 721 
 722     static List<Path> getDefaultModulePath() {
 723         List<Path> result = new ArrayList<Path>();
 724         Path jdkModulePath = Paths.get(
 725                 System.getProperty("java.home"), "jmods").toAbsolutePath();
 726 
 727         if (jdkModulePath != null && Files.exists(jdkModulePath)) {
 728             result.add(jdkModulePath);
 729         }
 730         else {
 731             // On a developer build the JDK Home isn't where we expect it
 732             // relative to the jmods directory. Do some extra
 733             // processing to find it.
 734             Map<String, String> env = System.getenv();
 735 
 736             if (env.containsKey("JDK_HOME")) {
 737                 jdkModulePath = Paths.get(env.get("JDK_HOME"),
 738                         ".." + File.separator + "images"
 739                         + File.separator + "jmods").toAbsolutePath();
 740 
 741                 if (jdkModulePath != null && Files.exists(jdkModulePath)) {
 742                     result.add(jdkModulePath);
 743                 }
 744             }
 745         }
 746 
 747         return result;
 748     }
 749 }