/* * 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. * * <h3>Example</h3> * * <pre> * 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(); * </pre> * * 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<String, ByteArrayOutputStream> byteCodeForClasses = new HashMap<String, ByteArrayOutputStream>(); 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<JavaFileObject> 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<JavaFileObject> 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<String> options = new LinkedList<String>(); // 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<JavaFileManager> { 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; } } }