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