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