1/*
2 * Copyright (C) 2010 Google Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.google.clearsilver.jsilver.compiler;
18
19import java.net.URISyntaxException;
20import java.net.URI;
21import java.io.IOException;
22import java.io.ByteArrayOutputStream;
23import java.io.OutputStream;
24import static java.util.Collections.singleton;
25import java.util.Map;
26import java.util.HashMap;
27import java.util.List;
28import java.util.LinkedList;
29
30import javax.tools.JavaCompiler;
31import javax.tools.ToolProvider;
32import javax.tools.JavaFileObject;
33import javax.tools.SimpleJavaFileObject;
34import javax.tools.JavaFileManager;
35import javax.tools.ForwardingJavaFileManager;
36import javax.tools.FileObject;
37import javax.tools.DiagnosticListener;
38
39/**
40 * This is a Java ClassLoader that will attempt to load a class from a string of source code.
41 *
42 * <h3>Example</h3>
43 *
44 * <pre>
45 * String className = "com.foo.MyClass";
46 * String classSource =
47 *   "package com.foo;\n" +
48 *   "public class MyClass implements Runnable {\n" +
49 *   "  @Override public void run() {\n" +
50 *   "    System.out.println(\"Hello world\");\n" +
51 *   "  }\n" +
52 *   "}";
53 *
54 * // Load class from source.
55 * ClassLoader classLoader = new CompilingClassLoader(
56 *     parentClassLoader, className, classSource);
57 * Class myClass = classLoader.loadClass(className);
58 *
59 * // Use it.
60 * Runnable instance = (Runnable)myClass.newInstance();
61 * instance.run();
62 * </pre>
63 *
64 * Only one chunk of source can be compiled per instance of CompilingClassLoader. If you need to
65 * compile more, create multiple CompilingClassLoader instances.
66 *
67 * Uses Java 1.6's in built compiler API.
68 *
69 * If the class cannot be compiled, loadClass() will throw a ClassNotFoundException and log the
70 * compile errors to System.err. If you don't want the messages logged, or want to explicitly handle
71 * the messages you can provide your own {@link javax.tools.DiagnosticListener} through
72 * {#setDiagnosticListener()}.
73 *
74 * @see java.lang.ClassLoader
75 * @see javax.tools.JavaCompiler
76 */
77public class CompilingClassLoader extends ClassLoader {
78
79  /**
80   * Thrown when code cannot be compiled.
81   */
82  public static class CompilerException extends Exception {
83
84    public CompilerException(String message) {
85      super(message);
86    }
87  }
88
89  private Map<String, ByteArrayOutputStream> byteCodeForClasses =
90      new HashMap<String, ByteArrayOutputStream>();
91
92  private static final URI EMPTY_URI;
93
94  static {
95    try {
96      // Needed to keep SimpleFileObject constructor happy.
97      EMPTY_URI = new URI("");
98    } catch (URISyntaxException e) {
99      throw new Error(e);
100    }
101  }
102
103  /**
104   * @param parent Parent classloader to resolve dependencies from.
105   * @param className Name of class to compile. eg. "com.foo.MyClass".
106   * @param sourceCode Java source for class. e.g. "package com.foo; class MyClass { ... }".
107   * @param diagnosticListener Notified of compiler errors (may be null).
108   */
109  public CompilingClassLoader(ClassLoader parent, String className, CharSequence sourceCode,
110      DiagnosticListener<JavaFileObject> diagnosticListener) throws CompilerException {
111    super(parent);
112    if (!compileSourceCodeToByteCode(className, sourceCode, diagnosticListener)) {
113      throw new CompilerException("Could not compile " + className);
114    }
115  }
116
117  /**
118   * Override ClassLoader's class resolving method. Don't call this directly, instead use
119   * {@link ClassLoader#loadClass(String)}.
120   */
121  @Override
122  public Class findClass(String name) throws ClassNotFoundException {
123    ByteArrayOutputStream byteCode = byteCodeForClasses.get(name);
124    if (byteCode == null) {
125      throw new ClassNotFoundException(name);
126    }
127    return defineClass(name, byteCode.toByteArray(), 0, byteCode.size());
128  }
129
130  /**
131   * @return Whether compilation was successful.
132   */
133  private boolean compileSourceCodeToByteCode(String className, CharSequence sourceCode,
134      DiagnosticListener<JavaFileObject> diagnosticListener) {
135    JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler();
136
137    // Set up the in-memory filesystem.
138    InMemoryFileManager fileManager =
139        new InMemoryFileManager(javaCompiler.getStandardFileManager(null, null, null));
140    JavaFileObject javaFile = new InMemoryJavaFile(className, sourceCode);
141
142    // Javac option: remove these when the javac zip impl is fixed
143    // (http://b/issue?id=1822932)
144    System.setProperty("useJavaUtilZip", "true"); // setting value to any non-null string
145    List<String> options = new LinkedList<String>();
146    // this is ignored by javac currently but useJavaUtilZip should be
147    // a valid javac XD option, which is another bug
148    options.add("-XDuseJavaUtilZip");
149
150    // Now compile!
151    JavaCompiler.CompilationTask compilationTask = javaCompiler.getTask(null, // Null: log any
152                                                                              // unhandled errors to
153                                                                              // stderr.
154        fileManager, diagnosticListener, options, null, singleton(javaFile));
155    return compilationTask.call();
156  }
157
158  /**
159   * Provides an in-memory representation of JavaFileManager abstraction, so we do not need to write
160   * any files to disk.
161   *
162   * When files are written to, rather than putting the bytes on disk, they are appended to buffers
163   * in byteCodeForClasses.
164   *
165   * @see javax.tools.JavaFileManager
166   */
167  private class InMemoryFileManager extends ForwardingJavaFileManager<JavaFileManager> {
168
169    public InMemoryFileManager(JavaFileManager fileManager) {
170      super(fileManager);
171    }
172
173    @Override
174    public JavaFileObject getJavaFileForOutput(Location location, final String className,
175        JavaFileObject.Kind kind, FileObject sibling) throws IOException {
176      return new SimpleJavaFileObject(EMPTY_URI, kind) {
177        public OutputStream openOutputStream() throws IOException {
178          ByteArrayOutputStream outputStream = byteCodeForClasses.get(className);
179          if (outputStream != null) {
180            throw new IllegalStateException("Cannot write more than once");
181          }
182          // Reasonable size for a simple .class.
183          outputStream = new ByteArrayOutputStream(256);
184          byteCodeForClasses.put(className, outputStream);
185          return outputStream;
186        }
187      };
188    }
189  }
190
191  private static class InMemoryJavaFile extends SimpleJavaFileObject {
192
193    private final CharSequence sourceCode;
194
195    public InMemoryJavaFile(String className, CharSequence sourceCode) {
196      super(makeUri(className), Kind.SOURCE);
197      this.sourceCode = sourceCode;
198    }
199
200    private static URI makeUri(String className) {
201      try {
202        return new URI(className.replaceAll("\\.", "/") + Kind.SOURCE.extension);
203      } catch (URISyntaxException e) {
204        throw new RuntimeException(e); // Not sure what could cause this.
205      }
206    }
207
208    @Override
209    public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
210      return sourceCode;
211    }
212  }
213}
214