1 /*
   2  * Copyright (c) 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 com.sun.tools.javac.launcher;
  27 
  28 import java.io.BufferedInputStream;
  29 import java.io.BufferedReader;
  30 import java.io.ByteArrayInputStream;
  31 import java.io.ByteArrayOutputStream;
  32 import java.io.FileNotFoundException;
  33 import java.io.IOException;
  34 import java.io.InputStream;
  35 import java.io.InputStreamReader;
  36 import java.io.OutputStream;
  37 import java.io.OutputStreamWriter;
  38 import java.io.PrintStream;
  39 import java.io.PrintWriter;
  40 import java.lang.reflect.InvocationTargetException;
  41 import java.lang.reflect.Method;
  42 import java.lang.reflect.Modifier;
  43 import java.net.MalformedURLException;
  44 import java.net.URI;
  45 import java.net.URL;
  46 import java.net.URLConnection;
  47 import java.net.URLStreamHandler;
  48 import java.nio.charset.Charset;
  49 import java.nio.file.Files;
  50 import java.nio.file.InvalidPathException;
  51 import java.nio.file.Path;
  52 import java.nio.file.Paths;
  53 import java.security.CodeSigner;
  54 import java.security.CodeSource;
  55 import java.security.ProtectionDomain;
  56 import java.text.MessageFormat;
  57 import java.util.ArrayList;
  58 import java.util.Arrays;
  59 import java.util.Collections;
  60 import java.util.Enumeration;
  61 import java.util.HashMap;
  62 import java.util.List;
  63 import java.util.Map;
  64 import java.util.MissingResourceException;
  65 import java.util.NoSuchElementException;
  66 import java.util.ResourceBundle;
  67 import java.util.stream.Collectors;
  68 import java.util.stream.Stream;
  69 
  70 import javax.lang.model.SourceVersion;
  71 import javax.lang.model.element.NestingKind;
  72 import javax.lang.model.element.TypeElement;
  73 import javax.tools.FileObject;
  74 import javax.tools.ForwardingJavaFileManager;
  75 import javax.tools.JavaFileManager;
  76 import javax.tools.JavaFileObject;
  77 import javax.tools.SimpleJavaFileObject;
  78 import javax.tools.StandardJavaFileManager;
  79 import javax.tools.StandardLocation;
  80 
  81 import com.sun.source.util.JavacTask;
  82 import com.sun.source.util.TaskEvent;
  83 import com.sun.source.util.TaskListener;
  84 import com.sun.tools.javac.api.JavacTool;
  85 import com.sun.tools.javac.code.Source;
  86 import com.sun.tools.javac.resources.LauncherProperties.Errors;
  87 import com.sun.tools.javac.util.JCDiagnostic.Error;
  88 
  89 import jdk.internal.misc.VM;
  90 
  91 import static javax.tools.JavaFileObject.Kind.SOURCE;
  92 
  93 /**
  94  * Compiles a source file, and executes the main method it contains.
  95  *
  96  * <p><b>This is NOT part of any supported API.
  97  * If you write code that depends on this, you do so at your own
  98  * risk.  This code and its internal interfaces are subject to change
  99  * or deletion without notice.</b></p>
 100  */
 101 public class Main {
 102     /**
 103      * An exception used to report errors.
 104      */
 105     public class Fault extends Exception {
 106         private static final long serialVersionUID = 1L;
 107         Fault(Error error) {
 108             super(Main.this.getMessage(error));
 109         }
 110     }
 111 
 112     /**
 113      * Compiles a source file, and executes the main method it contains.
 114      *
 115      * <p>This is normally invoked from the Java launcher, either when
 116      * the {@code --source} option is used, or when the first argument
 117      * that is not part of a runtime option ends in {@code .java}.
 118      *
 119      * <p>The first entry in the {@code args} array is the source file
 120      * to be compiled and run; all subsequent entries are passed as
 121      * arguments to the main method of the first class found in the file.
 122      *
 123      * <p>If any problem occurs before executing the main class, it will
 124      * be reported to the standard error stream, and the the JVM will be
 125      * terminated by calling {@code System.exit} with a non-zero return code.
 126      *
 127      * @param args the arguments
 128      * @throws Throwable if the main method throws an exception
 129      */
 130     public static void main(String... args) throws Throwable {
 131         try {
 132             new Main(System.err).run(VM.getRuntimeArguments(), args);
 133         } catch (Fault f) {
 134             System.err.println(f.getMessage());
 135             System.exit(1);
 136         } catch (InvocationTargetException e) {
 137             // leave VM to handle the stacktrace, in the standard manner
 138             throw e.getTargetException();
 139         }
 140     }
 141 
 142     /** Stream for reporting errors, such as compilation errors. */
 143     private PrintWriter out;
 144 
 145     /**
 146      * Creates an instance of this class, providing a stream to which to report
 147      * any errors.
 148      *
 149      * @param out the stream
 150      */
 151     public Main(PrintStream out) {
 152         this(new PrintWriter(new OutputStreamWriter(out), true));
 153     }
 154 
 155     /**
 156      * Creates an instance of this class, providing a stream to which to report
 157      * any errors.
 158      *
 159      * @param out the stream
 160      */
 161     public Main(PrintWriter out) {
 162         this.out = out;
 163     }
 164 
 165     /**
 166      * Compiles a source file, and executes the main method it contains.
 167      *
 168      * <p>The first entry in the {@code args} array is the source file
 169      * to be compiled and run; all subsequent entries are passed as
 170      * arguments to the main method of the first class found in the file.
 171      *
 172      * <p>Options for {@code javac} are obtained by filtering the runtime arguments.
 173      *
 174      * <p>If the main method throws an exception, it will be propagated in an
 175      * {@code InvocationTargetException}. In that case, the stack trace of the
 176      * target exception will be truncated such that the main method will be the
 177      * last entry on the stack. In other words, the stack frames leading up to the
 178      * invocation of the main method will be removed.
 179      *
 180      * @param runtimeArgs the runtime arguments
 181      * @param args the arguments
 182      * @throws Fault if a problem is detected before the main method can be executed
 183      * @throws InvocationTargetException if the main method throws an exception
 184      */
 185     public void run(String[] runtimeArgs, String[] args) throws Fault, InvocationTargetException {
 186         Path file = getFile(args);
 187 
 188         Context context = new Context(file.toAbsolutePath());
 189         String mainClassName = compile(file, getJavacOpts(runtimeArgs), context);
 190 
 191         String[] appArgs = Arrays.copyOfRange(args, 1, args.length);
 192         execute(mainClassName, appArgs, context);
 193     }
 194 
 195     /**
 196      * Returns the path for the filename found in the first of an array of arguments.
 197      *
 198      * @param args the array
 199      * @return the path, as given in the array of args
 200      * @throws Fault if there is a problem determining the path, or if the file does not exist
 201      */
 202     private Path getFile(String[] args) throws Fault {
 203         if (args.length == 0) {
 204             // should not happen when invoked from launcher
 205             throw new Fault(Errors.NoArgs);
 206         }
 207         Path file;
 208         try {
 209             file = Paths.get(args[0]);
 210         } catch (InvalidPathException e) {
 211             throw new Fault(Errors.InvalidFilename(args[0]));
 212         }
 213         if (!Files.exists(file)) {
 214             // should not happen when invoked from launcher
 215             throw new Fault(Errors.FileNotFound(file));
 216         }
 217         return file;
 218     }
 219 
 220     /**
 221      * Reads a source file, ignoring the first line if it is not a Java source file and
 222      * it begins with {@code #!}.
 223      *
 224      * <p>If it is not a Java source file, and if the first two bytes are {@code #!},
 225      * indicating a "magic number" of an executable text file, the rest of the first line
 226      * up to but not including the newline is ignored. All characters after the first two are
 227      * read in the {@link Charset#defaultCharset default platform encoding}.
 228      *
 229      * @param file the file
 230      * @return a file object containing the content of the file
 231      * @throws Fault if an error occurs while reading the file
 232      */
 233     private JavaFileObject readFile(Path file) throws Fault {
 234         // use a BufferedInputStream to guarantee that we can use mark and reset.
 235         try (BufferedInputStream in = new BufferedInputStream(Files.newInputStream(file))) {
 236             boolean ignoreFirstLine;
 237             if (file.getFileName().toString().endsWith(".java")) {
 238                 ignoreFirstLine = false;
 239             } else {
 240                 in.mark(2);
 241                 ignoreFirstLine = (in.read() == '#') && (in.read() == '!');
 242                 if (!ignoreFirstLine) {
 243                     in.reset();
 244                 }
 245             }
 246             try (BufferedReader r = new BufferedReader(new InputStreamReader(in, Charset.defaultCharset()))) {
 247                 StringBuilder sb = new StringBuilder();
 248                 if (ignoreFirstLine) {
 249                     r.readLine();
 250                     sb.append("\n"); // preserve line numbers
 251                 }
 252                 char[] buf = new char[1024];
 253                 int n;
 254                 while ((n = r.read(buf, 0, buf.length)) != -1)  {
 255                     sb.append(buf, 0, n);
 256                 }
 257                 return new SimpleJavaFileObject(file.toUri(), SOURCE) {
 258                     @Override
 259                     public String getName() {
 260                         return file.toString();
 261                     }
 262                     @Override
 263                     public CharSequence getCharContent(boolean ignoreEncodingErrors) {
 264                         return sb;
 265                     }
 266                     @Override
 267                     public boolean isNameCompatible(String simpleName, JavaFileObject.Kind kind) {
 268                         // reject package-info and module-info; accept other names
 269                         return (kind == JavaFileObject.Kind.SOURCE)
 270                                 && SourceVersion.isIdentifier(simpleName);
 271                     }
 272                     @Override
 273                     public String toString() {
 274                         return "JavacSourceLauncher[" + file + "]";
 275                     }
 276                 };
 277             }
 278         } catch (IOException e) {
 279             throw new Fault(Errors.CantReadFile(file, e));
 280         }
 281     }
 282 
 283     /**
 284      * Returns the subset of the runtime arguments that are relevant to {@code javac}.
 285      * Generally, the relevant options are those for setting paths and for configuring the
 286      * module system.
 287      *
 288      * @param runtimeArgs the runtime arguments
 289      * @return the subset of the runtime arguments
 290      **/
 291     private List<String> getJavacOpts(String... runtimeArgs) throws Fault {
 292         List<String> javacOpts = new ArrayList<>();
 293 
 294         String sourceOpt = System.getProperty("jdk.internal.javac.source");
 295         if (sourceOpt != null) {
 296             Source source = Source.lookup(sourceOpt);
 297             if (source == null) {
 298                 throw new Fault(Errors.InvalidValueForSource(sourceOpt));
 299             }
 300             javacOpts.addAll(List.of("--release", sourceOpt));
 301         }
 302 
 303         for (int i = 0; i < runtimeArgs.length; i++) {
 304             String arg = runtimeArgs[i];
 305             String opt = arg, value = null;
 306             if (arg.startsWith("--")) {
 307                 int eq = arg.indexOf('=');
 308                 if (eq > 0) {
 309                     opt = arg.substring(0, eq);
 310                     value = arg.substring(eq + 1);
 311                 }
 312             }
 313             switch (opt) {
 314                 // The following options all expect a value, either in the following
 315                 // position, or after '=', for options beginning "--".
 316                 case "--class-path": case "-classpath": case "-cp":
 317                 case "--module-path": case "-p":
 318                 case "--add-exports":
 319                 case "--add-modules":
 320                 case "--limit-modules":
 321                 case "--patch-module":
 322                 case "--upgrade-module-path":
 323                     if (value == null) {
 324                         if (i== runtimeArgs.length - 1) {
 325                             // should not happen when invoked from launcher
 326                             throw new Fault(Errors.NoValueForOption(opt));
 327                         }
 328                         value = runtimeArgs[++i];
 329                     }
 330                     if (opt.equals("--add-modules") && value.equals("ALL-DEFAULT")) {
 331                         // this option is only supported at run time;
 332                         // it is not required or supported at compile time
 333                         break;
 334                     }
 335                     javacOpts.add(opt);
 336                     javacOpts.add(value);
 337                     break;
 338                 case "--enable-preview":
 339                     javacOpts.add(opt);
 340                     if (sourceOpt == null) {
 341                         throw new Fault(Errors.EnablePreviewRequiresSource);
 342                     }
 343                     break;
 344                 default:
 345                     // ignore all other runtime args
 346             }
 347         }
 348 
 349         // add implicit options
 350         javacOpts.add("-proc:none");
 351 
 352         return javacOpts;
 353     }
 354 
 355     /**
 356      * Compiles a source file, placing the class files in a map in memory.
 357      * Any messages generated during compilation will be written to the stream
 358      * provided when this object was created.
 359      *
 360      * @param file the source file
 361      * @param javacOpts compilation options for {@code javac}
 362      * @param context the context for the compilation
 363      * @return the name of the first class found in the source file
 364      * @throws Fault if any compilation errors occur, or if no class was found
 365      */
 366     private String compile(Path file, List<String> javacOpts, Context context) throws Fault {
 367         JavaFileObject fo = readFile(file);
 368 
 369         JavacTool javaCompiler = JavacTool.create();
 370         StandardJavaFileManager stdFileMgr = javaCompiler.getStandardFileManager(null, null, null);
 371         try {
 372             stdFileMgr.setLocation(StandardLocation.SOURCE_PATH, Collections.emptyList());
 373         } catch (IOException e) {
 374             throw new java.lang.Error("unexpected exception from file manager", e);
 375         }
 376         JavaFileManager fm = context.getFileManager(stdFileMgr);
 377         JavacTask t = javaCompiler.getTask(out, fm, null, javacOpts, null, List.of(fo));
 378         MainClassListener l = new MainClassListener(t);
 379         Boolean ok = t.call();
 380         if (!ok) {
 381             throw new Fault(Errors.CompilationFailed);
 382         }
 383         if (l.mainClass == null) {
 384             throw new Fault(Errors.NoClass);
 385         }
 386         String mainClassName = l.mainClass.getQualifiedName().toString();
 387         return mainClassName;
 388     }
 389 
 390     /**
 391      * Invokes the {@code main} method of a specified class, using a class loader that
 392      * will load recently compiled classes from memory.
 393      *
 394      * @param mainClassName the class to be executed
 395      * @param appArgs the arguments for the {@code main} method
 396      * @param context the context for the class to be executed
 397      * @throws Fault if there is a problem finding or invoking the {@code main} method
 398      * @throws InvocationTargetException if the {@code main} method throws an exception
 399      */
 400     private void execute(String mainClassName, String[] appArgs, Context context)
 401             throws Fault, InvocationTargetException {
 402         System.setProperty("jdk.launcher.sourcefile", context.file.toString());
 403         ClassLoader cl = context.getClassLoader(ClassLoader.getSystemClassLoader());
 404         try {
 405             Class<?> appClass = Class.forName(mainClassName, true, cl);
 406             Method main = appClass.getDeclaredMethod("main", String[].class);
 407             int PUBLIC_STATIC = Modifier.PUBLIC | Modifier.STATIC;
 408             if ((main.getModifiers() & PUBLIC_STATIC) != PUBLIC_STATIC) {
 409                 throw new Fault(Errors.MainNotPublicStatic);
 410             }
 411             if (!main.getReturnType().equals(void.class)) {
 412                 throw new Fault(Errors.MainNotVoid);
 413             }
 414             main.setAccessible(true);
 415             main.invoke(0, (Object) appArgs);
 416         } catch (ClassNotFoundException e) {
 417             throw new Fault(Errors.CantFindClass(mainClassName));
 418         } catch (NoSuchMethodException e) {
 419             throw new Fault(Errors.CantFindMainMethod(mainClassName));
 420         } catch (IllegalAccessException e) {
 421             throw new Fault(Errors.CantAccessMainMethod(mainClassName));
 422         } catch (InvocationTargetException e) {
 423             // remove stack frames for source launcher
 424             int invocationFrames = e.getStackTrace().length;
 425             Throwable target = e.getTargetException();
 426             StackTraceElement[] targetTrace = target.getStackTrace();
 427             target.setStackTrace(Arrays.copyOfRange(targetTrace, 0, targetTrace.length - invocationFrames));
 428             throw e;
 429         }
 430     }
 431 
 432     private static final String bundleName = "com.sun.tools.javac.resources.launcher";
 433     private ResourceBundle resourceBundle = null;
 434     private String errorPrefix;
 435 
 436     /**
 437      * Returns a localized string from a resource bundle.
 438      *
 439      * @param error the error for which to get the localized text
 440      * @return the localized string
 441      */
 442     private String getMessage(Error error) {
 443         String key = error.key();
 444         Object[] args = error.getArgs();
 445         try {
 446             if (resourceBundle == null) {
 447                 resourceBundle = ResourceBundle.getBundle(bundleName);
 448                 errorPrefix = resourceBundle.getString("launcher.error");
 449             }
 450             String resource = resourceBundle.getString(key);
 451             String message = MessageFormat.format(resource, args);
 452             return errorPrefix + message;
 453         } catch (MissingResourceException e) {
 454             return "Cannot access resource; " + key + Arrays.toString(args);
 455         }
 456     }
 457 
 458     /**
 459      * A listener to detect the first class found in a compilation.
 460      */
 461     static class MainClassListener implements TaskListener {
 462         TypeElement mainClass;
 463 
 464         MainClassListener(JavacTask t) {
 465             t.addTaskListener(this);
 466         }
 467 
 468         @Override
 469         public void started(TaskEvent ev) {
 470             if (ev.getKind() == TaskEvent.Kind.ANALYZE && mainClass == null) {
 471                 TypeElement te = ev.getTypeElement();
 472                 if (te.getNestingKind() == NestingKind.TOP_LEVEL) {
 473                     mainClass = te;
 474                 }
 475             }
 476         }
 477     }
 478 
 479     /**
 480      * An object to encapulate the set of in-memory classes, such that
 481      * they can be written by a file manager and subsequently used by
 482      * a class loader.
 483      */
 484     private static class Context {
 485         private final Path file;
 486         private final Map<String, byte[]> inMemoryClasses = new HashMap<>();
 487 
 488         Context(Path file) {
 489             this.file = file;
 490         }
 491 
 492         JavaFileManager getFileManager(StandardJavaFileManager delegate) {
 493             return new MemoryFileManager(inMemoryClasses, delegate);
 494         }
 495 
 496         ClassLoader getClassLoader(ClassLoader parent) {
 497             return new MemoryClassLoader(inMemoryClasses, parent, file);
 498         }
 499     }
 500 
 501     /**
 502      * An in-memory file manager.
 503      *
 504      * <p>Class files (of kind {@link JavaFileObject.Kind#CLASS CLASS} written to
 505      * {@link StandardLocation#CLASS_OUTPUT} will be written to an in-memory cache.
 506      * All other file manager operations will be delegated to a specified file manager.
 507      */
 508     private static class MemoryFileManager extends ForwardingJavaFileManager<JavaFileManager> {
 509         private final Map<String, byte[]> map;
 510 
 511         MemoryFileManager(Map<String, byte[]> map, JavaFileManager delegate) {
 512             super(delegate);
 513             this.map = map;
 514         }
 515 
 516         @Override
 517         public JavaFileObject getJavaFileForOutput(Location location, String className,
 518                 JavaFileObject.Kind kind, FileObject sibling) throws IOException {
 519             if (location == StandardLocation.CLASS_OUTPUT && kind == JavaFileObject.Kind.CLASS) {
 520                 return createInMemoryClassFile(className);
 521             } else {
 522                 return super.getJavaFileForOutput(location, className, kind, sibling);
 523             }
 524         }
 525 
 526         private JavaFileObject createInMemoryClassFile(String className) {
 527             URI uri = URI.create("memory:///" + className.replace('.', '/') + ".class");
 528             return new SimpleJavaFileObject(uri, JavaFileObject.Kind.CLASS) {
 529                 @Override
 530                 public OutputStream openOutputStream() {
 531                     return new ByteArrayOutputStream() {
 532                         @Override
 533                         public void close() throws IOException {
 534                             super.close();
 535                             map.put(className, toByteArray());
 536                         }
 537                     };
 538                 }
 539             };
 540         }
 541     }
 542 
 543     /**
 544      * An in-memory classloader, that uses an in-memory cache of classes written by
 545      * {@link MemoryFileManager}.
 546      *
 547      * <p>The classloader inverts the standard parent-delegation model, giving preference
 548      * to classes defined in the source file before classes known to the parent (such
 549      * as any like-named classes that might be found on the application class path.)
 550      */
 551     private static class MemoryClassLoader extends ClassLoader {
 552         /**
 553          * The map of all classes found in the source file, indexed by
 554          * {@link ClassLoader#name binary name}.
 555          */
 556         private final Map<String, byte[]> sourceFileClasses;
 557 
 558         /**
 559          * A minimal protection domain, specifying a code source of the source file itself,
 560          * used for classes found in the source file and defined by this loader.
 561          */
 562         private final ProtectionDomain domain;
 563 
 564         MemoryClassLoader(Map<String, byte[]> sourceFileClasses, ClassLoader parent, Path file) {
 565             super(parent);
 566             this.sourceFileClasses = sourceFileClasses;
 567             CodeSource codeSource;
 568             try {
 569                 codeSource = new CodeSource(file.toUri().toURL(), (CodeSigner[]) null);
 570             } catch (MalformedURLException e) {
 571                 codeSource = null;
 572             }
 573             domain = new ProtectionDomain(codeSource, null, this, null);
 574         }
 575 
 576         /**
 577          * Override loadClass to check for classes defined in the source file
 578          * before checking for classes in the parent class loader,
 579          * including those on the classpath.
 580          *
 581          * {@code loadClass(String name)} calls this method, and so will have the same behavior.
 582          *
 583          * @param name the name of the class to load
 584          * @param resolve whether or not to resolve the class
 585          * @return the class
 586          * @throws ClassNotFoundException if the class is not found
 587          */
 588         @Override
 589         protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
 590             synchronized (getClassLoadingLock(name)) {
 591                 Class<?> c = findLoadedClass(name);
 592                 if (c == null) {
 593                     if (sourceFileClasses.containsKey(name)) {
 594                         c = findClass(name);
 595                     } else {
 596                         c = getParent().loadClass(name);
 597                     }
 598                     if (resolve) {
 599                         resolveClass(c);
 600                     }
 601                 }
 602                 return c;
 603             }
 604         }
 605 
 606 
 607         /**
 608          * Override getResource to check for resources (i.e. class files) defined in the
 609          * source file before checking resources in the parent class loader,
 610          * including those on the class path.
 611          *
 612          * {@code getResourceAsStream(String name)} calls this method,
 613          * and so will have the same behavior.
 614          *
 615          * @param name the name of the resource
 616          * @return a URL for the resource, or null if not found
 617          */
 618         @Override
 619         public URL getResource(String name) {
 620             if (sourceFileClasses.containsKey(toBinaryName(name))) {
 621                 return findResource(name);
 622             } else {
 623                 return getParent().getResource(name);
 624             }
 625         }
 626 
 627         /**
 628          * Override getResources to check for resources (i.e. class files) defined in the
 629          * source file before checking resources in the parent class loader,
 630          * including those on the class path.
 631          *
 632          * @param name the name of the resource
 633          * @return an enumeration of the resources in this loader and in the application class loader
 634          */
 635         @Override
 636         public Enumeration<URL> getResources(String name) throws IOException {
 637             URL u = findResource(name);
 638             Enumeration<URL> e = getParent().getResources(name);
 639             if (u == null) {
 640                 return e;
 641             } else {
 642                 List<URL> list = new ArrayList<>();
 643                 list.add(u);
 644                 while (e.hasMoreElements()) {
 645                     list.add(e.nextElement());
 646                 }
 647                 return Collections.enumeration(list);
 648             }
 649         }
 650 
 651         @Override
 652         protected Class<?> findClass(String name) throws ClassNotFoundException {
 653             byte[] bytes = sourceFileClasses.get(name);
 654             if (bytes == null) {
 655                 throw new ClassNotFoundException(name);
 656             }
 657             return defineClass(name, bytes, 0, bytes.length, domain);
 658         }
 659 
 660         @Override
 661         public URL findResource(String name) {
 662             String binaryName = toBinaryName(name);
 663             if (binaryName == null || sourceFileClasses.get(binaryName) == null) {
 664                 return null;
 665             }
 666 
 667             URLStreamHandler handler = this.handler;
 668             if (handler == null) {
 669                 this.handler = handler = new MemoryURLStreamHandler();
 670             }
 671 
 672             try {
 673                 return new URL(PROTOCOL, null, -1, name, handler);
 674             } catch (MalformedURLException e) {
 675                 return null;
 676             }
 677         }
 678 
 679         @Override
 680         public Enumeration<URL> findResources(String name) {
 681             return new Enumeration<URL>() {
 682                 private URL next = findResource(name);
 683 
 684                 @Override
 685                 public boolean hasMoreElements() {
 686                     return (next != null);
 687                 }
 688 
 689                 @Override
 690                 public URL nextElement() {
 691                     if (next == null) {
 692                         throw new NoSuchElementException();
 693                     }
 694                     URL u = next;
 695                     next = null;
 696                     return u;
 697                 }
 698             };
 699         }
 700 
 701         /**
 702          * Converts a "resource name" (as used in the getResource* methods)
 703          * to a binary name if the name identifies a class, or null otherwise.
 704          * @param name the resource name
 705          * @return the binary name
 706          */
 707         private String toBinaryName(String name) {
 708             if (!name.endsWith(".class")) {
 709                 return null;
 710             }
 711             return name.substring(0, name.length() - DOT_CLASS_LENGTH).replace('/', '.');
 712         }
 713 
 714         private static final int DOT_CLASS_LENGTH = ".class".length();
 715         private final String PROTOCOL = "sourcelauncher-" + getClass().getSimpleName() + hashCode();
 716         private URLStreamHandler handler;
 717 
 718         /**
 719          * A URLStreamHandler for use with URLs returned by MemoryClassLoader.getResource.
 720          */
 721         private class MemoryURLStreamHandler extends URLStreamHandler {
 722             @Override
 723             public URLConnection openConnection(URL u) {
 724                 if (!u.getProtocol().equalsIgnoreCase(PROTOCOL)) {
 725                     throw new IllegalArgumentException(u.toString());
 726                 }
 727                 return new MemoryURLConnection(u, sourceFileClasses.get(toBinaryName(u.getPath())));
 728             }
 729 
 730         }
 731 
 732         /**
 733          * A URLConnection for use with URLs returned by MemoryClassLoader.getResource.
 734          */
 735         private static class MemoryURLConnection extends URLConnection {
 736             private byte[] bytes;
 737             private InputStream in;
 738 
 739             MemoryURLConnection(URL u, byte[] bytes) {
 740                 super(u);
 741                 this.bytes = bytes;
 742             }
 743 
 744             @Override
 745             public void connect() throws IOException {
 746                 if (!connected) {
 747                     if (bytes == null) {
 748                         throw new FileNotFoundException(getURL().getPath());
 749                     }
 750                     in = new ByteArrayInputStream(bytes);
 751                     connected = true;
 752                 }
 753             }
 754 
 755             @Override
 756             public InputStream getInputStream() throws IOException {
 757                 connect();
 758                 return in;
 759             }
 760 
 761             @Override
 762             public long getContentLengthLong() {
 763                 return bytes.length;
 764             }
 765 
 766             @Override
 767             public String getContentType() {
 768                 return "application/octet-stream";
 769             }
 770         }
 771     }
 772 }