1 /* 2 * Copyright (c) 2015, 2016, 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.tools.jmod; 27 28 import java.io.BufferedInputStream; 29 import java.io.BufferedOutputStream; 30 import java.io.ByteArrayInputStream; 31 import java.io.ByteArrayOutputStream; 32 import java.io.File; 33 import java.io.IOException; 34 import java.io.InputStream; 35 import java.io.OutputStream; 36 import java.io.PrintStream; 37 import java.io.UncheckedIOException; 38 import java.lang.module.Configuration; 39 import java.lang.module.ModuleReader; 40 import java.lang.module.ModuleReference; 41 import java.lang.module.ModuleFinder; 42 import java.lang.module.ModuleDescriptor; 43 import java.lang.module.ModuleDescriptor.Exports; 44 import java.lang.module.ModuleDescriptor.Provides; 45 import java.lang.module.ModuleDescriptor.Requires; 46 import java.lang.module.ModuleDescriptor.Version; 47 import java.lang.module.ResolutionException; 48 import java.lang.module.ResolvedModule; 49 import java.net.URI; 50 import java.nio.file.FileSystems; 51 import java.nio.file.FileVisitResult; 52 import java.nio.file.Files; 53 import java.nio.file.InvalidPathException; 54 import java.nio.file.Path; 55 import java.nio.file.PathMatcher; 56 import java.nio.file.Paths; 57 import java.nio.file.SimpleFileVisitor; 58 import java.nio.file.StandardCopyOption; 59 import java.nio.file.attribute.BasicFileAttributes; 60 import java.text.MessageFormat; 61 import java.util.ArrayDeque; 62 import java.util.ArrayList; 63 import java.util.Arrays; 64 import java.util.Collection; 65 import java.util.Collections; 66 import java.util.Comparator; 67 import java.util.Deque; 68 import java.util.HashMap; 69 import java.util.HashSet; 70 import java.util.List; 71 import java.util.Locale; 72 import java.util.Map; 73 import java.util.MissingResourceException; 74 import java.util.Optional; 75 import java.util.ResourceBundle; 76 import java.util.Set; 77 import java.util.function.Consumer; 78 import java.util.function.Function; 79 import java.util.function.Predicate; 80 import java.util.function.Supplier; 81 import java.util.jar.JarEntry; 82 import java.util.jar.JarFile; 83 import java.util.stream.Collectors; 84 import java.util.regex.Pattern; 85 import java.util.regex.PatternSyntaxException; 86 import java.util.zip.ZipEntry; 87 import java.util.zip.ZipException; 88 import java.util.zip.ZipFile; 89 import java.util.zip.ZipInputStream; 90 import java.util.zip.ZipOutputStream; 91 92 import jdk.internal.joptsimple.BuiltinHelpFormatter; 93 import jdk.internal.joptsimple.NonOptionArgumentSpec; 94 import jdk.internal.joptsimple.OptionDescriptor; 95 import jdk.internal.joptsimple.OptionException; 96 import jdk.internal.joptsimple.OptionParser; 97 import jdk.internal.joptsimple.OptionSet; 98 import jdk.internal.joptsimple.OptionSpec; 99 import jdk.internal.joptsimple.ValueConverter; 100 import jdk.internal.misc.JavaLangModuleAccess; 101 import jdk.internal.misc.SharedSecrets; 102 import jdk.internal.module.ConfigurableModuleFinder; 103 import jdk.internal.module.ConfigurableModuleFinder.Phase; 104 import jdk.internal.module.ModuleHashes; 105 import jdk.internal.module.ModuleInfoExtender; 106 import jdk.tools.jlink.internal.Utils; 107 108 import static java.util.stream.Collectors.joining; 109 110 /** 111 * Implementation for the jmod tool. 112 */ 113 public class JmodTask { 114 115 static class CommandException extends RuntimeException { 116 private static final long serialVersionUID = 0L; 117 boolean showUsage; 118 119 CommandException(String key, Object... args) { 120 super(getMessageOrKey(key, args)); 121 } 122 123 CommandException showUsage(boolean b) { 124 showUsage = b; 125 return this; 126 } 127 128 private static String getMessageOrKey(String key, Object... args) { 129 try { 130 return MessageFormat.format(ResourceBundleHelper.bundle.getString(key), args); 131 } catch (MissingResourceException e) { 132 return key; 133 } 134 } 135 } 136 137 private static final String PROGNAME = "jmod"; 138 private static final String MODULE_INFO = "module-info.class"; 139 140 private Options options; 141 private PrintStream out = System.out; 142 void setLog(PrintStream out) { 143 this.out = out; 144 } 145 146 /* Result codes. */ 147 static final int EXIT_OK = 0, // Completed with no errors. 148 EXIT_ERROR = 1, // Completed but reported errors. 149 EXIT_CMDERR = 2, // Bad command-line arguments 150 EXIT_SYSERR = 3, // System error or resource exhaustion. 151 EXIT_ABNORMAL = 4;// terminated abnormally 152 153 enum Mode { 154 CREATE, 155 LIST, 156 DESCRIBE, 157 HASH 158 }; 159 160 static class Options { 161 Mode mode; 162 Path jmodFile; 163 boolean help; 164 boolean version; 165 List<Path> classpath; 166 List<Path> cmds; 167 List<Path> configs; 168 List<Path> libs; 169 ModuleFinder moduleFinder; 170 Version moduleVersion; 171 String mainClass; 172 String osName; 173 String osArch; 174 String osVersion; 175 Pattern modulesToHash; 176 boolean dryrun; 177 List<PathMatcher> excludes; 178 } 179 180 public int run(String[] args) { 181 182 try { 183 handleOptions(args); 184 if (options == null) { 185 showUsageSummary(); 186 return EXIT_CMDERR; 187 } 188 if (options.help) { 189 showHelp(); 190 return EXIT_OK; 191 } 192 if (options.version) { 193 showVersion(); 194 return EXIT_OK; 195 } 196 197 boolean ok; 198 switch (options.mode) { 199 case CREATE: 200 ok = create(); 201 break; 202 case LIST: 203 ok = list(); 204 break; 205 case DESCRIBE: 206 ok = describe(); 207 break; 208 case HASH: 209 ok = hashModules(); 210 break; 211 default: 212 throw new AssertionError("Unknown mode: " + options.mode.name()); 213 } 214 215 return ok ? EXIT_OK : EXIT_ERROR; 216 } catch (CommandException e) { 217 reportError(e.getMessage()); 218 if (e.showUsage) 219 showUsageSummary(); 220 return EXIT_CMDERR; 221 } catch (Exception x) { 222 reportError(x.getMessage()); 223 x.printStackTrace(); 224 return EXIT_ABNORMAL; 225 } finally { 226 out.flush(); 227 } 228 } 229 230 private boolean list() throws IOException { 231 ZipFile zip = null; 232 try { 233 try { 234 zip = new ZipFile(options.jmodFile.toFile()); 235 } catch (IOException x) { 236 throw new IOException("error opening jmod file", x); 237 } 238 239 // Trivially print the archive entries for now, pending a more complete implementation 240 zip.stream().forEach(e -> out.println(e.getName())); 241 return true; 242 } finally { 243 if (zip != null) 244 zip.close(); 245 } 246 } 247 248 private boolean hashModules() { 249 return new Hasher(options.moduleFinder).run(); 250 } 251 252 private boolean describe() throws IOException { 253 ZipFile zip = null; 254 try { 255 try { 256 zip = new ZipFile(options.jmodFile.toFile()); 257 } catch (IOException x) { 258 throw new IOException("error opening jmod file", x); 259 } 260 261 try (InputStream in = Files.newInputStream(options.jmodFile)) { 262 boolean found = printModuleDescriptor(in); 263 if (!found) 264 throw new CommandException("err.module.descriptor.not.found"); 265 return found; 266 } 267 } finally { 268 if (zip != null) 269 zip.close(); 270 } 271 } 272 273 static <T> String toString(Set<T> set) { 274 if (set.isEmpty()) { return ""; } 275 return set.stream().map(e -> e.toString().toLowerCase(Locale.ROOT)) 276 .collect(joining(" ")); 277 } 278 279 private static final JavaLangModuleAccess JLMA = SharedSecrets.getJavaLangModuleAccess(); 280 281 private boolean printModuleDescriptor(InputStream in) 282 throws IOException 283 { 284 final String mi = Section.CLASSES.jmodDir() + "/" + MODULE_INFO; 285 try (BufferedInputStream bis = new BufferedInputStream(in); 286 ZipInputStream zis = new ZipInputStream(bis)) { 287 288 ZipEntry e; 289 while ((e = zis.getNextEntry()) != null) { 290 if (e.getName().equals(mi)) { 291 ModuleDescriptor md = ModuleDescriptor.read(zis); 292 StringBuilder sb = new StringBuilder(); 293 sb.append("\n").append(md.toNameAndVersion()); 294 295 md.requires().stream() 296 .sorted(Comparator.comparing(Requires::name)) 297 .forEach(r -> { 298 sb.append("\n requires "); 299 if (!r.modifiers().isEmpty()) 300 sb.append(toString(r.modifiers())).append(" "); 301 sb.append(r.name()); 302 }); 303 304 md.uses().stream().sorted() 305 .forEach(s -> sb.append("\n uses ").append(s)); 306 307 md.exports().stream() 308 .sorted(Comparator.comparing(Exports::source)) 309 .forEach(p -> sb.append("\n exports ").append(p)); 310 311 md.conceals().stream().sorted() 312 .forEach(p -> sb.append("\n conceals ").append(p)); 313 314 md.provides().values().stream() 315 .sorted(Comparator.comparing(Provides::service)) 316 .forEach(p -> sb.append("\n provides ").append(p.service()) 317 .append(" with ") 318 .append(toString(p.providers()))); 319 320 md.mainClass().ifPresent(v -> sb.append("\n main-class " + v)); 321 322 md.osName().ifPresent(v -> sb.append("\n operating-system-name " + v)); 323 324 md.osArch().ifPresent(v -> sb.append("\n operating-system-architecture " + v)); 325 326 md.osVersion().ifPresent(v -> sb.append("\n operating-system-version " + v)); 327 328 JLMA.hashes(md).ifPresent( 329 hashes -> hashes.names().stream().sorted().forEach( 330 mod -> sb.append("\n hashes ").append(mod).append(" ") 331 .append(hashes.algorithm()).append(" ") 332 .append(hashes.hashFor(mod)))); 333 334 out.println(sb.toString()); 335 return true; 336 } 337 } 338 } 339 return false; 340 } 341 342 private boolean create() throws IOException { 343 JmodFileWriter jmod = new JmodFileWriter(); 344 345 // create jmod with temporary name to avoid it being examined 346 // when scanning the module path 347 Path target = options.jmodFile; 348 Path tempTarget = target.resolveSibling(target.getFileName() + ".tmp"); 349 try { 350 try (OutputStream out = Files.newOutputStream(tempTarget); 351 BufferedOutputStream bos = new BufferedOutputStream(out)) { 352 jmod.write(bos); 353 } 354 Files.move(tempTarget, target); 355 } catch (Exception e) { 356 if (Files.exists(tempTarget)) { 357 try { 358 Files.delete(tempTarget); 359 } catch (IOException ioe) { 360 e.addSuppressed(ioe); 361 } 362 } 363 throw e; 364 } 365 return true; 366 } 367 368 private class JmodFileWriter { 369 final List<Path> cmds = options.cmds; 370 final List<Path> libs = options.libs; 371 final List<Path> configs = options.configs; 372 final List<Path> classpath = options.classpath; 373 final Version moduleVersion = options.moduleVersion; 374 final String mainClass = options.mainClass; 375 final String osName = options.osName; 376 final String osArch = options.osArch; 377 final String osVersion = options.osVersion; 378 final List<PathMatcher> excludes = options.excludes; 379 final Hasher hasher = hasher(); 380 381 JmodFileWriter() { } 382 383 /** 384 * Writes the jmod to the given output stream. 385 */ 386 void write(OutputStream out) throws IOException { 387 try (ZipOutputStream zos = new ZipOutputStream(out)) { 388 389 // module-info.class 390 writeModuleInfo(zos, findPackages(classpath)); 391 392 // classes 393 processClasses(zos, classpath); 394 395 processSection(zos, Section.NATIVE_CMDS, cmds); 396 processSection(zos, Section.NATIVE_LIBS, libs); 397 processSection(zos, Section.CONFIG, configs); 398 } 399 } 400 401 /** 402 * Returns a supplier of an input stream to the module-info.class 403 * on the class path of directories and JAR files. 404 */ 405 Supplier<InputStream> newModuleInfoSupplier() throws IOException { 406 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 407 for (Path e: classpath) { 408 if (Files.isDirectory(e)) { 409 Path mi = e.resolve(MODULE_INFO); 410 if (Files.isRegularFile(mi)) { 411 Files.copy(mi, baos); 412 break; 413 } 414 } else if (Files.isRegularFile(e) && e.toString().endsWith(".jar")) { 415 try (JarFile jf = new JarFile(e.toFile())) { 416 ZipEntry entry = jf.getEntry(MODULE_INFO); 417 if (entry != null) { 418 jf.getInputStream(entry).transferTo(baos); 419 break; 420 } 421 } catch (ZipException x) { 422 // Skip. Do nothing. No packages will be added. 423 } 424 } 425 } 426 if (baos.size() == 0) { 427 return null; 428 } else { 429 byte[] bytes = baos.toByteArray(); 430 return () -> new ByteArrayInputStream(bytes); 431 } 432 } 433 434 /** 435 * Writes the updated module-info.class to the ZIP output stream. 436 * 437 * The updated module-info.class will have a ConcealedPackages attribute 438 * with the set of module-private/non-exported packages. 439 * 440 * If --module-version, --main-class, or other options were provided 441 * then the corresponding class file attributes are added to the 442 * module-info here. 443 */ 444 void writeModuleInfo(ZipOutputStream zos, Set<String> packages) 445 throws IOException 446 { 447 Supplier<InputStream> miSupplier = newModuleInfoSupplier(); 448 if (miSupplier == null) { 449 throw new IOException(MODULE_INFO + " not found"); 450 } 451 452 ModuleDescriptor descriptor; 453 try (InputStream in = miSupplier.get()) { 454 descriptor = ModuleDescriptor.read(in); 455 } 456 457 // copy the module-info.class into the jmod with the additional 458 // attributes for the version, main class and other meta data 459 try (InputStream in = miSupplier.get()) { 460 ModuleInfoExtender extender = ModuleInfoExtender.newExtender(in); 461 462 // Add (or replace) the ConcealedPackages attribute 463 if (packages != null) { 464 Set<String> exported = descriptor.exports().stream() 465 .map(ModuleDescriptor.Exports::source) 466 .collect(Collectors.toSet()); 467 Set<String> concealed = packages.stream() 468 .filter(p -> !exported.contains(p)) 469 .collect(Collectors.toSet()); 470 extender.conceals(concealed); 471 } 472 473 // --main-class 474 if (mainClass != null) 475 extender.mainClass(mainClass); 476 477 // --os-name, --os-arch, --os-version 478 if (osName != null || osArch != null || osVersion != null) 479 extender.targetPlatform(osName, osArch, osVersion); 480 481 // --module-version 482 if (moduleVersion != null) 483 extender.version(moduleVersion); 484 485 if (hasher != null) { 486 ModuleHashes moduleHashes = hasher.computeHashes(descriptor.name()); 487 if (moduleHashes != null) { 488 extender.hashes(moduleHashes); 489 } else { 490 warning("warn.no.module.hashes", descriptor.name()); 491 } 492 } 493 494 // write the (possibly extended or modified) module-info.class 495 String e = Section.CLASSES.jmodDir() + "/" + MODULE_INFO; 496 ZipEntry ze = new ZipEntry(e); 497 zos.putNextEntry(ze); 498 extender.write(zos); 499 zos.closeEntry(); 500 } 501 } 502 503 /* 504 * Hasher resolves a module graph using the --hash-modules PATTERN 505 * as the roots. 506 * 507 * The jmod file is being created and does not exist in the 508 * given modulepath. 509 */ 510 private Hasher hasher() { 511 if (options.modulesToHash == null) 512 return null; 513 514 try { 515 Supplier<InputStream> miSupplier = newModuleInfoSupplier(); 516 if (miSupplier == null) { 517 throw new IOException(MODULE_INFO + " not found"); 518 } 519 520 ModuleDescriptor descriptor; 521 try (InputStream in = miSupplier.get()) { 522 descriptor = ModuleDescriptor.read(in); 523 } 524 525 URI uri = options.jmodFile.toUri(); 526 ModuleReference mref = new ModuleReference(descriptor, uri, new Supplier<>() { 527 @Override 528 public ModuleReader get() { 529 throw new UnsupportedOperationException(); 530 } 531 }); 532 533 // compose a module finder with the module path and also 534 // a module finder that can find the jmod file being created 535 ModuleFinder finder = ModuleFinder.compose(options.moduleFinder, 536 new ModuleFinder() { 537 @Override 538 public Optional<ModuleReference> find(String name) { 539 if (descriptor.name().equals(name)) 540 return Optional.of(mref); 541 else return Optional.empty(); 542 } 543 544 @Override 545 public Set<ModuleReference> findAll() { 546 return Collections.singleton(mref); 547 } 548 }); 549 550 return new Hasher(finder); 551 } catch (IOException e) { 552 throw new UncheckedIOException(e); 553 } 554 } 555 556 /** 557 * Returns the set of all packages on the given class path. 558 */ 559 Set<String> findPackages(List<Path> classpath) { 560 Set<String> packages = new HashSet<>(); 561 for (Path path : classpath) { 562 if (Files.isDirectory(path)) { 563 packages.addAll(findPackages(path)); 564 } else if (Files.isRegularFile(path) && path.toString().endsWith(".jar")) { 565 try (JarFile jf = new JarFile(path.toString())) { 566 packages.addAll(findPackages(jf)); 567 } catch (ZipException x) { 568 // Skip. Do nothing. No packages will be added. 569 } catch (IOException ioe) { 570 throw new UncheckedIOException(ioe); 571 } 572 } 573 } 574 return packages; 575 } 576 577 /** 578 * Returns the set of packages in the given directory tree. 579 */ 580 Set<String> findPackages(Path dir) { 581 try { 582 return Files.find(dir, Integer.MAX_VALUE, 583 ((path, attrs) -> attrs.isRegularFile() && 584 path.toString().endsWith(".class"))) 585 .map(path -> toPackageName(dir.relativize(path))) 586 .filter(pkg -> pkg.length() > 0) // module-info 587 .distinct() 588 .collect(Collectors.toSet()); 589 } catch (IOException ioe) { 590 throw new UncheckedIOException(ioe); 591 } 592 } 593 594 /** 595 * Returns the set of packages in the given JAR file. 596 */ 597 Set<String> findPackages(JarFile jf) { 598 return jf.stream() 599 .filter(e -> e.getName().endsWith(".class")) 600 .map(e -> toPackageName(e)) 601 .filter(pkg -> pkg.length() > 0) // module-info 602 .distinct() 603 .collect(Collectors.toSet()); 604 } 605 606 String toPackageName(Path path) { 607 String name = path.toString(); 608 assert name.endsWith(".class"); 609 int index = name.lastIndexOf(File.separatorChar); 610 if (index != -1) 611 return name.substring(0, index).replace(File.separatorChar, '.'); 612 613 if (!name.equals(MODULE_INFO)) { 614 IOException e = new IOException(name + " in the unnamed package"); 615 throw new UncheckedIOException(e); 616 } 617 return ""; 618 } 619 620 String toPackageName(ZipEntry entry) { 621 String name = entry.getName(); 622 assert name.endsWith(".class"); 623 int index = name.lastIndexOf("/"); 624 if (index != -1) 625 return name.substring(0, index).replace('/', '.'); 626 else 627 return ""; 628 } 629 630 void processClasses(ZipOutputStream zos, List<Path> classpaths) 631 throws IOException 632 { 633 if (classpaths == null) 634 return; 635 636 for (Path p : classpaths) { 637 if (Files.isDirectory(p)) { 638 processSection(zos, Section.CLASSES, p); 639 } else if (Files.isRegularFile(p) && p.toString().endsWith(".jar")) { 640 try (JarFile jf = new JarFile(p.toFile())) { 641 JarEntryConsumer jec = new JarEntryConsumer(zos, jf); 642 jf.stream().filter(jec).forEach(jec); 643 } 644 } 645 } 646 } 647 648 void processSection(ZipOutputStream zos, Section section, List<Path> paths) 649 throws IOException 650 { 651 if (paths == null) 652 return; 653 654 for (Path p : paths) 655 processSection(zos, section, p); 656 } 657 658 void processSection(ZipOutputStream zos, Section section, Path top) 659 throws IOException 660 { 661 final String prefix = section.jmodDir(); 662 663 Files.walkFileTree(top, new SimpleFileVisitor<Path>() { 664 @Override 665 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) 666 throws IOException 667 { 668 Path relPath = top.relativize(file); 669 if (relPath.toString().equals(MODULE_INFO) 670 && !Section.CLASSES.equals(section)) 671 warning("warn.ignore.entry", MODULE_INFO, section); 672 673 if (!relPath.toString().equals(MODULE_INFO) 674 && !matches(relPath, excludes)) { 675 try (InputStream in = Files.newInputStream(file)) { 676 writeZipEntry(zos, in, prefix, relPath.toString()); 677 } 678 } 679 return FileVisitResult.CONTINUE; 680 } 681 }); 682 } 683 684 boolean matches(Path path, List<PathMatcher> matchers) { 685 if (matchers != null) { 686 for (PathMatcher pm : matchers) { 687 if (pm.matches(path)) 688 return true; 689 } 690 } 691 return false; 692 } 693 694 void writeZipEntry(ZipOutputStream zos, InputStream in, String prefix, String other) 695 throws IOException 696 { 697 String name = Paths.get(prefix, other).toString() 698 .replace(File.separatorChar, '/'); 699 ZipEntry ze = new ZipEntry(name); 700 try { 701 zos.putNextEntry(ze); 702 in.transferTo(zos); 703 zos.closeEntry(); 704 } catch (ZipException x) { 705 if (x.getMessage().contains("duplicate entry")) { 706 warning("warn.ignore.duplicate.entry", name, prefix); 707 return; 708 } 709 throw x; 710 } 711 } 712 713 class JarEntryConsumer implements Consumer<JarEntry>, Predicate<JarEntry> { 714 final ZipOutputStream zos; 715 final JarFile jarfile; 716 JarEntryConsumer(ZipOutputStream zos, JarFile jarfile) { 717 this.zos = zos; 718 this.jarfile = jarfile; 719 } 720 @Override 721 public void accept(JarEntry je) { 722 try (InputStream in = jarfile.getInputStream(je)) { 723 writeZipEntry(zos, in, Section.CLASSES.jmodDir(), je.getName()); 724 } catch (IOException e) { 725 throw new UncheckedIOException(e); 726 } 727 } 728 @Override 729 public boolean test(JarEntry je) { 730 String name = je.getName(); 731 // ## no support for excludes. Is it really needed? 732 return !name.endsWith(MODULE_INFO) && !je.isDirectory(); 733 } 734 } 735 } 736 737 /** 738 * Compute and record hashes 739 */ 740 private class Hasher { 741 final ModuleFinder moduleFinder; 742 final Map<String, Path> moduleNameToPath; 743 final Set<String> modules; 744 final Configuration configuration; 745 final boolean dryrun = options.dryrun; 746 Hasher(ModuleFinder finder) { 747 this.moduleFinder = finder; 748 // Determine the modules that matches the pattern {@code modulesToHash} 749 this.modules = moduleFinder.findAll().stream() 750 .map(mref -> mref.descriptor().name()) 751 .filter(mn -> options.modulesToHash.matcher(mn).find()) 752 .collect(Collectors.toSet()); 753 754 // a map from a module name to Path of the packaged module 755 this.moduleNameToPath = moduleFinder.findAll().stream() 756 .map(mref -> mref.descriptor().name()) 757 .collect(Collectors.toMap(Function.identity(), mn -> moduleToPath(mn))); 758 759 // get a resolved module graph 760 Configuration config = null; 761 try { 762 config = Configuration.empty() 763 .resolveRequires(ModuleFinder.ofSystem(), moduleFinder, modules); 764 } catch (ResolutionException e) { 765 warning("warn.module.resolution.fail", e.getMessage()); 766 } 767 this.configuration = config; 768 } 769 770 /** 771 * This method is for jmod hash command. 772 * 773 * Identify the base modules in the module graph, i.e. no outgoing edge 774 * to any of the modules to be hashed. 775 * 776 * For each base module M, compute the hashes of all modules that depend 777 * upon M directly or indirectly. Then update M's module-info.class 778 * to record the hashes. 779 */ 780 boolean run() { 781 if (configuration == null) 782 return false; 783 784 // transposed graph containing the the packaged modules and 785 // its transitive dependences matching --hash-modules 786 Map<String, Set<String>> graph = new HashMap<>(); 787 for (String root : modules) { 788 Deque<String> deque = new ArrayDeque<>(); 789 deque.add(root); 790 Set<String> visited = new HashSet<>(); 791 while (!deque.isEmpty()) { 792 String mn = deque.pop(); 793 if (!visited.contains(mn)) { 794 visited.add(mn); 795 796 if (modules.contains(mn)) 797 graph.computeIfAbsent(mn, _k -> new HashSet<>()); 798 799 ResolvedModule resolvedModule = configuration.findModule(mn).get(); 800 for (ResolvedModule dm : resolvedModule.reads()) { 801 String name = dm.name(); 802 if (!visited.contains(name)) { 803 deque.push(name); 804 } 805 806 // reverse edge 807 if (modules.contains(name) && modules.contains(mn)) { 808 graph.computeIfAbsent(name, _k -> new HashSet<>()).add(mn); 809 } 810 } 811 } 812 } 813 } 814 815 if (dryrun) 816 out.println("Dry run:"); 817 818 // each node in a transposed graph is a matching packaged module 819 // in which the hash of the modules that depend upon it is recorded 820 graph.entrySet().stream() 821 .filter(e -> !e.getValue().isEmpty()) 822 .forEach(e -> { 823 String mn = e.getKey(); 824 Map<String, Path> modulesForHash = e.getValue().stream() 825 .collect(Collectors.toMap(Function.identity(), 826 moduleNameToPath::get)); 827 ModuleHashes hashes = ModuleHashes.generate(modulesForHash, "SHA-256"); 828 if (dryrun) { 829 out.format("%s%n", mn); 830 hashes.names().stream() 831 .sorted() 832 .forEach(name -> out.format(" hashes %s %s %s%n", 833 name, hashes.algorithm(), hashes.hashFor(name))); 834 } else { 835 try { 836 updateModuleInfo(mn, hashes); 837 } catch (IOException ex) { 838 throw new UncheckedIOException(ex); 839 } 840 } 841 }); 842 return true; 843 } 844 845 /** 846 * Compute hashes of the specified module. 847 * 848 * It records the hashing modules that depend upon the specified 849 * module directly or indirectly. 850 */ 851 ModuleHashes computeHashes(String name) { 852 if (configuration == null) 853 return null; 854 855 // the transposed graph includes all modules in the resolved graph 856 Map<String, Set<String>> graph = transpose(); 857 858 // find the modules that transitively depend upon the specified name 859 Deque<String> deque = new ArrayDeque<>(); 860 deque.add(name); 861 Set<String> mods = visitNodes(graph, deque); 862 863 // filter modules matching the pattern specified --hash-modules 864 // as well as itself as the jmod file is being generated 865 Map<String, Path> modulesForHash = mods.stream() 866 .filter(mn -> !mn.equals(name) && modules.contains(mn)) 867 .collect(Collectors.toMap(Function.identity(), moduleNameToPath::get)); 868 869 if (modulesForHash.isEmpty()) 870 return null; 871 872 return ModuleHashes.generate(modulesForHash, "SHA-256"); 873 } 874 875 /** 876 * Returns all nodes traversed from the given roots. 877 */ 878 private Set<String> visitNodes(Map<String, Set<String>> graph, 879 Deque<String> roots) { 880 Set<String> visited = new HashSet<>(); 881 while (!roots.isEmpty()) { 882 String mn = roots.pop(); 883 if (!visited.contains(mn)) { 884 visited.add(mn); 885 // the given roots may not be part of the graph 886 if (graph.containsKey(mn)) { 887 for (String dm : graph.get(mn)) { 888 if (!visited.contains(dm)) { 889 roots.push(dm); 890 } 891 } 892 } 893 } 894 } 895 return visited; 896 } 897 898 /** 899 * Returns a transposed graph from the resolved module graph. 900 */ 901 private Map<String, Set<String>> transpose() { 902 Map<String, Set<String>> transposedGraph = new HashMap<>(); 903 Deque<String> deque = new ArrayDeque<>(modules); 904 905 Set<String> visited = new HashSet<>(); 906 while (!deque.isEmpty()) { 907 String mn = deque.pop(); 908 if (!visited.contains(mn)) { 909 visited.add(mn); 910 911 transposedGraph.computeIfAbsent(mn, _k -> new HashSet<>()); 912 913 ResolvedModule resolvedModule = configuration.findModule(mn).get(); 914 for (ResolvedModule dm : resolvedModule.reads()) { 915 String name = dm.name(); 916 if (!visited.contains(name)) { 917 deque.push(name); 918 } 919 920 // reverse edge 921 transposedGraph.computeIfAbsent(name, _k -> new HashSet<>()) 922 .add(mn); 923 } 924 } 925 } 926 return transposedGraph; 927 } 928 929 /** 930 * Reads the given input stream of module-info.class and write 931 * the extended module-info.class with the given ModuleHashes 932 * 933 * @param in InputStream of module-info.class 934 * @param out OutputStream to write the extended module-info.class 935 * @param hashes ModuleHashes 936 */ 937 private void recordHashes(InputStream in, OutputStream out, ModuleHashes hashes) 938 throws IOException 939 { 940 ModuleInfoExtender extender = ModuleInfoExtender.newExtender(in); 941 extender.hashes(hashes); 942 extender.write(out); 943 } 944 945 private void updateModuleInfo(String name, ModuleHashes moduleHashes) 946 throws IOException 947 { 948 Path target = moduleNameToPath.get(name); 949 Path tempTarget = target.resolveSibling(target.getFileName() + ".tmp"); 950 ZipFile zip = new ZipFile(target.toFile()); 951 try { 952 try (OutputStream out = Files.newOutputStream(tempTarget); 953 ZipOutputStream zos = new ZipOutputStream(out)) { 954 zip.stream().forEach(e -> { 955 try { 956 InputStream in = zip.getInputStream(e); 957 if (e.getName().equals(MODULE_INFO) || 958 e.getName().equals(Section.CLASSES.jmodDir() + "/" + MODULE_INFO)) { 959 ZipEntry ze = new ZipEntry(e.getName()); 960 ze.setTime(System.currentTimeMillis()); 961 zos.putNextEntry(ze); 962 recordHashes(in, zos, moduleHashes); 963 zos.closeEntry(); 964 } else { 965 zos.putNextEntry(e); 966 zos.write(in.readAllBytes()); 967 zos.closeEntry(); 968 } 969 } catch (IOException x) { 970 throw new UncheckedIOException(x); 971 } 972 }); 973 } 974 } catch (IOException|RuntimeException e) { 975 if (Files.exists(tempTarget)) { 976 try { 977 Files.delete(tempTarget); 978 } catch (IOException ioe) { 979 e.addSuppressed(ioe); 980 } 981 } 982 throw e; 983 } finally { 984 zip.close(); 985 } 986 out.println(getMessage("module.hashes.recorded", name)); 987 Files.move(tempTarget, target, StandardCopyOption.REPLACE_EXISTING); 988 } 989 990 private Path moduleToPath(String name) { 991 ModuleReference mref = moduleFinder.find(name).orElseThrow( 992 () -> new InternalError("Selected module " + name + " not on module path")); 993 994 URI uri = mref.location().get(); 995 Path path = Paths.get(uri); 996 String fn = path.getFileName().toString(); 997 if (!fn.endsWith(".jar") && !fn.endsWith(".jmod")) { 998 throw new InternalError(path + " is not a modular JAR or jmod file"); 999 } 1000 return path; 1001 } 1002 } 1003 1004 enum Section { 1005 NATIVE_LIBS("native"), 1006 NATIVE_CMDS("bin"), 1007 CLASSES("classes"), 1008 CONFIG("conf"), 1009 UNKNOWN("unknown"); 1010 1011 private final String jmodDir; 1012 1013 Section(String jmodDir) { 1014 this.jmodDir = jmodDir; 1015 } 1016 1017 String jmodDir() { return jmodDir; } 1018 } 1019 1020 static class ClassPathConverter implements ValueConverter<Path> { 1021 static final ValueConverter<Path> INSTANCE = new ClassPathConverter(); 1022 1023 private static final Path CWD = Paths.get(""); 1024 1025 @Override 1026 public Path convert(String value) { 1027 try { 1028 Path path = CWD.resolve(value); 1029 if (Files.notExists(path)) 1030 throw new CommandException("err.path.not.found", path); 1031 if (! (Files.isDirectory(path) || 1032 (Files.isRegularFile(path) && path.toString().endsWith(".jar")))) 1033 throw new CommandException("err.invalid.class.path.entry", path); 1034 return path; 1035 } catch (InvalidPathException x) { 1036 throw new CommandException("err.path.not.valid", value); 1037 } 1038 } 1039 1040 @Override public Class<Path> valueType() { return Path.class; } 1041 1042 @Override public String valuePattern() { return "path"; } 1043 } 1044 1045 static class DirPathConverter implements ValueConverter<Path> { 1046 static final ValueConverter<Path> INSTANCE = new DirPathConverter(); 1047 1048 private static final Path CWD = Paths.get(""); 1049 1050 @Override 1051 public Path convert(String value) { 1052 try { 1053 Path path = CWD.resolve(value); 1054 if (Files.notExists(path)) 1055 throw new CommandException("err.path.not.found", path); 1056 if (!Files.isDirectory(path)) 1057 throw new CommandException("err.path.not.a.dir", path); 1058 return path; 1059 } catch (InvalidPathException x) { 1060 throw new CommandException("err.path.not.valid", value); 1061 } 1062 } 1063 1064 @Override public Class<Path> valueType() { return Path.class; } 1065 1066 @Override public String valuePattern() { return "path"; } 1067 } 1068 1069 static class ModuleVersionConverter implements ValueConverter<Version> { 1070 @Override 1071 public Version convert(String value) { 1072 try { 1073 return Version.parse(value); 1074 } catch (IllegalArgumentException x) { 1075 throw new CommandException("err.invalid.version", x.getMessage()); 1076 } 1077 } 1078 1079 @Override public Class<Version> valueType() { return Version.class; } 1080 1081 @Override public String valuePattern() { return "module-version"; } 1082 } 1083 1084 static class PatternConverter implements ValueConverter<Pattern> { 1085 @Override 1086 public Pattern convert(String value) { 1087 try { 1088 if (value.startsWith("regex:")) { 1089 value = value.substring("regex:".length()).trim(); 1090 } 1091 1092 return Pattern.compile(value); 1093 } catch (PatternSyntaxException e) { 1094 throw new CommandException("err.bad.pattern", value); 1095 } 1096 } 1097 1098 @Override public Class<Pattern> valueType() { return Pattern.class; } 1099 1100 @Override public String valuePattern() { return "regex-pattern"; } 1101 } 1102 1103 static class PathMatcherConverter implements ValueConverter<PathMatcher> { 1104 @Override 1105 public PathMatcher convert(String pattern) { 1106 try { 1107 return Utils.getPathMatcher(FileSystems.getDefault(), pattern); 1108 } catch (PatternSyntaxException e) { 1109 throw new CommandException("err.bad.pattern", pattern); 1110 } 1111 } 1112 1113 @Override public Class<PathMatcher> valueType() { return PathMatcher.class; } 1114 1115 @Override public String valuePattern() { return "pattern-list"; } 1116 } 1117 1118 /* Support for @<file> in jmod help */ 1119 private static final String CMD_FILENAME = "@<filename>"; 1120 1121 /** 1122 * This formatter is adding the @filename option and does the required 1123 * formatting. 1124 */ 1125 private static final class JmodHelpFormatter extends BuiltinHelpFormatter { 1126 1127 private JmodHelpFormatter() { super(80, 2); } 1128 1129 @Override 1130 public String format(Map<String, ? extends OptionDescriptor> options) { 1131 Map<String, OptionDescriptor> all = new HashMap<>(); 1132 all.putAll(options); 1133 all.put(CMD_FILENAME, new OptionDescriptor() { 1134 @Override 1135 public Collection<String> options() { 1136 List<String> ret = new ArrayList<>(); 1137 ret.add(CMD_FILENAME); 1138 return ret; 1139 } 1140 @Override 1141 public String description() { return getMessage("main.opt.cmdfile"); } 1142 @Override 1143 public List<?> defaultValues() { return Collections.emptyList(); } 1144 @Override 1145 public boolean isRequired() { return false; } 1146 @Override 1147 public boolean acceptsArguments() { return false; } 1148 @Override 1149 public boolean requiresArgument() { return false; } 1150 @Override 1151 public String argumentDescription() { return null; } 1152 @Override 1153 public String argumentTypeIndicator() { return null; } 1154 @Override 1155 public boolean representsNonOptions() { return false; } 1156 }); 1157 String content = super.format(all); 1158 StringBuilder builder = new StringBuilder(); 1159 1160 builder.append(getMessage("main.opt.mode")).append("\n "); 1161 builder.append(getMessage("main.opt.mode.create")).append("\n "); 1162 builder.append(getMessage("main.opt.mode.list")).append("\n "); 1163 builder.append(getMessage("main.opt.mode.describe")).append("\n "); 1164 builder.append(getMessage("main.opt.mode.hash")).append("\n\n"); 1165 1166 String cmdfile = null; 1167 String[] lines = content.split("\n"); 1168 for (String line : lines) { 1169 if (line.startsWith("--@")) { 1170 cmdfile = line.replace("--" + CMD_FILENAME, CMD_FILENAME + " "); 1171 } else if (line.startsWith("Option") || line.startsWith("------")) { 1172 builder.append(" ").append(line).append("\n"); 1173 } else if (!line.matches("Non-option arguments")){ 1174 builder.append(" ").append(line).append("\n"); 1175 } 1176 } 1177 if (cmdfile != null) { 1178 builder.append(" ").append(cmdfile).append("\n"); 1179 } 1180 return builder.toString(); 1181 } 1182 } 1183 1184 private final OptionParser parser = new OptionParser("hp"); 1185 1186 private void handleOptions(String[] args) { 1187 parser.formatHelpWith(new JmodHelpFormatter()); 1188 1189 OptionSpec<Path> classPath 1190 = parser.accepts("class-path", getMessage("main.opt.class-path")) 1191 .withRequiredArg() 1192 .withValuesSeparatedBy(File.pathSeparatorChar) 1193 .withValuesConvertedBy(ClassPathConverter.INSTANCE); 1194 1195 OptionSpec<Path> cmds 1196 = parser.accepts("cmds", getMessage("main.opt.cmds")) 1197 .withRequiredArg() 1198 .withValuesSeparatedBy(File.pathSeparatorChar) 1199 .withValuesConvertedBy(DirPathConverter.INSTANCE); 1200 1201 OptionSpec<Path> config 1202 = parser.accepts("config", getMessage("main.opt.config")) 1203 .withRequiredArg() 1204 .withValuesSeparatedBy(File.pathSeparatorChar) 1205 .withValuesConvertedBy(DirPathConverter.INSTANCE); 1206 1207 OptionSpec<Void> dryrun 1208 = parser.accepts("dry-run", getMessage("main.opt.dry-run")); 1209 1210 OptionSpec<PathMatcher> excludes 1211 = parser.accepts("exclude", getMessage("main.opt.exclude")) 1212 .withRequiredArg() 1213 .withValuesConvertedBy(new PathMatcherConverter()); 1214 1215 OptionSpec<Pattern> hashModules 1216 = parser.accepts("hash-modules", getMessage("main.opt.hash-modules")) 1217 .withRequiredArg() 1218 .withValuesConvertedBy(new PatternConverter()); 1219 1220 OptionSpec<Void> help 1221 = parser.acceptsAll(Set.of("h", "help"), getMessage("main.opt.help")) 1222 .forHelp(); 1223 1224 OptionSpec<Path> libs 1225 = parser.accepts("libs", getMessage("main.opt.libs")) 1226 .withRequiredArg() 1227 .withValuesSeparatedBy(File.pathSeparatorChar) 1228 .withValuesConvertedBy(DirPathConverter.INSTANCE); 1229 1230 OptionSpec<String> mainClass 1231 = parser.accepts("main-class", getMessage("main.opt.main-class")) 1232 .withRequiredArg() 1233 .describedAs(getMessage("main.opt.main-class.arg")); 1234 1235 OptionSpec<Path> modulePath 1236 = parser.acceptsAll(Set.of("p", "module-path"), 1237 getMessage("main.opt.module-path")) 1238 .withRequiredArg() 1239 .withValuesSeparatedBy(File.pathSeparatorChar) 1240 .withValuesConvertedBy(DirPathConverter.INSTANCE); 1241 1242 OptionSpec<Version> moduleVersion 1243 = parser.accepts("module-version", getMessage("main.opt.module-version")) 1244 .withRequiredArg() 1245 .withValuesConvertedBy(new ModuleVersionConverter()); 1246 1247 OptionSpec<String> osName 1248 = parser.accepts("os-name", getMessage("main.opt.os-name")) 1249 .withRequiredArg() 1250 .describedAs(getMessage("main.opt.os-name.arg")); 1251 1252 OptionSpec<String> osArch 1253 = parser.accepts("os-arch", getMessage("main.opt.os-arch")) 1254 .withRequiredArg() 1255 .describedAs(getMessage("main.opt.os-arch.arg")); 1256 1257 OptionSpec<String> osVersion 1258 = parser.accepts("os-version", getMessage("main.opt.os-version")) 1259 .withRequiredArg() 1260 .describedAs(getMessage("main.opt.os-version.arg")); 1261 1262 OptionSpec<Void> version 1263 = parser.accepts("version", getMessage("main.opt.version")); 1264 1265 NonOptionArgumentSpec<String> nonOptions 1266 = parser.nonOptions(); 1267 1268 try { 1269 OptionSet opts = parser.parse(args); 1270 1271 if (opts.has(help) || opts.has(version)) { 1272 options = new Options(); 1273 options.help = opts.has(help); 1274 options.version = opts.has(version); 1275 return; // informational message will be shown 1276 } 1277 1278 List<String> words = opts.valuesOf(nonOptions); 1279 if (words.isEmpty()) 1280 throw new CommandException("err.missing.mode").showUsage(true); 1281 String verb = words.get(0); 1282 options = new Options(); 1283 try { 1284 options.mode = Enum.valueOf(Mode.class, verb.toUpperCase()); 1285 } catch (IllegalArgumentException e) { 1286 throw new CommandException("err.invalid.mode", verb).showUsage(true); 1287 } 1288 1289 if (opts.has(classPath)) 1290 options.classpath = opts.valuesOf(classPath); 1291 if (opts.has(cmds)) 1292 options.cmds = opts.valuesOf(cmds); 1293 if (opts.has(config)) 1294 options.configs = opts.valuesOf(config); 1295 if (opts.has(dryrun)) 1296 options.dryrun = true; 1297 if (opts.has(excludes)) 1298 options.excludes = opts.valuesOf(excludes); 1299 if (opts.has(libs)) 1300 options.libs = opts.valuesOf(libs); 1301 if (opts.has(modulePath)) { 1302 Path[] dirs = opts.valuesOf(modulePath).toArray(new Path[0]); 1303 options.moduleFinder = ModuleFinder.of(dirs); 1304 if (options.moduleFinder instanceof ConfigurableModuleFinder) 1305 ((ConfigurableModuleFinder)options.moduleFinder).configurePhase(Phase.LINK_TIME); 1306 } 1307 if (opts.has(moduleVersion)) 1308 options.moduleVersion = opts.valueOf(moduleVersion); 1309 if (opts.has(mainClass)) 1310 options.mainClass = opts.valueOf(mainClass); 1311 if (opts.has(osName)) 1312 options.osName = opts.valueOf(osName); 1313 if (opts.has(osArch)) 1314 options.osArch = opts.valueOf(osArch); 1315 if (opts.has(osVersion)) 1316 options.osVersion = opts.valueOf(osVersion); 1317 if (opts.has(hashModules)) { 1318 options.modulesToHash = opts.valueOf(hashModules); 1319 // if storing hashes then the module path is required 1320 if (options.moduleFinder == null) 1321 throw new CommandException("err.modulepath.must.be.specified") 1322 .showUsage(true); 1323 } 1324 1325 if (options.mode.equals(Mode.HASH)) { 1326 if (options.moduleFinder == null || options.modulesToHash == null) 1327 throw new CommandException("err.modulepath.must.be.specified") 1328 .showUsage(true); 1329 } else { 1330 if (words.size() <= 1) 1331 throw new CommandException("err.jmod.must.be.specified").showUsage(true); 1332 Path path = Paths.get(words.get(1)); 1333 1334 if (options.mode.equals(Mode.CREATE) && Files.exists(path)) 1335 throw new CommandException("err.file.already.exists", path); 1336 else if ((options.mode.equals(Mode.LIST) || 1337 options.mode.equals(Mode.DESCRIBE)) 1338 && Files.notExists(path)) 1339 throw new CommandException("err.jmod.not.found", path); 1340 1341 if (options.dryrun) { 1342 throw new CommandException("err.invalid.dryrun.option"); 1343 } 1344 options.jmodFile = path; 1345 1346 if (words.size() > 2) 1347 throw new CommandException("err.unknown.option", 1348 words.subList(2, words.size())).showUsage(true); 1349 } 1350 1351 if (options.mode.equals(Mode.CREATE) && options.classpath == null) 1352 throw new CommandException("err.classpath.must.be.specified").showUsage(true); 1353 if (options.mainClass != null && !isValidJavaIdentifier(options.mainClass)) 1354 throw new CommandException("err.invalid.main-class", options.mainClass); 1355 } catch (OptionException e) { 1356 throw new CommandException(e.getMessage()); 1357 } 1358 } 1359 1360 /** 1361 * Returns true if, and only if, the given main class is a legal. 1362 */ 1363 static boolean isValidJavaIdentifier(String mainClass) { 1364 if (mainClass.length() == 0) 1365 return false; 1366 1367 if (!Character.isJavaIdentifierStart(mainClass.charAt(0))) 1368 return false; 1369 1370 int n = mainClass.length(); 1371 for (int i=1; i < n; i++) { 1372 char c = mainClass.charAt(i); 1373 if (!Character.isJavaIdentifierPart(c) && c != '.') 1374 return false; 1375 } 1376 if (mainClass.charAt(n-1) == '.') 1377 return false; 1378 1379 return true; 1380 } 1381 1382 private void reportError(String message) { 1383 out.println(getMessage("error.prefix") + " " + message); 1384 } 1385 1386 private void warning(String key, Object... args) { 1387 out.println(getMessage("warn.prefix") + " " + getMessage(key, args)); 1388 } 1389 1390 private void showUsageSummary() { 1391 out.println(getMessage("main.usage.summary", PROGNAME)); 1392 } 1393 1394 private void showHelp() { 1395 out.println(getMessage("main.usage", PROGNAME)); 1396 try { 1397 parser.printHelpOn(out); 1398 } catch (IOException x) { 1399 throw new AssertionError(x); 1400 } 1401 } 1402 1403 private void showVersion() { 1404 out.println(version()); 1405 } 1406 1407 private String version() { 1408 return System.getProperty("java.version"); 1409 } 1410 1411 private static String getMessage(String key, Object... args) { 1412 try { 1413 return MessageFormat.format(ResourceBundleHelper.bundle.getString(key), args); 1414 } catch (MissingResourceException e) { 1415 throw new InternalError("Missing message: " + key); 1416 } 1417 } 1418 1419 private static class ResourceBundleHelper { 1420 static final ResourceBundle bundle; 1421 1422 static { 1423 Locale locale = Locale.getDefault(); 1424 try { 1425 bundle = ResourceBundle.getBundle("jdk.tools.jmod.resources.jmod", locale); 1426 } catch (MissingResourceException e) { 1427 throw new InternalError("Cannot find jmod resource bundle for locale " + locale); 1428 } 1429 } 1430 } 1431 }