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