1 /*
   2  * Copyright (c) 2012, 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 package com.sun.tools.jdeps;
  26 
  27 import com.sun.tools.classfile.ClassFile;
  28 import com.sun.tools.classfile.ConstantPoolException;
  29 import com.sun.tools.classfile.Dependencies;
  30 import com.sun.tools.classfile.Dependencies.ClassFileError;
  31 import com.sun.tools.classfile.Dependency;
  32 import com.sun.tools.classfile.Dependency.Finder;
  33 import com.sun.tools.classfile.Dependency.Location;
  34 import java.io.*;
  35 import java.nio.file.FileVisitResult;
  36 import java.nio.file.Files;
  37 import java.nio.file.Path;
  38 import java.nio.file.SimpleFileVisitor;
  39 import java.nio.file.attribute.BasicFileAttributes;
  40 import java.text.MessageFormat;
  41 import java.util.*;
  42 import java.util.regex.Pattern;
  43 
  44 /**
  45  * Implementation for the jdeps tool for static class dependency analysis.
  46  */
  47 class JdepsTask {
  48     class BadArgs extends Exception {
  49         static final long serialVersionUID = 8765093759964640721L;
  50         BadArgs(String key, Object... args) {
  51             super(JdepsTask.this.getMessage(key, args));
  52             this.key = key;
  53             this.args = args;
  54         }
  55 
  56         BadArgs showUsage(boolean b) {
  57             showUsage = b;
  58             return this;
  59         }
  60         final String key;
  61         final Object[] args;
  62         boolean showUsage;
  63     }
  64 
  65     static abstract class Option {
  66         Option(boolean hasArg, String... aliases) {
  67             this.hasArg = hasArg;
  68             this.aliases = aliases;
  69         }
  70 
  71         boolean isHidden() {
  72             return false;
  73         }
  74 
  75         boolean matches(String opt) {
  76             for (String a : aliases) {
  77                 if (a.equals(opt)) {
  78                     return true;
  79                 }
  80             }
  81             return false;
  82         }
  83 
  84         boolean ignoreRest() {
  85             return false;
  86         }
  87 
  88         abstract void process(JdepsTask task, String opt, String arg) throws BadArgs;
  89         final boolean hasArg;
  90         final String[] aliases;
  91     }
  92 
  93     static Option[] recognizedOptions = {
  94         new Option(false, "-h", "--help") {
  95             void process(JdepsTask task, String opt, String arg) {
  96                 task.options.help = true;
  97             }
  98         },
  99         new Option(false, "-version") {
 100             void process(JdepsTask task, String opt, String arg) {
 101                 task.options.version = true;
 102             }
 103         },
 104         new Option(false, "-fullversion") {
 105             void process(JdepsTask task, String opt, String arg) {
 106                 task.options.fullVersion = true;
 107             }
 108         },
 109         new Option(true, "-classpath") {
 110             void process(JdepsTask task, String opt, String arg) {
 111                 task.options.classpath = arg;
 112             }
 113         },
 114         new Option(false, "-v", "--verbose") {
 115             void process(JdepsTask task, String opt, String arg) {
 116                 task.options.verbose = true;
 117             }
 118         },
 119         new Option(false, "-r", "--reverse") {
 120             void process(JdepsTask task, String opt, String arg) {
 121                 task.options.reverse = true;
 122             }
 123         },
 124         new Option(true, "-p") {
 125             void process(JdepsTask task, String opt, String arg) {
 126                 task.options.packageNames.add(arg);
 127             }
 128         },
 129         new Option(true, "-e", "--regex") {
 130             void process(JdepsTask task, String opt, String arg) {
 131                 task.options.regex = arg;
 132             }
 133         },
 134         new Option(false, "-P", "--profile") {
 135             void process(JdepsTask task, String opt, String arg) {
 136                 task.options.showProfile = true;
 137             }
 138         },
 139         new Option(true, "-d", "--depth") {
 140             void process(JdepsTask task, String opt, String arg) throws BadArgs {
 141                 try {
 142                     task.options.depth = Integer.parseInt(arg);
 143                 } catch (NumberFormatException e) {
 144                     throw task.new BadArgs("err.invalid.arg.for.option", opt);
 145                 }
 146             }
 147         },
 148         new Option(false, "-all") {
 149             void process(JdepsTask task, String opt, String arg) {
 150                 task.options.all = true;
 151             }
 152         },};
 153     private static final String PROGNAME = "jdeps";
 154     private final Options options = new Options();
 155     private final List<String> filenames = new ArrayList<>();
 156 
 157     private PrintWriter log;
 158     void setLog(PrintWriter out) {
 159         log = out;
 160     }
 161     /**
 162      * Result codes.
 163      */
 164     static final int EXIT_OK = 0, // Completed with no errors.
 165             EXIT_ERROR = 1, // Completed but reported errors.
 166             EXIT_CMDERR = 2, // Bad command-line arguments
 167             EXIT_SYSERR = 3, // System error or resource exhaustion.
 168             EXIT_ABNORMAL = 4;// terminated abnormally
 169 
 170     int run(String[] args) {
 171         if (log == null) {
 172             log = new PrintWriter(System.out);
 173         }
 174         try {
 175             handleOptions(Arrays.asList(args));
 176             if (filenames.isEmpty()) {
 177                 if (options.help || options.version || options.fullVersion) {
 178                     return EXIT_OK;
 179                 } else {
 180                     return EXIT_CMDERR;
 181                 }
 182             }
 183             if (options.regex != null && options.packageNames.size() > 0) {
 184                 showHelp();
 185                 return EXIT_CMDERR;
 186             }
 187             boolean ok = run();
 188             return ok ? EXIT_OK : EXIT_ERROR;
 189         } catch (BadArgs e) {
 190             reportError(e.key, e.args);
 191             if (e.showUsage) {
 192                 log.println(getMessage("main.usage.summary", PROGNAME));
 193             }
 194             return EXIT_CMDERR;
 195         } catch (IOException e) {
 196             return EXIT_ABNORMAL;
 197         } finally {
 198             log.flush();
 199         }
 200     }
 201 
 202     private boolean run() throws IOException {
 203         Dependency.Filter filter;
 204         if (options.regex != null) {
 205             filter = Dependencies.getRegexFilter(Pattern.compile(options.regex));
 206         } else if (options.packageNames.size() > 0) {
 207             filter = Dependencies.getPackageFilter(options.packageNames, false);
 208         } else {
 209             filter = new Dependency.Filter() {
 210                 public boolean accepts(Dependency dependency) {
 211                     return !dependency.getOrigin().equals(dependency.getTarget());
 212                 }
 213             };
 214         }
 215 
 216         Finder finder = Dependencies.getClassDependencyFinder();
 217         DependencyRecorder recorder = new DependencyRecorder(options.reverse);
 218         findDependencies(finder, filter, recorder);
 219 
 220         if (options.verbose) {
 221             printClassDeps(log, recorder);
 222         } else {
 223             printPackageDeps(log, recorder);
 224         }
 225         return true;
 226     }
 227 
 228     private Map<String, ClassFileReader> classToReaderMap = new HashMap<>();
 229     private void findDependencies(Dependency.Finder finder,
 230                                   Dependency.Filter filter,
 231                                   DependencyRecorder recorder)
 232         throws IOException
 233     {
 234         // Work queue of names of classfiles to be searched.
 235         // Entries will be unique, and for classes that do not yet have
 236         // dependencies in the results map.
 237         Deque<String> deque = new LinkedList<>();
 238 
 239         // get the immediate dependencies of the input files
 240         for (ClassFileReader r :  getClassFileReaders(filenames)) {
 241             for (ClassFile cf : r.getClassFiles()) {
 242                 String className;
 243                 try {
 244                     className = cf.getName().replace('/', '.');
 245                 } catch (ConstantPoolException e) {
 246                     throw new ClassFileError(e);
 247                 }
 248                 if (!classToReaderMap.containsKey(className)) {
 249                     classToReaderMap.put(className, r);
 250                 }
 251                 for (Dependency d : finder.findDependencies(cf)) {
 252                     if (filter.accepts(d)) {
 253                         String cn = d.getTarget().getClassName();
 254                         if (!classToReaderMap.containsKey(cn) && !deque.contains(cn)) {
 255                             deque.add(cn);
 256                         }
 257                         recorder.addDependency(d);
 258                     }
 259                 }
 260             }
 261         }
 262 
 263         // add ClassFileReader for looking up classes from the classpath
 264         // for transitive dependency analysis
 265         int depths = options.depth == 0 ? Integer.MAX_VALUE : options.depth;
 266         initJavaHomeReaders();
 267         List<ClassFileReader> readers = new ArrayList<>(classPathReaders(options.classpath));
 268         readers.addAll(javaHomeReaders);
 269         while (!deque.isEmpty() && depths-- >= 0) {
 270             Deque<String> unresolved = deque;
 271             deque = new LinkedList<>();
 272             String className;
 273             while ((className = unresolved.poll()) != null) {
 274                 if (classToReaderMap.containsKey(className)) {
 275                     continue;
 276                 }
 277 
 278                 ClassFile cf = null;
 279                 for (ClassFileReader r : readers) {
 280                     cf = r.getClassFile(className);
 281                     if (cf != null) {
 282                         classToReaderMap.put(className, r);
 283                         if (depths > 0) {
 284                             // do dependency analysis if depths > 0
 285                             for (Dependency d : finder.findDependencies(cf)) {
 286                                 if (filter.accepts(d)) {
 287                                     String cn = d.getTarget().getClassName();
 288                                     if (!classToReaderMap.containsKey(cn) && !deque.contains(cn)) {
 289                                         deque.add(cn);
 290                                     }
 291                                     recorder.addDependency(d);
 292                                 }
 293                             }
 294                         }
 295                         break;
 296                     }
 297                 }
 298 
 299                 if (cf == null) {
 300                     warning("warn.class.not.found", className);
 301                     classToReaderMap.put(className, null);
 302                 }
 303             }
 304         }
 305     }
 306 
 307     private void printPackageDeps(PrintWriter out, DependencyRecorder r) {
 308         if (options.all) {
 309             SortedMap<String, ClassFileReader> pkgs = new TreeMap<>();
 310             for (Location loc : r.getAllClasses()) {
 311                 String pn = packageOf(loc);
 312                 ClassFileReader reader = classToReaderMap.get(loc.getClassName());
 313                 if (!pkgs.containsKey(pn)) {
 314                     pkgs.put(pn, reader);
 315                 } else if (pkgs.get(pn) != reader) {
 316                     warning("warn.split.package", pn, reader, pkgs.get(pn));
 317                 }
 318             }
 319             for (Map.Entry<String, ClassFileReader> e : pkgs.entrySet()) {
 320                 String pn = e.getKey();
 321                 ClassFileReader reader = e.getValue();
 322                 out.format("    %-60s %s%n", pn, getPackageInfo(pn, reader));
 323             }
 324         } else {
 325             String pkg = "";
 326             SortedMap<Location, SortedSet<Location>> deps = getDependencies(r);
 327             SortedMap<String, ClassFileReader> targets = new TreeMap<>();
 328             for (Map.Entry<Location, SortedSet<Location>> e : getDependencies(r).entrySet()) {
 329                 Location o = e.getKey();
 330                 String p = packageOf(o);
 331                 if (!p.equals(pkg)) {
 332                     printTargets(out, targets);
 333                     pkg = p;
 334                     targets.clear();
 335                     out.format("%s (%s)%n", p, classToReaderMap.get(o.getClassName()));
 336                 }
 337 
 338                 for (Location t : e.getValue()) {
 339                     p = packageOf(t);
 340                     ClassFileReader reader = classToReaderMap.get(t.getClassName());
 341                     if (!targets.containsKey(p)) {
 342                         targets.put(p, reader);
 343                     }
 344                 }
 345             }
 346             printTargets(out, targets);
 347         }
 348     }
 349 
 350     private void printTargets(PrintWriter out, Map<String, ClassFileReader> targets) {
 351         String arrow = options.reverse ? "<-" : "->";
 352         for (Map.Entry<String, ClassFileReader> t : targets.entrySet()) {
 353             String pn = t.getKey();
 354             out.format("    %s %-40s %s%n", arrow, pn, getPackageInfo(pn, t.getValue()));
 355         }
 356     }
 357 
 358     private String getPackageInfo(String pn, ClassFileReader reader) {
 359         Profile profile = Profile.getProfile(pn);
 360         if (profile == null && javaHomeReaders.contains(reader)) {
 361             return "JDK internal API";
 362         }
 363         if (options.showProfile) {
 364             String s = reader != null ? reader.toString() : null;
 365             return profile != null ? profile.toString() : s;
 366         } else {
 367             // default no package information
 368             return "";
 369         }
 370     }
 371 
 372     private static String packageOf(Location loc) {
 373         String cn = loc.getClassName();
 374         int i = cn.lastIndexOf('.');
 375         return i > 0 ? cn.substring(0, i) : "<unnamed>";
 376     }
 377 
 378     private void printClassDeps(PrintWriter out, DependencyRecorder r) {
 379         if (options.all) {
 380             SortedSet<Location> classes = r.getAllClasses();
 381             for (Location t : classes) {
 382                 out.format("    %s%n", t.getClassName());
 383             }
 384         } else {
 385             String arrow = options.reverse ? "<-" : "->";
 386             SortedMap<Location, SortedSet<Location>> deps = getDependencies(r);
 387             for (Map.Entry<Location, SortedSet<Location>> e : deps.entrySet()) {
 388                 Location o = e.getKey();
 389                 String cn = o.getClassName();
 390                 ClassFileReader reader = classToReaderMap.get(cn);
 391                 out.format("%s (%s)%n", cn, reader);
 392                 for (Location t : e.getValue()) {
 393                     cn = t.getClassName();
 394                     reader = classToReaderMap.get(cn);
 395                     out.format("    %s %-60s %s%n", arrow, cn, getPackageInfo(packageOf(t), reader));
 396                 }
 397             }
 398         }
 399     }
 400 
 401     private SortedMap<Location, SortedSet<Location>> getDependencies(DependencyRecorder r) {
 402         return r.getDependencies(new DependencyRecorder.Filter() {
 403             @Override
 404             public boolean accept(Location origin, Location target) {
 405                  String o = origin.getClassName();
 406                  String t = target.getClassName();
 407                  return (options.all || classToReaderMap.get(o) != classToReaderMap.get(t));
 408         }});
 409     }
 410 
 411     public void handleOptions(Iterable<String> args) throws BadArgs {
 412         Iterator<String> iter = args.iterator();
 413         boolean noArgs = !iter.hasNext();
 414         while (iter.hasNext()) {
 415             String arg = iter.next();
 416             if (arg.startsWith("-")) {
 417                 handleOption(arg, iter);
 418             } else {
 419                 filenames.add(arg);
 420                 while (iter.hasNext()) {
 421                     filenames.add(iter.next());
 422                 }
 423             }
 424         }
 425 
 426         if (noArgs || filenames.isEmpty() || options.help) {
 427             showHelp();
 428         }
 429 
 430         if (options.version || options.fullVersion) {
 431             showVersion(options.fullVersion);
 432         }
 433     }
 434 
 435     private void handleOption(String name, Iterator<String> rest) throws BadArgs {
 436         for (Option o : recognizedOptions) {
 437             if (o.matches(name)) {
 438                 if (o.hasArg) {
 439                     String arg = rest.hasNext() ? rest.next() : "-";
 440                     if (arg.startsWith("-")) {
 441                         throw new BadArgs("err.missing.arg", name).showUsage(true);
 442                     } else {
 443                         o.process(this, name, arg);
 444                     }
 445                 } else {
 446                     o.process(this, name, null);
 447                 }
 448 
 449                 if (o.ignoreRest()) {
 450                     while (rest.hasNext()) {
 451                         rest.next();
 452                     }
 453                 }
 454                 return;
 455             }
 456         }
 457         throw new BadArgs("err.unknown.option", name).showUsage(true);
 458     }
 459 
 460     private void reportError(String key, Object... args) {
 461         log.println(getMessage("error.prefix") + " " + getMessage(key, args));
 462     }
 463 
 464     private void warning(String key, Object... args) {
 465         log.println(getMessage("warn.prefix") + " " + getMessage(key, args));
 466     }
 467 
 468     private void showHelp() {
 469         log.println(getMessage("main.usage", PROGNAME));
 470         for (Option o : recognizedOptions) {
 471             String name = o.aliases[0].substring(1); // there must always be at least one name
 472             if (name.equals("fullversion") || name.equals("h")) {
 473                 continue;
 474             }
 475             log.println(getMessage("main.opt." + name));
 476         }
 477     }
 478 
 479     private void showVersion(boolean full) {
 480         log.println(version(full ? "full" : "release"));
 481     }
 482 
 483     private String version(String key) {
 484         // key=version:  mm.nn.oo[-milestone]
 485         // key=full:     mm.mm.oo[-milestone]-build
 486         if (ResourceBundleHelper.versionRB == null) {
 487             return System.getProperty("java.version");
 488         }
 489         try {
 490             return ResourceBundleHelper.versionRB.getString(key);
 491         } catch (MissingResourceException e) {
 492             return getMessage("version.unknown", System.getProperty("java.version"));
 493         }
 494     }
 495 
 496     public String getMessage(String key, Object... args) {
 497         try {
 498             return MessageFormat.format(ResourceBundleHelper.bundle.getString(key), args);
 499         } catch (MissingResourceException e) {
 500             throw new InternalError("Missing message: " + key);
 501         }
 502     }
 503 
 504     private static class Options {
 505         public boolean help;
 506         public boolean verbose;
 507         public boolean version;
 508         public boolean fullVersion;
 509         public boolean showFlags;
 510         public boolean reverse;
 511         public boolean all;
 512         public boolean showProfile;
 513         public String regex;
 514         public String classpath;
 515         public int depth = 1;
 516         public Set<String> packageNames = new HashSet<>();
 517     }
 518 
 519     private static class ResourceBundleHelper {
 520         static final ResourceBundle versionRB;
 521         static final ResourceBundle bundle;
 522 
 523         static {
 524             Locale locale = Locale.getDefault();
 525             try {
 526                 bundle = ResourceBundle.getBundle("com.sun.tools.jdeps.resources.jdeps", locale);
 527             } catch (MissingResourceException e) {
 528                 throw new InternalError("Cannot find jdeps resource bundle for locale " + locale);
 529             }
 530             try {
 531                 versionRB = ResourceBundle.getBundle("com.sun.tools.jdeps.resources.version");
 532             } catch (MissingResourceException e) {
 533                 throw new InternalError("version.resource.missing");
 534             }
 535         }
 536     }
 537 
 538     static class DependencyRecorder {
 539         static interface Filter {
 540             boolean accept(Location origin, Location target);
 541         }
 542         public DependencyRecorder(boolean reverse) {
 543             this.reverse = reverse;
 544         }
 545 
 546         public void addDependency(Dependency d) {
 547             Location origin = reverse ? d.getTarget() : d.getOrigin();
 548             Location target = reverse ? d.getOrigin() : d.getTarget();
 549             SortedSet<Location> odeps = map.get(origin);
 550             if (odeps == null) {
 551                 map.put(origin, odeps = new TreeSet<>(locationComparator));
 552             }
 553             odeps.add(target);
 554         }
 555 
 556         public SortedMap<Location, SortedSet<Location>> getClassMap() {
 557             return map;
 558         }
 559 
 560         public SortedSet<Location> getAllClasses() {
 561             SortedSet<Location> classes = new TreeSet<>(locationComparator);
 562             for (SortedSet<Location> set : map.values()) {
 563                 classes.addAll(set);
 564             }
 565             return classes;
 566         }
 567 
 568         public SortedMap<Location, SortedSet<Location>> getDependencies(Filter filter) {
 569             SortedMap<Location, SortedSet<Location>> result = new TreeMap<>(locationComparator);
 570             for (Map.Entry<Location, SortedSet<Location>> e : map.entrySet()) {
 571                 Location o = e.getKey();
 572                 for (Location t : e.getValue()) {
 573                     if (filter.accept(o, t)) {
 574                         SortedSet<Location> odeps = result.get(o);
 575                         if (odeps == null) {
 576                             result.put(o, odeps = new TreeSet<>(locationComparator));
 577                         }
 578                         odeps.add(t);
 579                     }
 580                 }
 581             }
 582             return result;
 583         }
 584 
 585         private Comparator<Location> locationComparator =
 586                 new Comparator<Location>() {
 587                     public int compare(Location o1, Location o2) {
 588                         return o1.toString().compareTo(o2.toString());
 589                     }
 590                 };
 591         private final SortedMap<Location, SortedSet<Location>> map =
 592                 new TreeMap<>(locationComparator);
 593         private final boolean reverse;
 594     }
 595 
 596     private List<ClassFileReader> getClassFileReaders(List<String> filenames) throws IOException {
 597         List<ClassFileReader> readers = new ArrayList<>();
 598         for (String s : filenames) {
 599             File f = new File(s);
 600             if (f.exists()) {
 601                 readers.add(ClassFileReader.newInstance(f));
 602             } else {
 603                 System.err.println("WARNING: " + s + " not exist");
 604             }
 605         }
 606         return readers;
 607     }
 608 
 609     private List<ClassFileReader> classPathReaders(String classpath) throws IOException {
 610         List<ClassFileReader> result = new ArrayList<>();
 611         if (classpath == null || classpath.isEmpty()) {
 612             return result;
 613         }
 614         for (String p : classpath.split(File.pathSeparator)) {
 615             if (p.length() > 0) {
 616                 File f = new File(p);
 617                 if (f.exists()) {
 618                     result.add(ClassFileReader.newInstance(f));
 619                 }
 620             }
 621         }
 622         return result;
 623     }
 624 
 625     private final List<ClassFileReader> javaHomeReaders = new ArrayList<>();
 626     private void initJavaHomeReaders() throws IOException {
 627         String javaHome = System.getProperty("java.home");
 628         List<File> files = new ArrayList<>();
 629         File jre = new File(javaHome, "jre");
 630         File lib = new File(javaHome, "lib");
 631 
 632         if (jre.exists() && jre.isDirectory()) {
 633             javaHomeReaders.addAll(addJarFiles(new File(jre, "lib")));
 634             javaHomeReaders.addAll(addJarFiles(lib));
 635         } else if (lib.exists() && lib.isDirectory()) {
 636             // either a JRE or a jdk build image
 637             File classes = new File(javaHome, "classes");
 638             if (classes.exists() && classes.isDirectory()) {
 639                 // jdk build outputdir
 640                 javaHomeReaders.add(ClassFileReader.newInstance(classes));
 641             }
 642             // add other JAR files
 643             javaHomeReaders.addAll(addJarFiles(lib));
 644         } else {
 645             throw new RuntimeException("\"" + javaHome + "\" not a JDK home");
 646         }
 647     }
 648 
 649     private List<ClassFileReader> addJarFiles(File f) throws IOException {
 650         final List<ClassFileReader> result = new ArrayList<>();
 651         Files.walkFileTree(f.toPath(), new SimpleFileVisitor<Path>() {
 652             @Override
 653             public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
 654                     throws IOException {
 655                 String fn = file.toFile().getName();
 656                 if (fn.endsWith(".jar") && !fn.equals("alt-rt.jar")) {
 657                     result.add(ClassFileReader.newInstance(file.toFile()));
 658                 }
 659                 return FileVisitResult.CONTINUE;
 660             }
 661 
 662             @Override
 663             public FileVisitResult postVisitDirectory(Path dir, IOException e)
 664                     throws IOException {
 665                 if (e == null) {
 666                     return FileVisitResult.CONTINUE;
 667                 } else {
 668                     // directory iteration failed
 669                     throw e;
 670                 }
 671             }
 672         });
 673         return result;
 674     }
 675 }