/* * Copyright (C) 2010 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.clearsilver.jsilver.compiler; import java.net.URISyntaxException; import java.net.URI; import java.io.IOException; import java.io.ByteArrayOutputStream; import java.io.OutputStream; import static java.util.Collections.singleton; import java.util.Map; import java.util.HashMap; import java.util.List; import java.util.LinkedList; import javax.tools.JavaCompiler; import javax.tools.ToolProvider; import javax.tools.JavaFileObject; import javax.tools.SimpleJavaFileObject; import javax.tools.JavaFileManager; import javax.tools.ForwardingJavaFileManager; import javax.tools.FileObject; import javax.tools.DiagnosticListener; /** * This is a Java ClassLoader that will attempt to load a class from a string of source code. * *

Example

* *
 * String className = "com.foo.MyClass";
 * String classSource =
 *   "package com.foo;\n" +
 *   "public class MyClass implements Runnable {\n" +
 *   "  @Override public void run() {\n" +
 *   "    System.out.println(\"Hello world\");\n" +
 *   "  }\n" +
 *   "}";
 *
 * // Load class from source.
 * ClassLoader classLoader = new CompilingClassLoader(
 *     parentClassLoader, className, classSource);
 * Class myClass = classLoader.loadClass(className);
 *
 * // Use it.
 * Runnable instance = (Runnable)myClass.newInstance();
 * instance.run();
 * 
* * Only one chunk of source can be compiled per instance of CompilingClassLoader. If you need to * compile more, create multiple CompilingClassLoader instances. * * Uses Java 1.6's in built compiler API. * * If the class cannot be compiled, loadClass() will throw a ClassNotFoundException and log the * compile errors to System.err. If you don't want the messages logged, or want to explicitly handle * the messages you can provide your own {@link javax.tools.DiagnosticListener} through * {#setDiagnosticListener()}. * * @see java.lang.ClassLoader * @see javax.tools.JavaCompiler */ public class CompilingClassLoader extends ClassLoader { /** * Thrown when code cannot be compiled. */ public static class CompilerException extends Exception { public CompilerException(String message) { super(message); } } private Map byteCodeForClasses = new HashMap(); private static final URI EMPTY_URI; static { try { // Needed to keep SimpleFileObject constructor happy. EMPTY_URI = new URI(""); } catch (URISyntaxException e) { throw new Error(e); } } /** * @param parent Parent classloader to resolve dependencies from. * @param className Name of class to compile. eg. "com.foo.MyClass". * @param sourceCode Java source for class. e.g. "package com.foo; class MyClass { ... }". * @param diagnosticListener Notified of compiler errors (may be null). */ public CompilingClassLoader(ClassLoader parent, String className, CharSequence sourceCode, DiagnosticListener diagnosticListener) throws CompilerException { super(parent); if (!compileSourceCodeToByteCode(className, sourceCode, diagnosticListener)) { throw new CompilerException("Could not compile " + className); } } /** * Override ClassLoader's class resolving method. Don't call this directly, instead use * {@link ClassLoader#loadClass(String)}. */ @Override public Class findClass(String name) throws ClassNotFoundException { ByteArrayOutputStream byteCode = byteCodeForClasses.get(name); if (byteCode == null) { throw new ClassNotFoundException(name); } return defineClass(name, byteCode.toByteArray(), 0, byteCode.size()); } /** * @return Whether compilation was successful. */ private boolean compileSourceCodeToByteCode(String className, CharSequence sourceCode, DiagnosticListener diagnosticListener) { JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler(); // Set up the in-memory filesystem. InMemoryFileManager fileManager = new InMemoryFileManager(javaCompiler.getStandardFileManager(null, null, null)); JavaFileObject javaFile = new InMemoryJavaFile(className, sourceCode); // Javac option: remove these when the javac zip impl is fixed // (http://b/issue?id=1822932) System.setProperty("useJavaUtilZip", "true"); // setting value to any non-null string List options = new LinkedList(); // this is ignored by javac currently but useJavaUtilZip should be // a valid javac XD option, which is another bug options.add("-XDuseJavaUtilZip"); // Now compile! JavaCompiler.CompilationTask compilationTask = javaCompiler.getTask(null, // Null: log any // unhandled errors to // stderr. fileManager, diagnosticListener, options, null, singleton(javaFile)); return compilationTask.call(); } /** * Provides an in-memory representation of JavaFileManager abstraction, so we do not need to write * any files to disk. * * When files are written to, rather than putting the bytes on disk, they are appended to buffers * in byteCodeForClasses. * * @see javax.tools.JavaFileManager */ private class InMemoryFileManager extends ForwardingJavaFileManager { public InMemoryFileManager(JavaFileManager fileManager) { super(fileManager); } @Override public JavaFileObject getJavaFileForOutput(Location location, final String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException { return new SimpleJavaFileObject(EMPTY_URI, kind) { public OutputStream openOutputStream() throws IOException { ByteArrayOutputStream outputStream = byteCodeForClasses.get(className); if (outputStream != null) { throw new IllegalStateException("Cannot write more than once"); } // Reasonable size for a simple .class. outputStream = new ByteArrayOutputStream(256); byteCodeForClasses.put(className, outputStream); return outputStream; } }; } } private static class InMemoryJavaFile extends SimpleJavaFileObject { private final CharSequence sourceCode; public InMemoryJavaFile(String className, CharSequence sourceCode) { super(makeUri(className), Kind.SOURCE); this.sourceCode = sourceCode; } private static URI makeUri(String className) { try { return new URI(className.replaceAll("\\.", "/") + Kind.SOURCE.extension); } catch (URISyntaxException e) { throw new RuntimeException(e); // Not sure what could cause this. } } @Override public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { return sourceCode; } } }