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