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 }