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 static final StandardBundlerParam<String> TITLE = 293 new StandardBundlerParam<>( 294 I18N.getString("param.title.name"), 295 I18N.getString("param.title.description"), 296 BundleParams.PARAM_TITLE, 297 String.class, 298 APP_NAME::fetchFrom, 299 (s, p) -> s 300 ); 301 302 // note that each bundler is likely to replace this one with 303 // their own converter 304 static final StandardBundlerParam<String> VERSION = 305 new StandardBundlerParam<>( 306 I18N.getString("param.version.name"), 307 I18N.getString("param.version.description"), 308 Arguments.CLIOptions.VERSION.getId(), 309 String.class, 310 params -> I18N.getString("param.version.default"), 311 (s, p) -> s 312 ); 313 314 @SuppressWarnings("unchecked") 315 public static final StandardBundlerParam<String> LICENSE_FILE = 316 new StandardBundlerParam<>( 317 I18N.getString("param.license-file.name"), 318 I18N.getString("param.license-file.description"), 319 Arguments.CLIOptions.LICENSE_FILE.getId(), 320 String.class, 321 params -> null, 322 (s, p) -> s 323 ); 324 325 static final StandardBundlerParam<File> BUILD_ROOT = 326 new StandardBundlerParam<>( 327 I18N.getString("param.build-root.name"), 328 I18N.getString("param.build-root.description"), 329 Arguments.CLIOptions.BUILD_ROOT.getId(), 330 File.class, 331 params -> { 332 try { 333 return Files.createTempDirectory( 334 "jdk.jpackage").toFile(); 335 } catch (IOException ioe) { 336 return null; 337 } 338 }, 339 (s, p) -> new File(s) 340 ); 341 342 public static final StandardBundlerParam<File> CONFIG_ROOT = 343 new StandardBundlerParam<>( 344 I18N.getString("param.config-root.name"), 345 I18N.getString("param.config-root.description"), 346 "configRoot", 347 File.class, 348 params -> { 349 File root = 350 new File(BUILD_ROOT.fetchFrom(params), "config"); 351 root.mkdirs(); 352 return root; 353 }, 354 (s, p) -> null 355 ); 356 357 static final StandardBundlerParam<String> IDENTIFIER = 358 new StandardBundlerParam<>( 359 I18N.getString("param.identifier.name"), 360 I18N.getString("param.identifier.description"), 361 Arguments.CLIOptions.IDENTIFIER.getId(), 362 String.class, 363 params -> { 364 String s = MAIN_CLASS.fetchFrom(params); 365 if (s == null) return null; 366 367 int idx = s.lastIndexOf("."); 368 if (idx >= 1) { 369 return s.substring(0, idx); 370 } 371 return s; 372 }, 373 (s, p) -> s 374 ); 375 376 static final StandardBundlerParam<String> PREFERENCES_ID = 377 new StandardBundlerParam<>( 378 I18N.getString("param.preferences-id.name"), 379 I18N.getString("param.preferences-id.description"), 380 "preferencesID", 381 String.class, 382 p -> Optional.ofNullable(IDENTIFIER.fetchFrom(p)). 383 orElse("").replace('.', '/'), 384 (s, p) -> s 385 ); 386 387 static final StandardBundlerParam<Boolean> VERBOSE = 388 new StandardBundlerParam<>( 389 I18N.getString("param.verbose.name"), 390 I18N.getString("param.verbose.description"), 391 Arguments.CLIOptions.VERBOSE.getId(), 392 Boolean.class, 393 params -> false, 394 // valueOf(null) is false, and we actually do want null 395 (s, p) -> (s == null || "null".equalsIgnoreCase(s)) ? 396 true : Boolean.valueOf(s) 397 ); 398 399 static final StandardBundlerParam<Boolean> OVERWRITE = 400 new StandardBundlerParam<>( 401 I18N.getString("param.overwrite.name"), 402 I18N.getString("param.overwrite.description"), 403 Arguments.CLIOptions.OVERWRITE.getId(), 404 Boolean.class, 405 params -> false, 406 // valueOf(null) is false, and we actually do want null 407 (s, p) -> (s == null || "null".equalsIgnoreCase(s)) ? 408 true : Boolean.valueOf(s) 409 ); 410 411 static final StandardBundlerParam<File> RESOURCE_DIR = 412 new StandardBundlerParam<>( 413 I18N.getString("param.resource-dir.name"), 414 I18N.getString("param.resource-dir.description"), 415 Arguments.CLIOptions.RESOURCE_DIR.getId(), 416 File.class, 417 params -> null, 418 (s, p) -> new File(s) 419 ); 420 421 static final BundlerParamInfo<String> INSTALL_DIR = 422 new StandardBundlerParam<>( 423 I18N.getString("param.install-dir.name"), 424 I18N.getString("param.install-dir.description"), 425 Arguments.CLIOptions.INSTALL_DIR.getId(), 426 String.class, 427 params -> null, 428 (s, p) -> s 429 ); 430 431 static final StandardBundlerParam<File> PREDEFINED_APP_IMAGE = 432 new StandardBundlerParam<>( 433 I18N.getString("param.predefined-app-image.name"), 434 I18N.getString("param.predefined-app-image.description"), 435 Arguments.CLIOptions.PREDEFINED_APP_IMAGE.getId(), 436 File.class, 437 params -> null, 438 (s, p) -> new File(s)); 439 440 static final StandardBundlerParam<File> PREDEFINED_RUNTIME_IMAGE = 441 new StandardBundlerParam<>( 442 I18N.getString("param.predefined-runtime-image.name"), 443 I18N.getString("param.predefined-runtime-image.description"), 444 Arguments.CLIOptions.PREDEFINED_RUNTIME_IMAGE.getId(), 445 File.class, 446 params -> null, 447 (s, p) -> new File(s)); 448 449 @SuppressWarnings("unchecked") 450 static final StandardBundlerParam<List<Map<String, ? super Object>>> SECONDARY_LAUNCHERS = 451 new StandardBundlerParam<>( 452 I18N.getString("param.secondary-launchers.name"), 453 I18N.getString("param.secondary-launchers.description"), 454 Arguments.CLIOptions.SECONDARY_LAUNCHER.getId(), 455 (Class<List<Map<String, ? super Object>>>) (Object) 456 List.class, 457 params -> new ArrayList<>(1), 458 // valueOf(null) is false, and we actually do want null 459 (s, p) -> null 460 ); 461 462 @SuppressWarnings("unchecked") 463 static final StandardBundlerParam 464 <List<Map<String, ? super Object>>> FILE_ASSOCIATIONS = 465 new StandardBundlerParam<>( 466 I18N.getString("param.file-associations.name"), 467 I18N.getString("param.file-associations.description"), 468 Arguments.CLIOptions.FILE_ASSOCIATIONS.getId(), 469 (Class<List<Map<String, ? super Object>>>) (Object) 470 List.class, 471 params -> new ArrayList<>(1), 472 // valueOf(null) is false, and we actually do want null 473 (s, p) -> null 474 ); 475 476 @SuppressWarnings("unchecked") 477 static final StandardBundlerParam<List<String>> FA_EXTENSIONS = 478 new StandardBundlerParam<>( 479 I18N.getString("param.fa-extension.name"), 480 I18N.getString("param.fa-extension.description"), 481 "fileAssociation.extension", 482 (Class<List<String>>) (Object) List.class, 483 params -> null, // null means not matched to an extension 484 (s, p) -> Arrays.asList(s.split("(,|\\s)+")) 485 ); 486 487 @SuppressWarnings("unchecked") 488 static final StandardBundlerParam<List<String>> FA_CONTENT_TYPE = 489 new StandardBundlerParam<>( 490 I18N.getString("param.fa-content-type.name"), 491 I18N.getString("param.fa-content-type.description"), 492 "fileAssociation.contentType", 493 (Class<List<String>>) (Object) List.class, 494 params -> null, 495 // null means not matched to a content/mime type 496 (s, p) -> Arrays.asList(s.split("(,|\\s)+")) 497 ); 498 499 static final StandardBundlerParam<String> FA_DESCRIPTION = 500 new StandardBundlerParam<>( 501 I18N.getString("param.fa-description.name"), 502 I18N.getString("param.fa-description.description"), 503 "fileAssociation.description", 504 String.class, 505 params -> APP_NAME.fetchFrom(params) + " File", 506 null 507 ); 508 509 static final StandardBundlerParam<File> FA_ICON = 510 new StandardBundlerParam<>( 511 I18N.getString("param.fa-icon.name"), 512 I18N.getString("param.fa-icon.description"), 513 "fileAssociation.icon", 514 File.class, 515 ICON::fetchFrom, 516 (s, p) -> new File(s) 517 ); 518 519 @SuppressWarnings("unchecked") 520 static final BundlerParamInfo<List<Path>> MODULE_PATH = 521 new StandardBundlerParam<>( 522 I18N.getString("param.module-path.name"), 523 I18N.getString("param.module-path.description"), 524 Arguments.CLIOptions.MODULE_PATH.getId(), 525 (Class<List<Path>>) (Object)List.class, 526 p -> { return getDefaultModulePath(); }, 527 (s, p) -> { 528 List<Path> modulePath = Arrays.asList(s 529 .split(File.pathSeparator)).stream() 530 .map(ss -> new File(ss).toPath()) 531 .collect(Collectors.toList()); 532 Path javaBasePath = null; 533 if (modulePath != null) { 534 javaBasePath = JLinkBundlerHelper 535 .findPathOfModule(modulePath, JAVABASEJMOD); 536 } else { 537 modulePath = new ArrayList<Path>(); 538 } 539 540 // Add the default JDK module path to the module path. 541 if (javaBasePath == null) { 542 List<Path> jdkModulePath = getDefaultModulePath(); 543 544 if (jdkModulePath != null) { 545 modulePath.addAll(jdkModulePath); 546 javaBasePath = 547 JLinkBundlerHelper.findPathOfModule( 548 modulePath, JAVABASEJMOD); 549 } 550 } 551 552 if (javaBasePath == null || 553 !Files.exists(javaBasePath)) { 554 Log.error(String.format(I18N.getString( 555 "warning.no.jdk.modules.found"))); 556 } 557 558 return modulePath; 559 }); 560 561 static final BundlerParamInfo<String> MODULE = 562 new StandardBundlerParam<>( 563 I18N.getString("param.main.module.name"), 564 I18N.getString("param.main.module.description"), 565 Arguments.CLIOptions.MODULE.getId(), 566 String.class, 567 p -> null, 568 (s, p) -> { 569 return String.valueOf(s); 570 }); 571 572 @SuppressWarnings("unchecked") 573 static final BundlerParamInfo<Set<String>> ADD_MODULES = 574 new StandardBundlerParam<>( 575 I18N.getString("param.add-modules.name"), 576 I18N.getString("param.add-modules.description"), 577 Arguments.CLIOptions.ADD_MODULES.getId(), 578 (Class<Set<String>>) (Object) Set.class, 579 p -> new LinkedHashSet<String>(), 580 (s, p) -> new LinkedHashSet<>(Arrays.asList(s.split(","))) 581 ); 582 583 @SuppressWarnings("unchecked") 584 static final BundlerParamInfo<Set<String>> LIMIT_MODULES = 585 new StandardBundlerParam<>( 586 I18N.getString("param.limit-modules.name"), 587 I18N.getString("param.limit-modules.description"), 588 "limit-modules", 589 (Class<Set<String>>) (Object) Set.class, 590 p -> new LinkedHashSet<String>(), 591 (s, p) -> new LinkedHashSet<>(Arrays.asList(s.split(","))) 592 ); 593 594 static final BundlerParamInfo<Boolean> STRIP_NATIVE_COMMANDS = 595 new StandardBundlerParam<>( 596 I18N.getString("param.strip-executables.name"), 597 I18N.getString("param.strip-executables.description"), 598 Arguments.CLIOptions.STRIP_NATIVE_COMMANDS.getId(), 599 Boolean.class, 600 p -> Boolean.FALSE, 601 (s, p) -> Boolean.valueOf(s) 602 ); 603 604 static File getPredefinedAppImage(Map<String, ? super Object> p) { 605 File applicationImage = null; 606 if (PREDEFINED_APP_IMAGE.fetchFrom(p) != null) { 607 applicationImage = PREDEFINED_APP_IMAGE.fetchFrom(p); 608 Log.debug("Using App Image from " + applicationImage); 609 if (!applicationImage.exists()) { 610 throw new RuntimeException( 611 MessageFormat.format(I18N.getString( 612 "message.app-image-dir-does-not-exist"), 613 PREDEFINED_APP_IMAGE.getID(), 614 applicationImage.toString())); 615 } 616 } 617 return applicationImage; 618 } 619 620 static void copyPredefinedRuntimeImage( 621 Map<String, ? super Object> p, 622 AbstractAppImageBuilder appBuilder) 623 throws IOException , ConfigException { 624 File image = PREDEFINED_RUNTIME_IMAGE.fetchFrom(p); 625 if (!image.exists()) { 626 throw new ConfigException( 627 MessageFormat.format(I18N.getString( 628 "message.runtime-image-dir-does-not-exist"), 629 PREDEFINED_RUNTIME_IMAGE.getID(), 630 image.toString()), 631 MessageFormat.format(I18N.getString( 632 "message.runtime-image-dir-does-not-exist.advice"), 633 PREDEFINED_RUNTIME_IMAGE.getID())); 634 } 635 // copy whole runtime, need to skip jmods and src.zip 636 final List<String> excludes = Arrays.asList("jmods", "src.zip"); 637 IOUtils.copyRecursive(image.toPath(), appBuilder.getRoot(), excludes); 638 639 // if module-path given - copy modules to appDir/mods 640 List<Path> modulePath = 641 StandardBundlerParam.MODULE_PATH.fetchFrom(p); 642 List<Path> defaultModulePath = getDefaultModulePath(); 643 Path dest = appBuilder.getAppModsDir(); 644 645 if (dest != null) { 646 for (Path mp : modulePath) { 647 if (!defaultModulePath.contains(mp)) { 648 Files.createDirectories(dest); 649 IOUtils.copyRecursive(mp, dest); 650 } 651 } 652 } 653 654 appBuilder.prepareApplicationFiles(); 655 } 656 657 static void extractMainClassInfoFromAppResources( 658 Map<String, ? super Object> params) { 659 boolean hasMainClass = params.containsKey(MAIN_CLASS.getID()); 660 boolean hasMainJar = params.containsKey(MAIN_JAR.getID()); 661 boolean hasMainJarClassPath = params.containsKey(CLASSPATH.getID()); 662 boolean hasModule = params.containsKey(MODULE.getID()); 663 boolean runtimeInstaller = 664 params.containsKey(RUNTIME_INSTALLER.getID()); 665 666 if (hasMainClass && hasMainJar && hasMainJarClassPath || hasModule || 667 runtimeInstaller) { 668 return; 669 } 670 671 // it's a pair. 672 // The [0] is the srcdir [1] is the file relative to sourcedir 673 List<String[]> filesToCheck = new ArrayList<>(); 674 675 if (hasMainJar) { 676 RelativeFileSet rfs = MAIN_JAR.fetchFrom(params); 677 for (String s : rfs.getIncludedFiles()) { 678 filesToCheck.add( 679 new String[] {rfs.getBaseDirectory().toString(), s}); 680 } 681 } else if (hasMainJarClassPath) { 682 for (String s : CLASSPATH.fetchFrom(params).split("\\s+")) { 683 if (APP_RESOURCES.fetchFrom(params) != null) { 684 filesToCheck.add( 685 new String[] {APP_RESOURCES.fetchFrom(params) 686 .getBaseDirectory().toString(), s}); 687 } 688 } 689 } else { 690 List<RelativeFileSet> rfsl = APP_RESOURCES_LIST.fetchFrom(params); 691 if (rfsl == null || rfsl.isEmpty()) { 692 return; 693 } 694 for (RelativeFileSet rfs : rfsl) { 695 if (rfs == null) continue; 696 697 for (String s : rfs.getIncludedFiles()) { 698 filesToCheck.add( 699 new String[]{rfs.getBaseDirectory().toString(), s}); 700 } 701 } 702 } 703 704 // presume the set iterates in-order 705 for (String[] fnames : filesToCheck) { 706 try { 707 // only sniff jars 708 if (!fnames[1].toLowerCase().endsWith(".jar")) continue; 709 710 File file = new File(fnames[0], fnames[1]); 711 // that actually exist 712 if (!file.exists()) continue; 713 714 try (JarFile jf = new JarFile(file)) { 715 Manifest m = jf.getManifest(); 716 Attributes attrs = (m != null) ? 717 m.getMainAttributes() : null; 718 719 if (attrs != null) { 720 if (!hasMainJar) { 721 if (fnames[0] == null) { 722 fnames[0] = file.getParentFile().toString(); 723 } 724 params.put(MAIN_JAR.getID(), new RelativeFileSet( 725 new File(fnames[0]), 726 new LinkedHashSet<>(Collections 727 .singletonList(file)))); 728 } 729 if (!hasMainJarClassPath) { 730 String cp = 731 attrs.getValue(Attributes.Name.CLASS_PATH); 732 params.put(CLASSPATH.getID(), 733 cp == null ? "" : cp); 734 } 735 break; 736 } 737 } 738 } catch (IOException ignore) { 739 ignore.printStackTrace(); 740 } 741 } 742 } 743 744 static void validateMainClassInfoFromAppResources( 745 Map<String, ? super Object> params) throws ConfigException { 746 boolean hasMainClass = params.containsKey(MAIN_CLASS.getID()); 747 boolean hasMainJar = params.containsKey(MAIN_JAR.getID()); 748 boolean hasMainJarClassPath = params.containsKey(CLASSPATH.getID()); 749 boolean hasModule = params.containsKey(MODULE.getID()); 750 boolean hasAppImage = params.containsKey(PREDEFINED_APP_IMAGE.getID()); 751 boolean runtimeInstaller = 752 params.containsKey(RUNTIME_INSTALLER.getID()); 753 754 if (hasMainClass && hasMainJar && hasMainJarClassPath || 755 hasModule || runtimeInstaller || hasAppImage) { 756 return; 757 } 758 759 extractMainClassInfoFromAppResources(params); 760 761 if (!params.containsKey(MAIN_CLASS.getID())) { 762 if (hasMainJar) { 763 throw new ConfigException( 764 MessageFormat.format(I18N.getString( 765 "error.no-main-class-with-main-jar"), 766 MAIN_JAR.fetchFrom(params)), 767 MessageFormat.format(I18N.getString( 768 "error.no-main-class-with-main-jar.advice"), 769 MAIN_JAR.fetchFrom(params))); 770 } else { 771 throw new ConfigException( 772 I18N.getString("error.no-main-class"), 773 I18N.getString("error.no-main-class.advice")); 774 } 775 } 776 } 777 778 779 private static List<String> splitStringWithEscapes(String s) { 780 List<String> l = new ArrayList<>(); 781 StringBuilder current = new StringBuilder(); 782 boolean quoted = false; 783 boolean escaped = false; 784 for (char c : s.toCharArray()) { 785 if (escaped) { 786 current.append(c); 787 } else if ('"' == c) { 788 quoted = !quoted; 789 } else if (!quoted && Character.isWhitespace(c)) { 790 l.add(current.toString()); 791 current = new StringBuilder(); 792 } else { 793 current.append(c); 794 } 795 } 796 l.add(current.toString()); 797 return l; 798 } 799 800 private static List<RelativeFileSet> 801 createAppResourcesListFromString(String s, 802 Map<String, ? super Object> objectObjectMap) { 803 List<RelativeFileSet> result = new ArrayList<>(); 804 for (String path : s.split("[:;]")) { 805 File f = new File(path); 806 if (f.getName().equals("*") || path.endsWith("/") || 807 path.endsWith("\\")) { 808 if (f.getName().equals("*")) { 809 f = f.getParentFile(); 810 } 811 Set<File> theFiles = new HashSet<>(); 812 try { 813 Files.walk(f.toPath()) 814 .filter(Files::isRegularFile) 815 .forEach(p -> theFiles.add(p.toFile())); 816 } catch (IOException e) { 817 e.printStackTrace(); 818 } 819 result.add(new RelativeFileSet(f, theFiles)); 820 } else { 821 result.add(new RelativeFileSet(f.getParentFile(), 822 Collections.singleton(f))); 823 } 824 } 825 return result; 826 } 827 828 private static RelativeFileSet getMainJar( 829 String mainJarValue, Map<String, ? super Object> params) { 830 for (RelativeFileSet rfs : APP_RESOURCES_LIST.fetchFrom(params)) { 831 File appResourcesRoot = rfs.getBaseDirectory(); 832 File mainJarFile = new File(appResourcesRoot, mainJarValue); 833 834 if (mainJarFile.exists()) { 835 return new RelativeFileSet(appResourcesRoot, 836 new LinkedHashSet<>(Collections.singletonList( 837 mainJarFile))); 838 } 839 mainJarFile = new File(mainJarValue); 840 if (mainJarFile.exists()) { 841 // absolute path for main-jar may fail is only legal if 842 // path is within the appResourceRoot directory 843 try { 844 return new RelativeFileSet(appResourcesRoot, 845 new LinkedHashSet<>(Collections.singletonList( 846 mainJarFile))); 847 } catch (Exception e) { 848 // if not within, RelativeFileSet constructor throws a 849 // RuntimeException, but the IllegalArgumentException 850 // below contains a more explicit error message. 851 } 852 } else { 853 List<Path> modulePath = MODULE_PATH.fetchFrom(params); 854 modulePath.removeAll(getDefaultModulePath()); 855 if (!modulePath.isEmpty()) { 856 Path modularJarPath = JLinkBundlerHelper.findPathOfModule( 857 modulePath, mainJarValue); 858 if (modularJarPath != null && 859 Files.exists(modularJarPath)) { 860 return new RelativeFileSet(appResourcesRoot, 861 new LinkedHashSet<>(Collections.singletonList( 862 modularJarPath.toFile()))); 863 } 864 } 865 } 866 } 867 868 throw new IllegalArgumentException( 869 new ConfigException(MessageFormat.format(I18N.getString( 870 "error.main-jar-does-not-exist"), 871 mainJarValue), I18N.getString( 872 "error.main-jar-does-not-exist.advice"))); 873 } 874 875 static List<Path> getDefaultModulePath() { 876 List<Path> result = new ArrayList<Path>(); 877 Path jdkModulePath = Paths.get( 878 System.getProperty("java.home"), "jmods").toAbsolutePath(); 879 880 if (jdkModulePath != null && Files.exists(jdkModulePath)) { 881 result.add(jdkModulePath); 882 } 883 else { 884 // On a developer build the JDK Home isn't where we expect it 885 // relative to the jmods directory. Do some extra 886 // processing to find it. 887 Map<String, String> env = System.getenv(); 888 889 if (env.containsKey("JDK_HOME")) { 890 jdkModulePath = Paths.get(env.get("JDK_HOME"), 891 ".." + File.separator + "images" 892 + File.separator + "jmods").toAbsolutePath(); 893 894 if (jdkModulePath != null && Files.exists(jdkModulePath)) { 895 result.add(jdkModulePath); 896 } 897 } 898 } 899 900 return result; 901 } 902 }