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