package org.joor;
import static java.lang.StackWalker.Option.RETAIN_CLASS_REFERENCE;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.OutputStream;
import java.io.StringWriter;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodHandles.Lookup;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.tools.FileObject;
import javax.tools.ForwardingJavaFileManager;
import javax.tools.JavaCompiler;
import javax.tools.JavaCompiler.CompilationTask;
import javax.tools.JavaFileManager;
import javax.tools.SimpleJavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
class Compile {
static Class<?> compile(String className, String content, CompileOptions compileOptions) {
Lookup lookup = MethodHandles.lookup();
ClassLoader cl = lookup.lookupClass().getClassLoader();
try {
return cl.loadClass(className);
}
catch (ClassNotFoundException ignore) {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
try {
ClassFileManager fileManager = new ClassFileManager(compiler.getStandardFileManager(null, null, null));
List<CharSequenceJavaFileObject> files = new ArrayList<>();
files.add(new CharSequenceJavaFileObject(className, content));
StringWriter out = new StringWriter();
List<String> options = new ArrayList<>(compileOptions.options);
if (!options.contains("-classpath")) {
StringBuilder classpath = new StringBuilder();
String separator = System.getProperty("path.separator");
String prop = System.getProperty("java.class.path");
if (prop != null && !"".equals(prop))
classpath.append(prop);
if (cl instanceof URLClassLoader) {
for (URL url : ((URLClassLoader) cl).getURLs()) {
if (classpath.length() > 0)
classpath.append(separator);
if ("file".equals(url.getProtocol()))
classpath.append(new File(url.toURI()));
}
}
options.addAll(Arrays.asList("-classpath", classpath.toString()));
}
CompilationTask task = compiler.getTask(out, fileManager, null, options, null, files);
if (!compileOptions.processors.isEmpty())
task.setProcessors(compileOptions.processors);
task.call();
if (fileManager.isEmpty())
throw new ReflectException("Compilation error: " + out);
Class<?> result = null;
if (Reflect.CACHED_LOOKUP_CONSTRUCTOR != null) {
result = fileManager.loadAndReturnMainClass(className,
(name, bytes) -> Reflect.on(cl).call("defineClass", name, bytes, 0, bytes.length).get());
}
else {
Class<?> caller = StackWalker
.getInstance(RETAIN_CLASS_REFERENCE)
.walk(s -> s
.skip(2)
.findFirst()
.get()
.getDeclaringClass());
if (className.startsWith(caller.getPackageName() + ".") &&
Character.isUpperCase(className.charAt(caller.getPackageName().length() + 1))) {
Lookup privateLookup = MethodHandles.privateLookupIn(caller, lookup);
result = fileManager.loadAndReturnMainClass(className,
(name, bytes) -> privateLookup.defineClass(bytes));
}
else {
ByteArrayClassLoader c = new ByteArrayClassLoader(fileManager.classes());
result = fileManager.loadAndReturnMainClass(className,
(name, bytes) -> c.loadClass(name));
}
}
return result;
}
catch (ReflectException e) {
throw e;
}
catch (Exception e) {
throw new ReflectException("Error while compiling " + className, e);
}
}
}
static final class ByteArrayClassLoader extends ClassLoader {
private final Map<String, byte[]> classes;
ByteArrayClassLoader(Map<String, byte[]> classes) {
super(ByteArrayClassLoader.class.getClassLoader());
this.classes = classes;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] bytes = classes.get(name);
if (bytes == null)
return super.findClass(name);
else
return defineClass(name, bytes, 0, bytes.length);
}
}
static final class JavaFileObject extends SimpleJavaFileObject {
final ByteArrayOutputStream os = new ByteArrayOutputStream();
JavaFileObject(String name, JavaFileObject.Kind kind) {
super(URI.create("string:///" + name.replace('.', '/') + kind.extension), kind);
}
byte[] getBytes() {
return os.toByteArray();
}
@Override
public OutputStream openOutputStream() {
return os;
}
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) {
return new String(os.toByteArray(), StandardCharsets.UTF_8);
}
}
static final class ClassFileManager extends ForwardingJavaFileManager<StandardJavaFileManager> {
private final Map<String, JavaFileObject> fileObjectMap;
private Map<String, byte[]> classes;
ClassFileManager(StandardJavaFileManager standardManager) {
super(standardManager);
fileObjectMap = new HashMap<>();
}
@Override
public JavaFileObject getJavaFileForOutput(
JavaFileManager.Location location,
String className,
JavaFileObject.Kind kind,
FileObject sibling
) {
JavaFileObject result = new JavaFileObject(className, kind);
fileObjectMap.put(className, result);
return result;
}
boolean isEmpty() {
return fileObjectMap.isEmpty();
}
Map<String, byte[]> classes() {
if (classes == null) {
classes = new HashMap<>();
for (Entry<String, JavaFileObject> entry : fileObjectMap.entrySet())
classes.put(entry.getKey(), entry.getValue().getBytes());
}
return classes;
}
Class<?> loadAndReturnMainClass(String mainClassName, ThrowingBiFunction<String, byte[], Class<?>> definer) throws Exception {
Class<?> result = null;
for (Entry<String, byte[]> entry : classes().entrySet()) {
Class<?> c = definer.apply(entry.getKey(), entry.getValue());
if (mainClassName.equals(entry.getKey()))
result = c;
}
return result;
}
}
@FunctionalInterface
interface ThrowingBiFunction<T, U, R> {
R apply(T t, U u) throws Exception;
}
static final class CharSequenceJavaFileObject extends SimpleJavaFileObject {
final CharSequence content;
public CharSequenceJavaFileObject(String className, CharSequence content) {
super(URI.create("string:///" + className.replace('.', '/') + JavaFileObject.Kind.SOURCE.extension), JavaFileObject.Kind.SOURCE);
this.content = content;
}
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) {
return content;
}
}
}