/*******************************************************************************
* Copyright (c) 2009-2013 CWI
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* * Jurgen J. Vinju - Jurgen.Vinju@cwi.nl - CWI
* * Anya Helene Bagge - anya@ii.uib.no (Univ. Bergen)
* * Paul Klint - Paul.Klint@cwi.nl - CWI
* * Mark Hills - Mark.Hills@cwi.nl (CWI)
* * Arnold Lankamp - Arnold.Lankamp@cwi.nl
*******************************************************************************/
package org.rascalmpl.interpreter.utils;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
import javax.tools.StandardLocation;
import javax.tools.ToolProvider;
import org.eclipse.imp.pdb.facts.IBool;
import org.eclipse.imp.pdb.facts.IConstructor;
import org.eclipse.imp.pdb.facts.IDateTime;
import org.eclipse.imp.pdb.facts.IInteger;
import org.eclipse.imp.pdb.facts.IList;
import org.eclipse.imp.pdb.facts.IMap;
import org.eclipse.imp.pdb.facts.INode;
import org.eclipse.imp.pdb.facts.INumber;
import org.eclipse.imp.pdb.facts.IRational;
import org.eclipse.imp.pdb.facts.IReal;
import org.eclipse.imp.pdb.facts.ISet;
import org.eclipse.imp.pdb.facts.ISourceLocation;
import org.eclipse.imp.pdb.facts.IString;
import org.eclipse.imp.pdb.facts.ITuple;
import org.eclipse.imp.pdb.facts.IValue;
import org.eclipse.imp.pdb.facts.IValueFactory;
import org.eclipse.imp.pdb.facts.type.ITypeVisitor;
import org.eclipse.imp.pdb.facts.type.Type;
import org.rascalmpl.ast.Expression;
import org.rascalmpl.ast.FunctionDeclaration;
import org.rascalmpl.ast.KeywordFormal;
import org.rascalmpl.ast.Parameters;
import org.rascalmpl.ast.Tag;
import org.rascalmpl.ast.TagString;
import org.rascalmpl.ast.Tags;
import org.rascalmpl.interpreter.Configuration;
import org.rascalmpl.interpreter.IEvaluator;
import org.rascalmpl.interpreter.IEvaluatorContext;
import org.rascalmpl.interpreter.asserts.ImplementationError;
import org.rascalmpl.interpreter.env.Environment;
import org.rascalmpl.interpreter.result.Result;
import org.rascalmpl.interpreter.staticErrors.JavaCompilation;
import org.rascalmpl.interpreter.staticErrors.JavaMethodLink;
import org.rascalmpl.interpreter.staticErrors.MissingTag;
import org.rascalmpl.interpreter.staticErrors.NonAbstractJavaFunction;
import org.rascalmpl.interpreter.staticErrors.UndeclaredJavaMethod;
public class JavaBridge {
private static final String JAVA_CLASS_TAG = "javaClass";
private final List<ClassLoader> loaders;
private final static JavaClasses javaClasses = new JavaClasses();
private final IValueFactory vf;
private final Map<Class<?>, Object> instanceCache;
private final Map<Class<?>, JavaFileManager> fileManagerCache;
private final Configuration config;
public JavaBridge(List<ClassLoader> classLoaders, IValueFactory valueFactory, Configuration config) {
this.loaders = classLoaders;
this.vf = valueFactory;
this.instanceCache = new HashMap<Class<?>, Object>();
this.fileManagerCache = new ConcurrentHashMap<Class<?>, JavaFileManager>();
this.config = config;
if (ToolProvider.getSystemJavaCompiler() == null) {
throw new ImplementationError("Could not find an installed System Java Compiler, please provide a Java Runtime that includes the Java Development Tools (JDK 1.6 or higher).");
}
}
public <T> Class<T> compileJava(URI loc, String className, String source) {
return compileJava(loc, className, getClass(), source);
}
public <T> Class<T> compileJava(URI loc, String className, Class<?> parent, String source) {
try {
// watch out, if you start sharing this compiler, classes will not be able to reload
List<String> commandline = Arrays.asList(new String[] {"-cp", config.getRascalJavaClassPathProperty()});
JavaCompiler<T> javaCompiler = new JavaCompiler<T>(parent.getClassLoader(), fileManagerCache.get(parent), commandline);
Class<T> result = javaCompiler.compile(className, source, null, Object.class);
fileManagerCache.put(result, javaCompiler.getFileManager());
return result;
} catch (ClassCastException e) {
throw new JavaCompilation(e.getMessage(), vf.sourceLocation(loc));
} catch (JavaCompilerException e) {
throw new JavaCompilation("with classpath [" + config.getRascalJavaClassPathProperty() + "]: " + e.getDiagnostics().getDiagnostics().iterator().next().getMessage(null), vf.sourceLocation(loc));
}
}
private String getClassName(FunctionDeclaration declaration) {
Tags tags = declaration.getTags();
if (tags.hasTags()) {
for (Tag tag : tags.getTags()) {
if (Names.name(tag.getName()).equals(JAVA_CLASS_TAG)) {
if(tag.hasContents()){
String contents = ((TagString.Lexical) tag.getContents()).getString();
if (contents.length() > 2 && contents.startsWith("{")) {
contents = contents.substring(1, contents.length() - 1);
}
return contents;
}
}
}
}
return "";
}
private Class<?>[] getJavaTypes(Parameters parameters, Environment env, boolean hasReflectiveAccess) {
List<Expression> formals = parameters.getFormals().getFormals();
int arity = formals.size();
int kwArity = 0;
List<KeywordFormal> keywordFormals = null;
if(parameters.getKeywordFormals().isDefault()){
keywordFormals = parameters.getKeywordFormals().getKeywordFormalList();
kwArity = keywordFormals.size();
}
Class<?>[] classes = new Class<?>[arity + kwArity + (hasReflectiveAccess ? 1 : 0)];
int i = 0;
while (i < arity) {
Class<?> clazz;
if (i == arity - 1 && parameters.isVarArgs()) {
clazz = IList.class;
}
else {
clazz = toJavaClass(formals.get(i), env);
}
if (clazz != null) {
classes[i++] = clazz;
}
}
while(i < arity + kwArity){
Class<?> clazz = toJavaClass(keywordFormals.get(i - arity).getType(), env);
if (clazz != null) {
classes[i++] = clazz;
}
}
if (hasReflectiveAccess) {
classes[arity + kwArity] = IEvaluatorContext.class;
}
return classes;
}
private Class<?> toJavaClass(Expression formal, Environment env) {
return toJavaClass(toValueType(formal, env));
}
private Class<?> toJavaClass(org.rascalmpl.ast.Type tp, Environment env) {
return toJavaClass(tp.typeOf(env, true, null));
}
private Class<?> toJavaClass(org.eclipse.imp.pdb.facts.type.Type type) {
return type.accept(javaClasses);
}
private org.eclipse.imp.pdb.facts.type.Type toValueType(Expression formal, Environment env) {
return formal.typeOf(env, true, null);
}
private static class JavaClasses implements ITypeVisitor<Class<?>, RuntimeException> {
@Override
public Class<?> visitBool(org.eclipse.imp.pdb.facts.type.Type boolType) {
return IBool.class;
}
@Override
public Class<?> visitReal(org.eclipse.imp.pdb.facts.type.Type type) {
return IReal.class;
}
@Override
public Class<?> visitInteger(org.eclipse.imp.pdb.facts.type.Type type) {
return IInteger.class;
}
@Override
public Class<?> visitRational(org.eclipse.imp.pdb.facts.type.Type type) {
return IRational.class;
}
@Override
public Class<?> visitNumber(org.eclipse.imp.pdb.facts.type.Type type) {
return INumber.class;
}
@Override
public Class<?> visitList(org.eclipse.imp.pdb.facts.type.Type type) {
return IList.class;
}
@Override
public Class<?> visitMap(org.eclipse.imp.pdb.facts.type.Type type) {
return IMap.class;
}
@Override
public Class<?> visitAlias(org.eclipse.imp.pdb.facts.type.Type type) {
return type.getAliased().accept(this);
}
@Override
public Class<?> visitAbstractData(org.eclipse.imp.pdb.facts.type.Type type) {
return IConstructor.class;
}
@Override
public Class<?> visitSet(org.eclipse.imp.pdb.facts.type.Type type) {
return ISet.class;
}
@Override
public Class<?> visitSourceLocation(org.eclipse.imp.pdb.facts.type.Type type) {
return ISourceLocation.class;
}
@Override
public Class<?> visitString(org.eclipse.imp.pdb.facts.type.Type type) {
return IString.class;
}
@Override
public Class<?> visitNode(org.eclipse.imp.pdb.facts.type.Type type) {
return INode.class;
}
@Override
public Class<?> visitConstructor(org.eclipse.imp.pdb.facts.type.Type type) {
return IConstructor.class;
}
@Override
public Class<?> visitTuple(org.eclipse.imp.pdb.facts.type.Type type) {
return ITuple.class;
}
@Override
public Class<?> visitValue(org.eclipse.imp.pdb.facts.type.Type type) {
return IValue.class;
}
@Override
public Class<?> visitVoid(org.eclipse.imp.pdb.facts.type.Type type) {
return null;
}
@Override
public Class<?> visitParameter(org.eclipse.imp.pdb.facts.type.Type parameterType) {
return parameterType.getBound().accept(this);
}
@Override
public Class<?> visitExternal(
org.eclipse.imp.pdb.facts.type.Type externalType) {
return IValue.class;
}
@Override
public Class<?> visitDateTime(Type type) {
return IDateTime.class;
}
}
public synchronized Object getJavaClassInstance(Class<?> clazz){
Object instance = instanceCache.get(clazz);
if(instance != null){
return instance;
}
try{
Constructor<?> constructor = clazz.getConstructor(IValueFactory.class);
instance = constructor.newInstance(vf);
instanceCache.put(clazz, instance);
return instance;
} catch (IllegalArgumentException e) {
throw new ImplementationError(e.getMessage(), e);
} catch (InstantiationException e) {
throw new ImplementationError(e.getMessage(), e);
} catch (IllegalAccessException e) {
throw new ImplementationError(e.getMessage(), e);
} catch (InvocationTargetException e) {
throw new ImplementationError(e.getMessage(), e);
} catch (SecurityException e) {
throw new ImplementationError(e.getMessage(), e);
} catch (NoSuchMethodException e) {
throw new ImplementationError(e.getMessage(), e);
}
}
public synchronized Object getJavaClassInstance(FunctionDeclaration func){
String className = getClassName(func);
try {
for(ClassLoader loader : loaders){
try{
Class<?> clazz = loader.loadClass(className);
Object instance = instanceCache.get(clazz);
if(instance != null){
return instance;
}
Constructor<?> constructor = clazz.getConstructor(IValueFactory.class);
instance = constructor.newInstance(vf);
instanceCache.put(clazz, instance);
return instance;
}
catch(ClassNotFoundException e){
continue;
}
}
}
catch(NoClassDefFoundError e) {
throw new JavaMethodLink(className, e.getMessage(), func, e);
}
catch (IllegalArgumentException e) {
throw new JavaMethodLink(className, e.getMessage(), func, e);
} catch (InstantiationException e) {
throw new JavaMethodLink(className, e.getMessage(), func, e);
} catch (IllegalAccessException e) {
throw new JavaMethodLink(className, e.getMessage(), func, e);
} catch (InvocationTargetException e) {
throw new JavaMethodLink(className, e.getMessage(), func, e);
} catch (SecurityException e) {
throw new JavaMethodLink(className, e.getMessage(), func, e);
} catch (NoSuchMethodException e) {
throw new JavaMethodLink(className, e.getMessage(), func, e);
}
throw new JavaMethodLink(className, "class not found", func, null);
}
public Method lookupJavaMethod(IEvaluator<Result<IValue>> eval, FunctionDeclaration func, Environment env, boolean hasReflectiveAccess){
if(!func.isAbstract()){
throw new NonAbstractJavaFunction(func);
}
String className = getClassName(func);
String name = Names.name(func.getSignature().getName());
if(className.length() == 0){ // TODO: Can this ever be thrown since the Class instance has
// already been identified via the javaClass tag.
throw new MissingTag(JAVA_CLASS_TAG, func);
}
for(ClassLoader loader : loaders){
try{
Class<?> clazz = loader.loadClass(className);
Parameters parameters = func.getSignature().getParameters();
Class<?>[] javaTypes = getJavaTypes(parameters, env, hasReflectiveAccess);
try{
Method m;
if(javaTypes.length > 0){ // non-void
m = clazz.getMethod(name, javaTypes);
}else{
m = clazz.getMethod(name);
}
return m;
}catch(SecurityException e){
throw RuntimeExceptionFactory.permissionDenied(vf.string(e.getMessage()), eval.getCurrentAST(), eval.getStackTrace());
}catch(NoSuchMethodException e){
throw new UndeclaredJavaMethod(e.getMessage(), func);
}
}catch(ClassNotFoundException e){
continue;
}
}
throw new UndeclaredJavaMethod(className + "." + name, func);
}
/**
* Same as saveToJar("", clazz, outPath, false);
*/
public void saveToJar(Class<?> clazz, OutputStream outStream) throws IOException {
saveToJar("", clazz, null, outStream, false);
}
/**
* Save a compiled class and associated classes to a jar file.
*
* With a packageName = "" and recursive = false, it will save clazz and any classes
* compiled from the same source (I think); this is probably what you want.
*
* @param packageName package name prefix to search for classes, or "" for all
* @param clazz a class that has been previously compiled by this bridge
* @param outStream output stream
* @param recursive whether to retrieve classes from rest of the the JavaFileManager hierarchy
*
* @throws FileNotFoundException
* @throws IOException
*/
public void saveToJar(String packageName, Class<?> clazz, OutputStream outStream,
boolean recursive) throws IOException {
saveToJar(packageName, clazz, null, outStream, recursive);
}
/**
* Save a compiled class and associated classes to a jar file.
*
* With a packageName = "" and recursive = false, it will save clazz and any classes
* compiled from the same source (I think); this is probably what you want.
*
* @param packageName package name prefix to search for classes, or "" for all
* @param clazz a class that has been previously compiled by this bridge
* @param mainClazz a class that will be installed as the "Main-Class" of a runnable jar
* @param outStream output stream
* @param recursive whether to retrieve classes from rest of the the JavaFileManager hierarchy
*
* @throws FileNotFoundException
* @throws IOException
*/
public void saveToJar(String packageName, Class<?> clazz, Class<?> mainClazz, OutputStream outStream,
boolean recursive) throws IOException {
JavaFileManager manager = fileManagerCache.get(clazz);
List<JavaFileObject> list = new ArrayList<JavaFileObject>();
for(JavaFileObject obj : manager.list(StandardLocation.CLASS_PATH, packageName,
Collections.singleton(JavaFileObject.Kind.CLASS), false))
list.add(obj);
if (list.iterator().hasNext()) {
Manifest manifest = new Manifest();
manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION,
"1.0");
if(mainClazz != null) {
manifest.getMainAttributes().put(Attributes.Name.MAIN_CLASS, mainClazz.getName());
}
manifest.getMainAttributes().put(new Attributes.Name("X-Rascal-Saved-Class"), clazz.getName());
JarOutputStream target = new JarOutputStream(outStream, manifest);
JarEntry entry = new JarEntry("META-INF/");
target.putNextEntry(entry);
Collection<String> dirs = new ArrayList<String>();
for (JavaFileObject o : list) {
String path = o.toUri().getPath().replace(".", "/");
makeJarDirs(target, dirs, path);
entry = new JarEntry(path + ".class");
entry.setTime(o.getLastModified());
target.putNextEntry(entry);
try(InputStream stream = o.openInputStream()) {
byte[] buffer = new byte[8192];
int c = stream.read(buffer);
while (c > -1) {
target.write(buffer, 0, c);
c = stream.read(buffer);
}
}
target.closeEntry();
}
if(mainClazz != null) {
String name = mainClazz.getName();
String path = name.replace(".", "/") + ".class";
String dir = path.substring(0, path.lastIndexOf('/'));
StringBuilder dirTmp = new StringBuilder(dir.length());
for (String d : dir.split("/")) {
dirTmp.append(d);
dirTmp.append("/");
String tmp = dirTmp.toString();
if (!dirs.contains(tmp)) {
dirs.add(tmp);
entry = new JarEntry(tmp);
target.putNextEntry(entry);
}
}
entry = new JarEntry(path);
target.putNextEntry(entry);
try(InputStream stream = mainClazz.getClassLoader().getResourceAsStream(path)) {
byte[] buffer = new byte[8192];
int c = stream.read(buffer);
while (c > -1) {
target.write(buffer, 0, c);
c = stream.read(buffer);
}
}
target.closeEntry();
}
target.close();
}
}
private void makeJarDirs(JarOutputStream target, Collection<String> dirs,
String path) throws IOException {
JarEntry entry;
String dir = path.substring(0, path.lastIndexOf('/'));
while(dir.startsWith("/"))
dir = dir.substring(1);
StringBuilder dirTmp = new StringBuilder(dir.length());
for (String d : dir.split("/")) {
dirTmp.append(d);
dirTmp.append("/");
String tmp = dirTmp.toString();
if (!dirs.contains(tmp)) {
dirs.add(tmp);
entry = new JarEntry(tmp);
target.putNextEntry(entry);
}
}
}
}