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