1 /* 2 * Copyright (c) 2007, 2018, 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 sun.launcher; 27 28 /* 29 * 30 * <p><b>This is NOT part of any API supported by Sun Microsystems. 31 * If you write code that depends on this, you do so at your own 32 * risk. This code and its internal interfaces are subject to change 33 * or deletion without notice.</b> 34 * 35 */ 36 37 /** 38 * A utility package for the java(1), javaw(1) launchers. 39 * The following are helper methods that the native launcher uses 40 * to perform checks etc. using JNI, see src/share/bin/java.c 41 */ 42 import java.io.File; 43 import java.io.IOException; 44 import java.io.PrintStream; 45 import java.io.UnsupportedEncodingException; 46 import java.lang.module.Configuration; 47 import java.lang.module.FindException; 48 import java.lang.module.ModuleDescriptor; 49 import java.lang.module.ModuleDescriptor.Requires; 50 import java.lang.module.ModuleDescriptor.Exports; 51 import java.lang.module.ModuleDescriptor.Opens; 52 import java.lang.module.ModuleDescriptor.Provides; 53 import java.lang.module.ModuleFinder; 54 import java.lang.module.ModuleReference; 55 import java.lang.module.ResolvedModule; 56 import java.lang.reflect.InvocationTargetException; 57 import java.lang.reflect.Method; 58 import java.lang.reflect.Modifier; 59 import java.math.BigDecimal; 60 import java.math.RoundingMode; 61 import java.net.URI; 62 import java.nio.charset.Charset; 63 import java.nio.file.DirectoryStream; 64 import java.nio.file.Files; 65 import java.nio.file.NoSuchFileException; 66 import java.nio.file.Path; 67 import java.nio.file.Paths; 68 import java.nio.file.attribute.BasicFileAttributes; 69 import java.text.Normalizer; 70 import java.text.MessageFormat; 71 import java.util.ArrayList; 72 import java.util.Collections; 73 import java.util.Comparator; 74 import java.util.HashMap; 75 import java.util.Iterator; 76 import java.util.List; 77 import java.util.Locale; 78 import java.util.Locale.Category; 79 import java.util.Map; 80 import java.util.Optional; 81 import java.util.Properties; 82 import java.util.ResourceBundle; 83 import java.util.Set; 84 import java.util.TreeSet; 85 import java.util.jar.Attributes; 86 import java.util.jar.JarFile; 87 import java.util.jar.Manifest; 88 import java.util.stream.Collectors; 89 import java.util.stream.Stream; 90 91 import jdk.internal.misc.VM; 92 import jdk.internal.module.ModuleBootstrap; 93 import jdk.internal.module.Modules; 94 import jdk.internal.platform.Container; 95 import jdk.internal.platform.Metrics; 96 97 98 public final class LauncherHelper { 99 100 // No instantiation 101 private LauncherHelper() {} 102 103 // used to identify JavaFX applications 104 private static final String JAVAFX_APPLICATION_MARKER = 105 "JavaFX-Application-Class"; 106 private static final String JAVAFX_APPLICATION_CLASS_NAME = 107 "javafx.application.Application"; 108 private static final String JAVAFX_FXHELPER_CLASS_NAME_SUFFIX = 109 "sun.launcher.LauncherHelper$FXHelper"; 110 private static final String LAUNCHER_AGENT_CLASS = "Launcher-Agent-Class"; 111 private static final String MAIN_CLASS = "Main-Class"; 112 private static final String ADD_EXPORTS = "Add-Exports"; 113 private static final String ADD_OPENS = "Add-Opens"; 114 115 private static StringBuilder outBuf = new StringBuilder(); 116 117 private static final String INDENT = " "; 118 private static final String VM_SETTINGS = "VM settings:"; 119 private static final String PROP_SETTINGS = "Property settings:"; 120 private static final String LOCALE_SETTINGS = "Locale settings:"; 121 122 // sync with java.c and jdk.internal.misc.VM 123 private static final String diagprop = "sun.java.launcher.diag"; 124 static final boolean trace = VM.getSavedProperty(diagprop) != null; 125 126 private static final String defaultBundleName = 127 "sun.launcher.resources.launcher"; 128 private static class ResourceBundleHolder { 129 private static final ResourceBundle RB = 130 ResourceBundle.getBundle(defaultBundleName); 131 } 132 private static PrintStream ostream; 133 private static Class<?> appClass; // application class, for GUI/reporting purposes 134 135 /* 136 * A method called by the launcher to print out the standard settings, 137 * by default -XshowSettings is equivalent to -XshowSettings:all, 138 * Specific information may be gotten by using suboptions with possible 139 * values vm, properties and locale. 140 * 141 * printToStderr: choose between stdout and stderr 142 * 143 * optionFlag: specifies which options to print default is all other 144 * possible values are vm, properties, locale. 145 * 146 * initialHeapSize: in bytes, as set by the launcher, a zero-value indicates 147 * this code should determine this value, using a suitable method or 148 * the line could be omitted. 149 * 150 * maxHeapSize: in bytes, as set by the launcher, a zero-value indicates 151 * this code should determine this value, using a suitable method. 152 * 153 * stackSize: in bytes, as set by the launcher, a zero-value indicates 154 * this code determine this value, using a suitable method or omit the 155 * line entirely. 156 */ 157 @SuppressWarnings("fallthrough") 158 static void showSettings(boolean printToStderr, String optionFlag, 159 long initialHeapSize, long maxHeapSize, long stackSize) { 160 161 initOutput(printToStderr); 162 String opts[] = optionFlag.split(":"); 163 String optStr = (opts.length > 1 && opts[1] != null) 164 ? opts[1].trim() 165 : "all"; 166 switch (optStr) { 167 case "vm": 168 printVmSettings(initialHeapSize, maxHeapSize, stackSize); 169 break; 170 case "properties": 171 printProperties(); 172 break; 173 case "locale": 174 printLocale(); 175 break; 176 case "system": 177 if (System.getProperty("os.name").contains("Linux")) { 178 printSystemMetrics(); 179 break; 180 } 181 default: 182 printVmSettings(initialHeapSize, maxHeapSize, stackSize); 183 printProperties(); 184 printLocale(); 185 if (System.getProperty("os.name").contains("Linux")) { 186 printSystemMetrics(); 187 } 188 break; 189 } 190 } 191 192 /* 193 * prints the main vm settings subopt/section 194 */ 195 private static void printVmSettings( 196 long initialHeapSize, long maxHeapSize, 197 long stackSize) { 198 199 ostream.println(VM_SETTINGS); 200 if (stackSize != 0L) { 201 ostream.println(INDENT + "Stack Size: " + 202 SizePrefix.scaleValue(stackSize)); 203 } 204 if (initialHeapSize != 0L) { 205 ostream.println(INDENT + "Min. Heap Size: " + 206 SizePrefix.scaleValue(initialHeapSize)); 207 } 208 if (maxHeapSize != 0L) { 209 ostream.println(INDENT + "Max. Heap Size: " + 210 SizePrefix.scaleValue(maxHeapSize)); 211 } else { 212 ostream.println(INDENT + "Max. Heap Size (Estimated): " 213 + SizePrefix.scaleValue(Runtime.getRuntime().maxMemory())); 214 } 215 ostream.println(INDENT + "Using VM: " 216 + System.getProperty("java.vm.name")); 217 ostream.println(); 218 } 219 220 /* 221 * prints the properties subopt/section 222 */ 223 private static void printProperties() { 224 Properties p = System.getProperties(); 225 ostream.println(PROP_SETTINGS); 226 List<String> sortedPropertyKeys = new ArrayList<>(); 227 sortedPropertyKeys.addAll(p.stringPropertyNames()); 228 Collections.sort(sortedPropertyKeys); 229 for (String x : sortedPropertyKeys) { 230 printPropertyValue(x, p.getProperty(x)); 231 } 232 ostream.println(); 233 } 234 235 private static boolean isPath(String key) { 236 return key.endsWith(".dirs") || key.endsWith(".path"); 237 } 238 239 private static void printPropertyValue(String key, String value) { 240 ostream.print(INDENT + key + " = "); 241 if (key.equals("line.separator")) { 242 for (byte b : value.getBytes()) { 243 switch (b) { 244 case 0xd: 245 ostream.print("\\r "); 246 break; 247 case 0xa: 248 ostream.print("\\n "); 249 break; 250 default: 251 // print any bizzare line separators in hex, but really 252 // shouldn't happen. 253 ostream.printf("0x%02X", b & 0xff); 254 break; 255 } 256 } 257 ostream.println(); 258 return; 259 } 260 if (!isPath(key)) { 261 ostream.println(value); 262 return; 263 } 264 String[] values = value.split(System.getProperty("path.separator")); 265 boolean first = true; 266 for (String s : values) { 267 if (first) { // first line treated specially 268 ostream.println(s); 269 first = false; 270 } else { // following lines prefix with indents 271 ostream.println(INDENT + INDENT + s); 272 } 273 } 274 } 275 276 /* 277 * prints the locale subopt/section 278 */ 279 private static void printLocale() { 280 Locale locale = Locale.getDefault(); 281 ostream.println(LOCALE_SETTINGS); 282 ostream.println(INDENT + "default locale = " + 283 locale.getDisplayName()); 284 ostream.println(INDENT + "default display locale = " + 285 Locale.getDefault(Category.DISPLAY).getDisplayName()); 286 ostream.println(INDENT + "default format locale = " + 287 Locale.getDefault(Category.FORMAT).getDisplayName()); 288 printLocales(); 289 ostream.println(); 290 } 291 292 private static void printLocales() { 293 Locale[] tlocales = Locale.getAvailableLocales(); 294 final int len = tlocales == null ? 0 : tlocales.length; 295 if (len < 1 ) { 296 return; 297 } 298 // Locale does not implement Comparable so we convert it to String 299 // and sort it for pretty printing. 300 Set<String> sortedSet = new TreeSet<>(); 301 for (Locale l : tlocales) { 302 sortedSet.add(l.toString()); 303 } 304 305 ostream.print(INDENT + "available locales = "); 306 Iterator<String> iter = sortedSet.iterator(); 307 final int last = len - 1; 308 for (int i = 0 ; iter.hasNext() ; i++) { 309 String s = iter.next(); 310 ostream.print(s); 311 if (i != last) { 312 ostream.print(", "); 313 } 314 // print columns of 8 315 if ((i + 1) % 8 == 0) { 316 ostream.println(); 317 ostream.print(INDENT + INDENT); 318 } 319 } 320 } 321 322 public static void printSystemMetrics() { 323 Metrics c = Container.metrics(); 324 325 ostream.println("Operating System Metrics:"); 326 327 if (c == null) { 328 ostream.println(INDENT + "No metrics available for this platform"); 329 return; 330 } 331 332 ostream.println(INDENT + "Provider: " + c.getProvider()); 333 ostream.println(INDENT + "Effective CPU Count: " + c.getEffectiveCpuCount()); 334 ostream.println(INDENT + "CPU Period: " + c.getCpuPeriod() + 335 (c.getCpuPeriod() == -1 ? "" : "us")); 336 ostream.println(INDENT + "CPU Quota: " + c.getCpuQuota() + 337 (c.getCpuQuota() == -1 ? "" : "us")); 338 ostream.println(INDENT + "CPU Shares: " + c.getCpuShares()); 339 340 int cpus[] = c.getCpuSetCpus(); 341 ostream.println(INDENT + "List of Processors, " 342 + cpus.length + " total: "); 343 344 ostream.print(INDENT); 345 for (int i = 0; i < cpus.length; i++) { 346 ostream.print(cpus[i] + " "); 347 } 348 if (cpus.length > 0) { 349 ostream.println(""); 350 } 351 352 cpus = c.getEffectiveCpuSetCpus(); 353 ostream.println(INDENT + "List of Effective Processors, " 354 + cpus.length + " total: "); 355 356 ostream.print(INDENT); 357 for (int i = 0; i < cpus.length; i++) { 358 ostream.print(cpus[i] + " "); 359 } 360 if (cpus.length > 0) { 361 ostream.println(""); 362 } 363 364 int mems[] = c.getCpuSetMems(); 365 ostream.println(INDENT + "List of Memory Nodes, " 366 + mems.length + " total: "); 367 368 ostream.print(INDENT); 369 for (int i = 0; i < mems.length; i++) { 370 ostream.print(mems[i] + " "); 371 } 372 if (mems.length > 0) { 373 ostream.println(""); 374 } 375 376 mems = c.getEffectiveCpuSetMems(); 377 ostream.println(INDENT + "List of Available Memory Nodes, " 378 + mems.length + " total: "); 379 380 ostream.print(INDENT); 381 for (int i = 0; i < mems.length; i++) { 382 ostream.print(mems[i] + " "); 383 } 384 if (mems.length > 0) { 385 ostream.println(""); 386 } 387 388 ostream.println(INDENT + "CPUSet Memory Pressure Enabled: " 389 + c.isCpuSetMemoryPressureEnabled()); 390 391 long limit = c.getMemoryLimit(); 392 ostream.println(INDENT + "Memory Limit: " + 393 ((limit >= 0) ? SizePrefix.scaleValue(limit) : "Unlimited")); 394 395 limit = c.getMemorySoftLimit(); 396 ostream.println(INDENT + "Memory Soft Limit: " + 397 ((limit >= 0) ? SizePrefix.scaleValue(limit) : "Unlimited")); 398 399 limit = c.getMemoryAndSwapLimit(); 400 ostream.println(INDENT + "Memory & Swap Limit: " + 401 ((limit >= 0) ? SizePrefix.scaleValue(limit) : "Unlimited")); 402 403 limit = c.getKernelMemoryLimit(); 404 ostream.println(INDENT + "Kernel Memory Limit: " + 405 ((limit >= 0) ? SizePrefix.scaleValue(limit) : "Unlimited")); 406 407 limit = c.getTcpMemoryLimit(); 408 ostream.println(INDENT + "TCP Memory Limit: " + 409 ((limit >= 0) ? SizePrefix.scaleValue(limit) : "Unlimited")); 410 411 ostream.println(INDENT + "Out Of Memory Killer Enabled: " 412 + c.isMemoryOOMKillEnabled()); 413 414 ostream.println(""); 415 } 416 417 private enum SizePrefix { 418 419 KILO(1024, "K"), 420 MEGA(1024 * 1024, "M"), 421 GIGA(1024 * 1024 * 1024, "G"), 422 TERA(1024L * 1024L * 1024L * 1024L, "T"); 423 long size; 424 String abbrev; 425 426 SizePrefix(long size, String abbrev) { 427 this.size = size; 428 this.abbrev = abbrev; 429 } 430 431 private static String scale(long v, SizePrefix prefix) { 432 return BigDecimal.valueOf(v).divide(BigDecimal.valueOf(prefix.size), 433 2, RoundingMode.HALF_EVEN).toPlainString() + prefix.abbrev; 434 } 435 /* 436 * scale the incoming values to a human readable form, represented as 437 * K, M, G and T, see java.c parse_size for the scaled values and 438 * suffixes. The lowest possible scaled value is Kilo. 439 */ 440 static String scaleValue(long v) { 441 if (v < MEGA.size) { 442 return scale(v, KILO); 443 } else if (v < GIGA.size) { 444 return scale(v, MEGA); 445 } else if (v < TERA.size) { 446 return scale(v, GIGA); 447 } else { 448 return scale(v, TERA); 449 } 450 } 451 } 452 453 /** 454 * A private helper method to get a localized message and also 455 * apply any arguments that we might pass. 456 */ 457 private static String getLocalizedMessage(String key, Object... args) { 458 String msg = ResourceBundleHolder.RB.getString(key); 459 return (args != null) ? MessageFormat.format(msg, args) : msg; 460 } 461 462 /** 463 * The java -help message is split into 3 parts, an invariant, followed 464 * by a set of platform dependent variant messages, finally an invariant 465 * set of lines. 466 * This method initializes the help message for the first time, and also 467 * assembles the invariant header part of the message. 468 */ 469 static void initHelpMessage(String progname) { 470 outBuf = outBuf.append(getLocalizedMessage("java.launcher.opt.header", 471 (progname == null) ? "java" : progname )); 472 } 473 474 /** 475 * Appends the vm selection messages to the header, already created. 476 * initHelpSystem must already be called. 477 */ 478 static void appendVmSelectMessage(String vm1, String vm2) { 479 outBuf = outBuf.append(getLocalizedMessage("java.launcher.opt.vmselect", 480 vm1, vm2)); 481 } 482 483 /** 484 * Appends the vm synoym message to the header, already created. 485 * initHelpSystem must be called before using this method. 486 */ 487 static void appendVmSynonymMessage(String vm1, String vm2) { 488 outBuf = outBuf.append(getLocalizedMessage("java.launcher.opt.hotspot", 489 vm1, vm2)); 490 } 491 492 /** 493 * Appends the last invariant part to the previously created messages, 494 * and finishes up the printing to the desired output stream. 495 * initHelpSystem must be called before using this method. 496 */ 497 static void printHelpMessage(boolean printToStderr) { 498 initOutput(printToStderr); 499 outBuf = outBuf.append(getLocalizedMessage("java.launcher.opt.footer", 500 File.pathSeparator)); 501 ostream.println(outBuf.toString()); 502 } 503 504 /** 505 * Prints the Xusage text to the desired output stream. 506 */ 507 static void printXUsageMessage(boolean printToStderr) { 508 initOutput(printToStderr); 509 ostream.println(getLocalizedMessage("java.launcher.X.usage", 510 File.pathSeparator)); 511 if (System.getProperty("os.name").contains("OS X")) { 512 ostream.println(getLocalizedMessage("java.launcher.X.macosx.usage", 513 File.pathSeparator)); 514 } 515 } 516 517 static void initOutput(boolean printToStderr) { 518 ostream = (printToStderr) ? System.err : System.out; 519 } 520 521 static void initOutput(PrintStream ps) { 522 ostream = ps; 523 } 524 525 static String getMainClassFromJar(String jarname) { 526 String mainValue; 527 try (JarFile jarFile = new JarFile(jarname)) { 528 Manifest manifest = jarFile.getManifest(); 529 if (manifest == null) { 530 abort(null, "java.launcher.jar.error2", jarname); 531 } 532 Attributes mainAttrs = manifest.getMainAttributes(); 533 if (mainAttrs == null) { 534 abort(null, "java.launcher.jar.error3", jarname); 535 } 536 537 // Main-Class 538 mainValue = mainAttrs.getValue(MAIN_CLASS); 539 if (mainValue == null) { 540 abort(null, "java.launcher.jar.error3", jarname); 541 } 542 543 // Launcher-Agent-Class (only check for this when Main-Class present) 544 String agentClass = mainAttrs.getValue(LAUNCHER_AGENT_CLASS); 545 if (agentClass != null) { 546 ModuleLayer.boot().findModule("java.instrument").ifPresent(m -> { 547 try { 548 String cn = "sun.instrument.InstrumentationImpl"; 549 Class<?> clazz = Class.forName(cn, false, null); 550 Method loadAgent = clazz.getMethod("loadAgent", String.class); 551 loadAgent.invoke(null, jarname); 552 } catch (Throwable e) { 553 if (e instanceof InvocationTargetException) e = e.getCause(); 554 abort(e, "java.launcher.jar.error4", jarname); 555 } 556 }); 557 } 558 559 // Add-Exports and Add-Opens 560 String exports = mainAttrs.getValue(ADD_EXPORTS); 561 if (exports != null) { 562 addExportsOrOpens(exports, false); 563 } 564 String opens = mainAttrs.getValue(ADD_OPENS); 565 if (opens != null) { 566 addExportsOrOpens(opens, true); 567 } 568 569 /* 570 * Hand off to FXHelper if it detects a JavaFX application 571 * This must be done after ensuring a Main-Class entry 572 * exists to enforce compliance with the jar specification 573 */ 574 if (mainAttrs.containsKey( 575 new Attributes.Name(JAVAFX_APPLICATION_MARKER))) { 576 FXHelper.setFXLaunchParameters(jarname, LM_JAR); 577 return FXHelper.class.getName(); 578 } 579 580 return mainValue.trim(); 581 } catch (IOException ioe) { 582 abort(ioe, "java.launcher.jar.error1", jarname); 583 } 584 return null; 585 } 586 587 /** 588 * Process the Add-Exports or Add-Opens value. The value is 589 * {@code <module>/<package> ( <module>/<package>)*}. 590 */ 591 static void addExportsOrOpens(String value, boolean open) { 592 for (String moduleAndPackage : value.split(" ")) { 593 String[] s = moduleAndPackage.trim().split("/"); 594 if (s.length == 2) { 595 String mn = s[0]; 596 String pn = s[1]; 597 ModuleLayer.boot() 598 .findModule(mn) 599 .filter(m -> m.getDescriptor().packages().contains(pn)) 600 .ifPresent(m -> { 601 if (open) { 602 Modules.addOpensToAllUnnamed(m, pn); 603 } else { 604 Modules.addExportsToAllUnnamed(m, pn); 605 } 606 }); 607 } 608 } 609 } 610 611 // From src/share/bin/java.c: 612 // enum LaunchMode { LM_UNKNOWN = 0, LM_CLASS, LM_JAR, LM_MODULE, LM_SOURCE } 613 614 private static final int LM_UNKNOWN = 0; 615 private static final int LM_CLASS = 1; 616 private static final int LM_JAR = 2; 617 private static final int LM_MODULE = 3; 618 private static final int LM_SOURCE = 4; 619 620 static void abort(Throwable t, String msgKey, Object... args) { 621 if (msgKey != null) { 622 ostream.println(getLocalizedMessage(msgKey, args)); 623 } 624 if (trace) { 625 if (t != null) { 626 t.printStackTrace(); 627 } else { 628 Thread.dumpStack(); 629 } 630 } 631 System.exit(1); 632 } 633 634 /** 635 * This method: 636 * 1. Loads the main class from the module or class path 637 * 2. Checks the public static void main method. 638 * 3. If the main class extends FX Application then call on FXHelper to 639 * perform the launch. 640 * 641 * @param printToStderr if set, all output will be routed to stderr 642 * @param mode LaunchMode as determined by the arguments passed on the 643 * command line 644 * @param what the module name[/class], JAR file, or the main class 645 * depending on the mode 646 * 647 * @return the application's main class 648 */ 649 @SuppressWarnings("fallthrough") 650 public static Class<?> checkAndLoadMain(boolean printToStderr, 651 int mode, 652 String what) { 653 initOutput(printToStderr); 654 655 Class<?> mainClass = null; 656 switch (mode) { 657 case LM_MODULE: case LM_SOURCE: 658 mainClass = loadModuleMainClass(what); 659 break; 660 default: 661 mainClass = loadMainClass(mode, what); 662 break; 663 } 664 665 // record the real main class for UI purposes 666 // neither method above can return null, they will abort() 667 appClass = mainClass; 668 669 /* 670 * Check if FXHelper can launch it using the FX launcher. In an FX app, 671 * the main class may or may not have a main method, so do this before 672 * validating the main class. 673 */ 674 if (JAVAFX_FXHELPER_CLASS_NAME_SUFFIX.equals(mainClass.getName()) || 675 doesExtendFXApplication(mainClass)) { 676 // Will abort() if there are problems with FX runtime 677 FXHelper.setFXLaunchParameters(what, mode); 678 mainClass = FXHelper.class; 679 } 680 681 validateMainClass(mainClass); 682 return mainClass; 683 } 684 685 /** 686 * Returns the main class for a module. The query is either a module name 687 * or module-name/main-class. For the former then the module's main class 688 * is obtained from the module descriptor (MainClass attribute). 689 */ 690 private static Class<?> loadModuleMainClass(String what) { 691 int i = what.indexOf('/'); 692 String mainModule; 693 String mainClass; 694 if (i == -1) { 695 mainModule = what; 696 mainClass = null; 697 } else { 698 mainModule = what.substring(0, i); 699 mainClass = what.substring(i+1); 700 } 701 702 // main module is in the boot layer 703 ModuleLayer layer = ModuleLayer.boot(); 704 Optional<Module> om = layer.findModule(mainModule); 705 if (!om.isPresent()) { 706 // should not happen 707 throw new InternalError("Module " + mainModule + " not in boot Layer"); 708 } 709 Module m = om.get(); 710 711 // get main class 712 if (mainClass == null) { 713 Optional<String> omc = m.getDescriptor().mainClass(); 714 if (!omc.isPresent()) { 715 abort(null, "java.launcher.module.error1", mainModule); 716 } 717 mainClass = omc.get(); 718 } 719 720 // load the class from the module 721 Class<?> c = null; 722 try { 723 c = Class.forName(m, mainClass); 724 if (c == null && System.getProperty("os.name", "").contains("OS X") 725 && Normalizer.isNormalized(mainClass, Normalizer.Form.NFD)) { 726 727 String cn = Normalizer.normalize(mainClass, Normalizer.Form.NFC); 728 c = Class.forName(m, cn); 729 } 730 } catch (LinkageError le) { 731 abort(null, "java.launcher.module.error3", mainClass, m.getName(), 732 le.getClass().getName() + ": " + le.getLocalizedMessage()); 733 } 734 if (c == null) { 735 abort(null, "java.launcher.module.error2", mainClass, mainModule); 736 } 737 738 System.setProperty("jdk.module.main.class", c.getName()); 739 return c; 740 } 741 742 /** 743 * Loads the main class from the class path (LM_CLASS or LM_JAR). 744 */ 745 private static Class<?> loadMainClass(int mode, String what) { 746 // get the class name 747 String cn; 748 switch (mode) { 749 case LM_CLASS: 750 cn = what; 751 break; 752 case LM_JAR: 753 cn = getMainClassFromJar(what); 754 break; 755 default: 756 // should never happen 757 throw new InternalError("" + mode + ": Unknown launch mode"); 758 } 759 760 // load the main class 761 cn = cn.replace('/', '.'); 762 Class<?> mainClass = null; 763 ClassLoader scl = ClassLoader.getSystemClassLoader(); 764 try { 765 try { 766 mainClass = Class.forName(cn, false, scl); 767 } catch (NoClassDefFoundError | ClassNotFoundException cnfe) { 768 if (System.getProperty("os.name", "").contains("OS X") 769 && Normalizer.isNormalized(cn, Normalizer.Form.NFD)) { 770 try { 771 // On Mac OS X since all names with diacritical marks are 772 // given as decomposed it is possible that main class name 773 // comes incorrectly from the command line and we have 774 // to re-compose it 775 String ncn = Normalizer.normalize(cn, Normalizer.Form.NFC); 776 mainClass = Class.forName(ncn, false, scl); 777 } catch (NoClassDefFoundError | ClassNotFoundException cnfe1) { 778 abort(cnfe1, "java.launcher.cls.error1", cn, 779 cnfe1.getClass().getCanonicalName(), cnfe1.getMessage()); 780 } 781 } else { 782 abort(cnfe, "java.launcher.cls.error1", cn, 783 cnfe.getClass().getCanonicalName(), cnfe.getMessage()); 784 } 785 } 786 } catch (LinkageError le) { 787 abort(le, "java.launcher.cls.error6", cn, 788 le.getClass().getName() + ": " + le.getLocalizedMessage()); 789 } 790 return mainClass; 791 } 792 793 /* 794 * Accessor method called by the launcher after getting the main class via 795 * checkAndLoadMain(). The "application class" is the class that is finally 796 * executed to start the application and in this case is used to report 797 * the correct application name, typically for UI purposes. 798 */ 799 public static Class<?> getApplicationClass() { 800 return appClass; 801 } 802 803 /* 804 * Check if the given class is a JavaFX Application class. This is done 805 * in a way that does not cause the Application class to load or throw 806 * ClassNotFoundException if the JavaFX runtime is not available. 807 */ 808 private static boolean doesExtendFXApplication(Class<?> mainClass) { 809 for (Class<?> sc = mainClass.getSuperclass(); sc != null; 810 sc = sc.getSuperclass()) { 811 if (sc.getName().equals(JAVAFX_APPLICATION_CLASS_NAME)) { 812 return true; 813 } 814 } 815 return false; 816 } 817 818 // Check the existence and signature of main and abort if incorrect 819 static void validateMainClass(Class<?> mainClass) { 820 Method mainMethod = null; 821 try { 822 mainMethod = mainClass.getMethod("main", String[].class); 823 } catch (NoSuchMethodException nsme) { 824 // invalid main or not FX application, abort with an error 825 abort(null, "java.launcher.cls.error4", mainClass.getName(), 826 JAVAFX_APPLICATION_CLASS_NAME); 827 } catch (Throwable e) { 828 if (mainClass.getModule().isNamed()) { 829 abort(e, "java.launcher.module.error5", 830 mainClass.getName(), mainClass.getModule(), 831 e.getClass().getName(), e.getLocalizedMessage()); 832 } else { 833 abort(e, "java.launcher.cls.error7", mainClass.getName(), 834 e.getClass().getName(), e.getLocalizedMessage()); 835 } 836 } 837 838 /* 839 * getMethod (above) will choose the correct method, based 840 * on its name and parameter type, however, we still have to 841 * ensure that the method is static and returns a void. 842 */ 843 int mod = mainMethod.getModifiers(); 844 if (!Modifier.isStatic(mod)) { 845 abort(null, "java.launcher.cls.error2", "static", 846 mainMethod.getDeclaringClass().getName()); 847 } 848 if (mainMethod.getReturnType() != java.lang.Void.TYPE) { 849 abort(null, "java.launcher.cls.error3", 850 mainMethod.getDeclaringClass().getName()); 851 } 852 } 853 854 private static final String encprop = "sun.jnu.encoding"; 855 private static String encoding = null; 856 private static boolean isCharsetSupported = false; 857 858 /* 859 * converts a c or a byte array to a platform specific string, 860 * previously implemented as a native method in the launcher. 861 */ 862 static String makePlatformString(boolean printToStderr, byte[] inArray) { 863 initOutput(printToStderr); 864 if (encoding == null) { 865 encoding = System.getProperty(encprop); 866 isCharsetSupported = Charset.isSupported(encoding); 867 } 868 try { 869 String out = isCharsetSupported 870 ? new String(inArray, encoding) 871 : new String(inArray); 872 return out; 873 } catch (UnsupportedEncodingException uee) { 874 abort(uee, null); 875 } 876 return null; // keep the compiler happy 877 } 878 879 static String[] expandArgs(String[] argArray) { 880 List<StdArg> aList = new ArrayList<>(); 881 for (String x : argArray) { 882 aList.add(new StdArg(x)); 883 } 884 return expandArgs(aList); 885 } 886 887 static String[] expandArgs(List<StdArg> argList) { 888 ArrayList<String> out = new ArrayList<>(); 889 if (trace) { 890 System.err.println("Incoming arguments:"); 891 } 892 for (StdArg a : argList) { 893 if (trace) { 894 System.err.println(a); 895 } 896 if (a.needsExpansion) { 897 File x = new File(a.arg); 898 File parent = x.getParentFile(); 899 String glob = x.getName(); 900 if (parent == null) { 901 parent = new File("."); 902 } 903 try (DirectoryStream<Path> dstream = 904 Files.newDirectoryStream(parent.toPath(), glob)) { 905 int entries = 0; 906 for (Path p : dstream) { 907 out.add(p.normalize().toString()); 908 entries++; 909 } 910 if (entries == 0) { 911 out.add(a.arg); 912 } 913 } catch (Exception e) { 914 out.add(a.arg); 915 if (trace) { 916 System.err.println("Warning: passing argument as-is " + a); 917 System.err.print(e); 918 } 919 } 920 } else { 921 out.add(a.arg); 922 } 923 } 924 String[] oarray = new String[out.size()]; 925 out.toArray(oarray); 926 927 if (trace) { 928 System.err.println("Expanded arguments:"); 929 for (String x : oarray) { 930 System.err.println(x); 931 } 932 } 933 return oarray; 934 } 935 936 /* duplicate of the native StdArg struct */ 937 private static class StdArg { 938 final String arg; 939 final boolean needsExpansion; 940 StdArg(String arg, boolean expand) { 941 this.arg = arg; 942 this.needsExpansion = expand; 943 } 944 // protocol: first char indicates whether expansion is required 945 // 'T' = true ; needs expansion 946 // 'F' = false; needs no expansion 947 StdArg(String in) { 948 this.arg = in.substring(1); 949 needsExpansion = in.charAt(0) == 'T'; 950 } 951 public String toString() { 952 return "StdArg{" + "arg=" + arg + ", needsExpansion=" + needsExpansion + '}'; 953 } 954 } 955 956 static final class FXHelper { 957 958 private static final String JAVAFX_GRAPHICS_MODULE_NAME = 959 "javafx.graphics"; 960 961 private static final String JAVAFX_LAUNCHER_CLASS_NAME = 962 "com.sun.javafx.application.LauncherImpl"; 963 964 /* 965 * The launch method used to invoke the JavaFX launcher. These must 966 * match the strings used in the launchApplication method. 967 * 968 * Command line JavaFX-App-Class Launch mode FX Launch mode 969 * java -cp fxapp.jar FXClass N/A LM_CLASS "LM_CLASS" 970 * java -cp somedir FXClass N/A LM_CLASS "LM_CLASS" 971 * java -jar fxapp.jar Present LM_JAR "LM_JAR" 972 * java -jar fxapp.jar Not Present LM_JAR "LM_JAR" 973 * java -m module/class [1] N/A LM_MODULE "LM_MODULE" 974 * java -m module N/A LM_MODULE "LM_MODULE" 975 * 976 * [1] - JavaFX-Application-Class is ignored when modular args are used, even 977 * if present in a modular jar 978 */ 979 private static final String JAVAFX_LAUNCH_MODE_CLASS = "LM_CLASS"; 980 private static final String JAVAFX_LAUNCH_MODE_JAR = "LM_JAR"; 981 private static final String JAVAFX_LAUNCH_MODE_MODULE = "LM_MODULE"; 982 983 /* 984 * FX application launcher and launch method, so we can launch 985 * applications with no main method. 986 */ 987 private static String fxLaunchName = null; 988 private static String fxLaunchMode = null; 989 990 private static Class<?> fxLauncherClass = null; 991 private static Method fxLauncherMethod = null; 992 993 /* 994 * Set the launch params according to what was passed to LauncherHelper 995 * so we can use the same launch mode for FX. Abort if there is any 996 * issue with loading the FX runtime or with the launcher method. 997 */ 998 private static void setFXLaunchParameters(String what, int mode) { 999 1000 // find the module with the FX launcher 1001 Optional<Module> om = ModuleLayer.boot().findModule(JAVAFX_GRAPHICS_MODULE_NAME); 1002 if (!om.isPresent()) { 1003 abort(null, "java.launcher.cls.error5"); 1004 } 1005 1006 // load the FX launcher class 1007 fxLauncherClass = Class.forName(om.get(), JAVAFX_LAUNCHER_CLASS_NAME); 1008 if (fxLauncherClass == null) { 1009 abort(null, "java.launcher.cls.error5"); 1010 } 1011 1012 try { 1013 /* 1014 * signature must be: 1015 * public static void launchApplication(String launchName, 1016 * String launchMode, String[] args); 1017 */ 1018 fxLauncherMethod = fxLauncherClass.getMethod("launchApplication", 1019 String.class, String.class, String[].class); 1020 1021 // verify launcher signature as we do when validating the main method 1022 int mod = fxLauncherMethod.getModifiers(); 1023 if (!Modifier.isStatic(mod)) { 1024 abort(null, "java.launcher.javafx.error1"); 1025 } 1026 if (fxLauncherMethod.getReturnType() != java.lang.Void.TYPE) { 1027 abort(null, "java.launcher.javafx.error1"); 1028 } 1029 } catch (NoSuchMethodException ex) { 1030 abort(ex, "java.launcher.cls.error5", ex); 1031 } 1032 1033 fxLaunchName = what; 1034 switch (mode) { 1035 case LM_CLASS: 1036 fxLaunchMode = JAVAFX_LAUNCH_MODE_CLASS; 1037 break; 1038 case LM_JAR: 1039 fxLaunchMode = JAVAFX_LAUNCH_MODE_JAR; 1040 break; 1041 case LM_MODULE: 1042 fxLaunchMode = JAVAFX_LAUNCH_MODE_MODULE; 1043 break; 1044 default: 1045 // should not have gotten this far... 1046 throw new InternalError(mode + ": Unknown launch mode"); 1047 } 1048 } 1049 1050 public static void main(String... args) throws Exception { 1051 if (fxLauncherMethod == null 1052 || fxLaunchMode == null 1053 || fxLaunchName == null) { 1054 throw new RuntimeException("Invalid JavaFX launch parameters"); 1055 } 1056 // launch appClass via fxLauncherMethod 1057 fxLauncherMethod.invoke(null, 1058 new Object[] {fxLaunchName, fxLaunchMode, args}); 1059 } 1060 } 1061 1062 /** 1063 * Called by the launcher to list the observable modules. 1064 */ 1065 static void listModules() { 1066 initOutput(System.out); 1067 1068 ModuleBootstrap.limitedFinder().findAll().stream() 1069 .sorted(new JrtFirstComparator()) 1070 .forEach(LauncherHelper::showModule); 1071 } 1072 1073 /** 1074 * Called by the launcher to show the resolved modules 1075 */ 1076 static void showResolvedModules() { 1077 initOutput(System.out); 1078 1079 ModuleLayer bootLayer = ModuleLayer.boot(); 1080 Configuration cf = bootLayer.configuration(); 1081 1082 cf.modules().stream() 1083 .map(ResolvedModule::reference) 1084 .sorted(new JrtFirstComparator()) 1085 .forEach(LauncherHelper::showModule); 1086 } 1087 1088 /** 1089 * Called by the launcher to describe a module 1090 */ 1091 static void describeModule(String moduleName) { 1092 initOutput(System.out); 1093 1094 ModuleFinder finder = ModuleBootstrap.limitedFinder(); 1095 ModuleReference mref = finder.find(moduleName).orElse(null); 1096 if (mref == null) { 1097 abort(null, "java.launcher.module.error4", moduleName); 1098 } 1099 ModuleDescriptor md = mref.descriptor(); 1100 1101 // one-line summary 1102 showModule(mref); 1103 1104 // unqualified exports (sorted by package) 1105 md.exports().stream() 1106 .filter(e -> !e.isQualified()) 1107 .sorted(Comparator.comparing(Exports::source)) 1108 .map(e -> Stream.concat(Stream.of(e.source()), 1109 toStringStream(e.modifiers())) 1110 .collect(Collectors.joining(" "))) 1111 .forEach(sourceAndMods -> ostream.format("exports %s%n", sourceAndMods)); 1112 1113 // dependences 1114 for (Requires r : md.requires()) { 1115 String nameAndMods = Stream.concat(Stream.of(r.name()), 1116 toStringStream(r.modifiers())) 1117 .collect(Collectors.joining(" ")); 1118 ostream.format("requires %s", nameAndMods); 1119 finder.find(r.name()) 1120 .map(ModuleReference::descriptor) 1121 .filter(ModuleDescriptor::isAutomatic) 1122 .ifPresent(any -> ostream.print(" automatic")); 1123 ostream.println(); 1124 } 1125 1126 // service use and provides 1127 for (String s : md.uses()) { 1128 ostream.format("uses %s%n", s); 1129 } 1130 for (Provides ps : md.provides()) { 1131 String names = ps.providers().stream().collect(Collectors.joining(" ")); 1132 ostream.format("provides %s with %s%n", ps.service(), names); 1133 1134 } 1135 1136 // qualified exports 1137 for (Exports e : md.exports()) { 1138 if (e.isQualified()) { 1139 String who = e.targets().stream().collect(Collectors.joining(" ")); 1140 ostream.format("qualified exports %s to %s%n", e.source(), who); 1141 } 1142 } 1143 1144 // open packages 1145 for (Opens opens: md.opens()) { 1146 if (opens.isQualified()) 1147 ostream.print("qualified "); 1148 String sourceAndMods = Stream.concat(Stream.of(opens.source()), 1149 toStringStream(opens.modifiers())) 1150 .collect(Collectors.joining(" ")); 1151 ostream.format("opens %s", sourceAndMods); 1152 if (opens.isQualified()) { 1153 String who = opens.targets().stream().collect(Collectors.joining(" ")); 1154 ostream.format(" to %s", who); 1155 } 1156 ostream.println(); 1157 } 1158 1159 // non-exported/non-open packages 1160 Set<String> concealed = new TreeSet<>(md.packages()); 1161 md.exports().stream().map(Exports::source).forEach(concealed::remove); 1162 md.opens().stream().map(Opens::source).forEach(concealed::remove); 1163 concealed.forEach(p -> ostream.format("contains %s%n", p)); 1164 } 1165 1166 /** 1167 * Prints a single line with the module name, version and modifiers 1168 */ 1169 private static void showModule(ModuleReference mref) { 1170 ModuleDescriptor md = mref.descriptor(); 1171 ostream.print(md.toNameAndVersion()); 1172 mref.location() 1173 .filter(uri -> !isJrt(uri)) 1174 .ifPresent(uri -> ostream.format(" %s", uri)); 1175 if (md.isOpen()) 1176 ostream.print(" open"); 1177 if (md.isAutomatic()) 1178 ostream.print(" automatic"); 1179 ostream.println(); 1180 } 1181 1182 /** 1183 * A ModuleReference comparator that considers modules in the run-time 1184 * image to be less than modules than not in the run-time image. 1185 */ 1186 private static class JrtFirstComparator implements Comparator<ModuleReference> { 1187 private final Comparator<ModuleReference> real; 1188 1189 JrtFirstComparator() { 1190 this.real = Comparator.comparing(ModuleReference::descriptor); 1191 } 1192 1193 @Override 1194 public int compare(ModuleReference a, ModuleReference b) { 1195 if (isJrt(a)) { 1196 return isJrt(b) ? real.compare(a, b) : -1; 1197 } else { 1198 return isJrt(b) ? 1 : real.compare(a, b); 1199 } 1200 } 1201 } 1202 1203 private static <T> Stream<String> toStringStream(Set<T> s) { 1204 return s.stream().map(e -> e.toString().toLowerCase()); 1205 } 1206 1207 private static boolean isJrt(ModuleReference mref) { 1208 return isJrt(mref.location().orElse(null)); 1209 } 1210 1211 private static boolean isJrt(URI uri) { 1212 return (uri != null && uri.getScheme().equalsIgnoreCase("jrt")); 1213 } 1214 1215 /** 1216 * Called by the launcher to validate the modules on the upgrade and 1217 * application module paths. 1218 * 1219 * @return {@code true} if no errors are found 1220 */ 1221 private static boolean validateModules() { 1222 initOutput(System.out); 1223 1224 ModuleValidator validator = new ModuleValidator(); 1225 1226 // upgrade module path 1227 String value = System.getProperty("jdk.module.upgrade.path"); 1228 if (value != null) { 1229 Stream.of(value.split(File.pathSeparator)) 1230 .map(Paths::get) 1231 .forEach(validator::scan); 1232 } 1233 1234 // system modules 1235 ModuleFinder.ofSystem().findAll().stream() 1236 .sorted(Comparator.comparing(ModuleReference::descriptor)) 1237 .forEach(validator::process); 1238 1239 // application module path 1240 value = System.getProperty("jdk.module.path"); 1241 if (value != null) { 1242 Stream.of(value.split(File.pathSeparator)) 1243 .map(Paths::get) 1244 .forEach(validator::scan); 1245 } 1246 1247 return !validator.foundErrors(); 1248 } 1249 1250 /** 1251 * A simple validator to check for errors and conflicts between modules. 1252 */ 1253 static class ModuleValidator { 1254 private static final String MODULE_INFO = "module-info.class"; 1255 1256 private Map<String, ModuleReference> nameToModule = new HashMap<>(); 1257 private Map<String, ModuleReference> packageToModule = new HashMap<>(); 1258 private boolean errorFound; 1259 1260 /** 1261 * Returns true if at least one error was found 1262 */ 1263 boolean foundErrors() { 1264 return errorFound; 1265 } 1266 1267 /** 1268 * Prints the module location and name. 1269 */ 1270 private void printModule(ModuleReference mref) { 1271 mref.location() 1272 .filter(uri -> !isJrt(uri)) 1273 .ifPresent(uri -> ostream.print(uri + " ")); 1274 ModuleDescriptor descriptor = mref.descriptor(); 1275 ostream.print(descriptor.name()); 1276 if (descriptor.isAutomatic()) 1277 ostream.print(" automatic"); 1278 ostream.println(); 1279 } 1280 1281 /** 1282 * Prints the module location and name, checks if the module is 1283 * shadowed by a previously seen module, and finally checks for 1284 * package conflicts with previously seen modules. 1285 */ 1286 void process(ModuleReference mref) { 1287 printModule(mref); 1288 1289 String name = mref.descriptor().name(); 1290 ModuleReference previous = nameToModule.putIfAbsent(name, mref); 1291 if (previous != null) { 1292 ostream.print(INDENT + "shadowed by "); 1293 printModule(previous); 1294 } else { 1295 // check for package conflicts when not shadowed 1296 for (String pkg : mref.descriptor().packages()) { 1297 previous = packageToModule.putIfAbsent(pkg, mref); 1298 if (previous != null) { 1299 String mn = previous.descriptor().name(); 1300 ostream.println(INDENT + "contains " + pkg 1301 + " conflicts with module " + mn); 1302 errorFound = true; 1303 } 1304 } 1305 } 1306 } 1307 1308 /** 1309 * Scan an element on a module path. The element is a directory 1310 * of modules, an exploded module, or a JAR file. 1311 */ 1312 void scan(Path entry) { 1313 BasicFileAttributes attrs; 1314 try { 1315 attrs = Files.readAttributes(entry, BasicFileAttributes.class); 1316 } catch (NoSuchFileException ignore) { 1317 return; 1318 } catch (IOException ioe) { 1319 ostream.println(entry + " " + ioe); 1320 errorFound = true; 1321 return; 1322 } 1323 1324 String fn = entry.getFileName().toString(); 1325 if (attrs.isRegularFile() && fn.endsWith(".jar")) { 1326 // JAR file, explicit or automatic module 1327 scanModule(entry).ifPresent(this::process); 1328 } else if (attrs.isDirectory()) { 1329 Path mi = entry.resolve(MODULE_INFO); 1330 if (Files.exists(mi)) { 1331 // exploded module 1332 scanModule(entry).ifPresent(this::process); 1333 } else { 1334 // directory of modules 1335 scanDirectory(entry); 1336 } 1337 } 1338 } 1339 1340 /** 1341 * Scan the JAR files and exploded modules in a directory. 1342 */ 1343 private void scanDirectory(Path dir) { 1344 try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) { 1345 Map<String, Path> moduleToEntry = new HashMap<>(); 1346 1347 for (Path entry : stream) { 1348 BasicFileAttributes attrs; 1349 try { 1350 attrs = Files.readAttributes(entry, BasicFileAttributes.class); 1351 } catch (IOException ioe) { 1352 ostream.println(entry + " " + ioe); 1353 errorFound = true; 1354 continue; 1355 } 1356 1357 ModuleReference mref = null; 1358 1359 String fn = entry.getFileName().toString(); 1360 if (attrs.isRegularFile() && fn.endsWith(".jar")) { 1361 mref = scanModule(entry).orElse(null); 1362 } else if (attrs.isDirectory()) { 1363 Path mi = entry.resolve(MODULE_INFO); 1364 if (Files.exists(mi)) { 1365 mref = scanModule(entry).orElse(null); 1366 } 1367 } 1368 1369 if (mref != null) { 1370 String name = mref.descriptor().name(); 1371 Path previous = moduleToEntry.putIfAbsent(name, entry); 1372 if (previous != null) { 1373 // same name as other module in the directory 1374 printModule(mref); 1375 ostream.println(INDENT + "contains same module as " 1376 + previous.getFileName()); 1377 errorFound = true; 1378 } else { 1379 process(mref); 1380 } 1381 } 1382 } 1383 } catch (IOException ioe) { 1384 ostream.println(dir + " " + ioe); 1385 errorFound = true; 1386 } 1387 } 1388 1389 /** 1390 * Scan a JAR file or exploded module. 1391 */ 1392 private Optional<ModuleReference> scanModule(Path entry) { 1393 ModuleFinder finder = ModuleFinder.of(entry); 1394 try { 1395 return finder.findAll().stream().findFirst(); 1396 } catch (FindException e) { 1397 ostream.println(entry); 1398 ostream.println(INDENT + e.getMessage()); 1399 Throwable cause = e.getCause(); 1400 if (cause != null) { 1401 ostream.println(INDENT + cause); 1402 } 1403 errorFound = true; 1404 return Optional.empty(); 1405 } 1406 } 1407 } 1408 }