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