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