• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.google.clearsilver.jsilver.compiler;
18 
19 import java.net.URISyntaxException;
20 import java.net.URI;
21 import java.io.IOException;
22 import java.io.ByteArrayOutputStream;
23 import java.io.OutputStream;
24 import static java.util.Collections.singleton;
25 import java.util.Map;
26 import java.util.HashMap;
27 import java.util.List;
28 import java.util.LinkedList;
29 
30 import javax.tools.JavaCompiler;
31 import javax.tools.ToolProvider;
32 import javax.tools.JavaFileObject;
33 import javax.tools.SimpleJavaFileObject;
34 import javax.tools.JavaFileManager;
35 import javax.tools.ForwardingJavaFileManager;
36 import javax.tools.FileObject;
37 import 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  */
77 public class CompilingClassLoader extends ClassLoader {
78 
79   /**
80    * Thrown when code cannot be compiled.
81    */
82   public static class CompilerException extends Exception {
83 
CompilerException(String message)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    */
CompilingClassLoader(ClassLoader parent, String className, CharSequence sourceCode, DiagnosticListener<JavaFileObject> diagnosticListener)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
findClass(String name)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    */
compileSourceCodeToByteCode(String className, CharSequence sourceCode, DiagnosticListener<JavaFileObject> diagnosticListener)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 
InMemoryFileManager(JavaFileManager fileManager)169     public InMemoryFileManager(JavaFileManager fileManager) {
170       super(fileManager);
171     }
172 
173     @Override
getJavaFileForOutput(Location location, final String className, JavaFileObject.Kind kind, FileObject sibling)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