package org.reldb.rel.v0.external;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.util.Enumeration;
import org.eclipse.jdt.core.compiler.CompilationProgress;
import org.reldb.rel.exceptions.*;
import org.reldb.rel.v0.generator.*;
import org.reldb.rel.v0.storage.RelDatabase;
import org.reldb.rel.v0.types.*;
import org.reldb.rel.v0.types.builtin.TypeBoolean;
import org.reldb.rel.v0.types.builtin.TypeCharacter;
import org.reldb.rel.v0.types.builtin.TypeInteger;
import org.reldb.rel.v0.types.builtin.TypeRational;
import org.reldb.rel.v0.version.Version;
/**
* @author Dave
*
*/
public class ForeignCompilerJava {
private Generator generator;
private boolean verbose;
public ForeignCompilerJava(Generator generator, boolean verbose) {
this.generator = generator;
this.verbose = verbose;
}
private static final String MANIFEST = "META-INF/MANIFEST.MF";
private static String relCoreJar = null;
// This bit of hackery is used to get a Rel core jar file so we can compile user Java code under a Web Start environment.
// From https://weblogs.java.net/blog/2005/05/27/using-java-compiler-your-web-start-application
private synchronized String getLocalWebStartRelJarName() {
if (relCoreJar != null)
return relCoreJar;
try {
for (Enumeration<?> e = getClass().getClassLoader().getResources(MANIFEST); e.hasMoreElements();) {
URL url = (URL) e.nextElement();
if (url.getFile().contains(Version.getCoreJarFilename())) {
String relCoreJarName = getLocalJarFilename(url);
if (relCoreJarName != null) {
relCoreJar = relCoreJarName;
return relCoreJar;
}
}
}
} catch (IOException exc) {
exc.printStackTrace();
}
return null;
}
private static String getLocalJarFilename(URL remoteManifestFileName) {
// remove trailing
String urlStrManifest = remoteManifestFileName.getFile();
String urlStrJar = urlStrManifest.substring(0, urlStrManifest.length() - MANIFEST.length() - 2);
InputStream inputStreamJar = null;
File tempJar;
FileOutputStream fosJar = null;
try {
URL urlJar = new URL(urlStrJar);
inputStreamJar = urlJar.openStream();
String strippedName = urlStrJar;
int dotIndex = strippedName.lastIndexOf('.');
if (dotIndex >= 0) {
strippedName = strippedName.substring(0, dotIndex);
strippedName = strippedName.replace("/", File.separator);
strippedName = strippedName.replace("\\", File.separator);
int slashIndex = strippedName.lastIndexOf(File.separator);
if (slashIndex >= 0) {
strippedName = strippedName.substring(slashIndex + 1);
}
}
tempJar = File.createTempFile(strippedName, ".jar");
tempJar.deleteOnExit();
fosJar = new FileOutputStream(tempJar);
byte[] ba = new byte[1024];
while (true) {
int bytesRead = inputStreamJar.read(ba);
if (bytesRead < 0) {
break;
}
fosJar.write(ba, 0, bytesRead);
}
return tempJar.getAbsolutePath();
} catch (Exception ioe) {
System.out.println(ioe.getMessage());
ioe.printStackTrace();
} finally {
try {
if (inputStreamJar != null) {
inputStreamJar.close();
}
} catch (IOException ioe) {}
try {
if (fosJar != null) {
fosJar.close();
}
} catch (IOException ioe) {}
}
return null;
}
/** Return the package to which this entire Rel core belongs. */
private String getPackagePrefix() {
String thisPackageName = getClass().getPackage().getName();
String relPackageName = thisPackageName.replace(".external", "");
return relPackageName;
}
/** Return a classpath cleaned of non-existent files and Web Start's deploy.jar.
* Classpath elements with spaces are converted to quote-delimited strings. */
private final static String cleanClassPath(String s) {
if (java.io.File.separatorChar == '/')
s = s.replace('\\', '/');
else
s = s.replace('/', '\\');
String outstr = "";
java.util.StringTokenizer st = new java.util.StringTokenizer(s, java.io.File.pathSeparator);
while (st.hasMoreElements()) {
String element = (String)st.nextElement();
java.io.File f = new java.io.File(element);
if (f.exists() && !element.contains("deploy.jar")) {
String fname = f.toString();
if (fname.indexOf(' ')>=0)
fname = '"' + fname + '"';
outstr += ((outstr.length()>0) ? java.io.File.pathSeparator : "") + fname;
}
}
return outstr;
}
/** Return classpath to the Rel core. */
private String getLocalClasspath(RelDatabase database) {
String classPath = System.getProperty("user.dir") +
java.io.File.pathSeparatorChar + Version.getCoreJarFilename() +
java.io.File.pathSeparatorChar + database.getJavaUserSourcePath() +
java.io.File.pathSeparatorChar + database.getHomeDir();
if (database.getAdditionalJarsForJavaCompilerClasspath() != null)
for (String path: database.getAdditionalJarsForJavaCompilerClasspath()) {
notify("ForeignCompilerJava: extra jar for classpath: " + path);
classPath += java.io.File.pathSeparator + path;
}
else
notify("ForeignCompilerJava: no extra jars for classpath");
notify("ForeignCompilerJava: raw classpath is " + classPath);
return classPath;
}
/** Given a Type, return the name of the equivalent Java type. */
private final static String getJavaTypeForType(Generator generator, Type t) {
if (t == null)
return "void";
else
return t.getValueClassname(generator);
}
/** Given a Type, return a Java expression to pop a Value from the operand
* stack and convert it to an equivalent Java type. */
private final static String getJavaPopForType(Generator generator, Type t) {
if (t == null)
throw new ExceptionFatal("RS0292: Got null type in getJavaPopForType()");
return "(" + getJavaTypeForType(generator, t) + ")context.pop()";
}
/** Given an operator signature, return a Java method parameter definition. */
private final static String getJavaMethodParmsForParameters(Generator generator, OperatorSignature os) {
String s = "Context context";
for (int i=0; i<os.getParmCount(); i++)
s += ", " + getJavaTypeForType(generator, os.getParameterType(i)) + " " + os.getParameterName(i);
return s;
}
/** Compile foreign code using Eclipse JDT compiler. */
private void compileForeignCode(RelDatabase database, PrintStream stream, String className, String src) {
ByteArrayOutputStream messageStream = new ByteArrayOutputStream();
ByteArrayOutputStream warningStream = new ByteArrayOutputStream();
String warningSetting = new String("allDeprecation,"
+ "allJavadoc," + "assertIdentifier," + "charConcat,"
+ "conditionAssign," + "constructorName," + "deprecation,"
+ "emptyBlock," + "fieldHiding," + "finalBound,"
+ "finally," + "indirectStatic," + "intfNonInherited,"
+ "javadoc," + "localHiding," + "maskedCatchBlocks,"
+ "noEffectAssign," + "pkgDefaultMethod," + "serial,"
+ "semicolon," + "specialParamHiding," + "staticReceiver,"
+ "syntheticAccess," + "unqualifiedField,"
+ "unnecessaryElse," + "uselessTypeCheck," + "unsafe,"
+ "unusedArgument," + "unusedImport," + "unusedLocal,"
+ "unusedPrivate," + "unusedThrown");
String classpath =
cleanClassPath(System.getProperty("java.class.path")) +
java.io.File.pathSeparatorChar +
cleanClassPath(getLocalClasspath(database));
String webclasspath = getLocalWebStartRelJarName();
if (webclasspath != null)
classpath += File.pathSeparatorChar + webclasspath;
// If resource directory doesn't exist, create it.
File resourceDir = new File(database.getJavaUserSourcePath());
if (!(resourceDir.exists()))
resourceDir.mkdirs();
File sourcef;
try {
// Write source to a Java source file
sourcef = new File(database.getJavaUserSourcePath() + java.io.File.separator + getStrippedClassname(className) + ".java");
PrintStream sourcePS = new PrintStream(new FileOutputStream(sourcef));
sourcePS.print(src);
sourcePS.close();
} catch (IOException ioe) {
throw new ExceptionFatal("RS0293: Unable to save Java source: " + ioe.toString());
}
notify("ForeignCompilerJava: classpath = " + classpath);
// Start compilation using JDT
String commandLine = "-1.8 -source 1.8 -warn:" +
warningSetting + " " +
"-cp " + classpath + " \"" + sourcef + "\"";
boolean compiled = org.eclipse.jdt.core.compiler.batch.BatchCompiler.compile(
commandLine,
new PrintWriter(messageStream), new PrintWriter(warningStream),
new CompilationProgress() {
@Override
public void begin(int arg0) {
}
@Override
public void done() {
}
@Override
public boolean isCanceled() {
return false;
}
@Override
public void setTaskName(String arg0) {
ForeignCompilerJava.this.notify(arg0);
}
@Override
public void worked(int arg0, int arg1) {
}
}
);
String compilerMessages = "";
// Parse the messages and the warnings.
BufferedReader br = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(messageStream.toByteArray())));
while (true) {
String str = null;
try {
str = br.readLine();
} catch (IOException e) {
e.printStackTrace();
}
if (str == null) {
break;
}
compilerMessages += str + '\n';
}
BufferedReader brWarnings = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(warningStream.toByteArray())));
while (true) {
String str = null;
try {
str = brWarnings.readLine();
} catch (IOException e) {
e.printStackTrace();
}
if (str == null) {
break;
}
compilerMessages += str + '\n';
}
if (!compiled)
throw new ExceptionSemantic("RS0005: Compilation failed due to errors: \n" + compilerMessages + "\n");
}
/** Get a stripped name. Only return text after the final '.' */
private static String getStrippedName(String name) {
int lastDot = name.lastIndexOf('.');
if (lastDot >= 0)
return name.substring(lastDot + 1);
else
return name;
}
/** Get stripped Java Class name. */
private static String getStrippedClassname(String name) {
return getStrippedName(name);
}
private Type getTypeForClassName(Generator generator, String className) {
if (className.equals(getPackagePrefix() + ".values.ValueBoolean"))
return TypeBoolean.getInstance();
else if (className.equals(getPackagePrefix() + ".values.ValueCharacter"))
return TypeCharacter.getInstance();
else if (className.equals(getPackagePrefix() + ".values.ValueInteger"))
return TypeInteger.getInstance();
else if (className.equals(getPackagePrefix() + ".values.ValueRational"))
return TypeRational.getInstance();
else if (className.equals(getPackagePrefix() + ".values.ValueObject"))
return TypeInteger.getInstance();
else if (className.equals(getPackagePrefix() + ".values.ValueOperator"))
return TypeOperator.getInstance();
else
return generator.locateType(getStrippedClassname(className));
}
private void notify(String s) {
if (verbose)
System.out.println(s);
}
/** Given a Java class, return the Type for which it is a Value, if there is one.
* Return null if there isn't one. */
private Type getTypeForClass(Class<?> c) {
return getTypeForClassName(generator, c.getName());
}
/** Given a class, and one of its constructors, define a selector operator. */
private void addSelectorForClass(Class<?> c, java.lang.reflect.Constructor<?> con) {
String name = getTypeForClass(c).getValueClassname(generator);
OperatorSignature sig = new OperatorSignature(name);
Class<?>[] rawParameters = con.getParameterTypes();
if (rawParameters.length == 0)
return;
sig.setReturnType(getTypeForClass(c));
if (sig.getReturnType() == null)
throw new ExceptionSemantic("RS0006: Unable to create selector for TYPE " + c.getName());
String arguments = "context.getGenerator()";
if (!rawParameters[0].toString().equals("class " + Generator.class.getCanonicalName())) {
notify("x Constructor " + con + " of " + c + " not implemented as OPERATOR due to missing first parameter of type " + Generator.class.getCanonicalName());
return;
}
for (int i=1; i<rawParameters.length; i++) {
if (rawParameters[i].isPrimitive()) {
notify("x Constructor " + con + " of " + c + " not implemented as selector due to primitive parameter type.");
return;
}
Type t = getTypeForClass(rawParameters[i]);
if (t == null) {
notify("x Constructor " + con + " of " + c + " not implemented as selector due to unrecognised parameter type " + rawParameters[i].toString());
return; // type not found -- not implementable
}
String parmName = "p" + i;
sig.addParameter("p" + i, t);
arguments += ((arguments.length() > 0) ? ", " : "") + parmName;
}
String language = "Java";
String typeName = getStrippedClassname(c.getName());
String src = "return new " + typeName + "(" + arguments + ");";
notify("* Constructor " + con + " of " + c + " implemented as selector OPERATOR " + sig);
OperatorDefinition newOp = compileForeignOperator(sig, language, src);
newOp.setCreatedByType(typeName);
newOp.getReferences().addReferenceToType(typeName);
generator.addOperator(newOp);
}
/** Given a class, and one of its methods, define an operator. */
private void addOperatorForClass(Class<?> c, java.lang.reflect.Method m) {
String name;
if (Modifier.isStatic(m.getModifiers()))
name = m.getName();
else
name = "THE_" + m.getName();
OperatorSignature sig = new OperatorSignature(name);
Class<?>[] rawParameters = m.getParameterTypes();
Class<?> rawReturnType = m.getReturnType();
if (rawReturnType.isPrimitive()) {
notify("x Method " + m + " of " + c + " not implemented as OPERATOR due to primitive return type.");
return;
}
Type returnType = getTypeForClass(rawReturnType);
if (returnType == null) {
notify("x Method " + m + " of " + c + " not implemented as OPERATOR due to unrecognised return type.");
return;
}
sig.setReturnType(returnType);
Type instanceType = getTypeForClass(c);
if (instanceType == null)
throw new ExceptionSemantic("RS0007: Java type " + c + " not recognised.");
String arguments = "context.getGenerator()";
if (!Modifier.isStatic(m.getModifiers()))
sig.addParameter("px", getTypeForClass(c));
else if (!rawParameters[0].toString().equals("class " + Generator.class.getCanonicalName())) {
notify("x Method " + m + " of " + c + " not implemented as OPERATOR due to missing first parameter of type " + Generator.class.getCanonicalName());
return;
}
for (int i=1; i<rawParameters.length; i++) {
String parmName = "p" + i;
if (rawParameters[i].isPrimitive()) {
notify("x Method " + m + " of " + c + " not implemented as OPERATOR due to primitive parameter type.");
return;
}
Type t = getTypeForClass(rawParameters[i]);
if (t == null) {
notify("x Method " + m + " of " + c + " not implemented as OPERATOR due to unrecognised parameter type " + rawParameters[i].toString());
return; // type not found -- not implementable
}
sig.addParameter(parmName, t);
arguments += ((arguments.length() > 0) ? ", " : "") + parmName;
}
String language = "Java";
String src;
String typeName = getStrippedClassname(c.getName());
String returnStr = (returnType == null) ? "" : "return ";
if (Modifier.isStatic(m.getModifiers()))
src = returnStr + typeName + "." + m.getName() + "(" + arguments + ");";
else
src = returnStr + "px." + m.getName() + "(" + arguments + ");";
notify("* Method " + m + " of " + c + " implemented as OPERATOR " + sig);
OperatorDefinition newOp = compileForeignOperator(sig, language, src);
newOp.setCreatedByType(typeName);
newOp.getReferences().addReferenceToType(typeName);
generator.addOperator(newOp);
}
/** Given a Java Class, generate Rel Operators to access it. */
private void addOperatorsForClass(Class<?> c) {
try {
for (java.lang.reflect.Constructor<?> constructor: c.getDeclaredConstructors())
addSelectorForClass(c, constructor);
for (java.lang.reflect.Method method: c.getDeclaredMethods())
addOperatorForClass(c, method);
} catch (Throwable t) {
throw new ExceptionSemantic("RS0008: Unable to add operators for " + c.getName() + ": " + t.toString());
}
}
/** Compile a user-defined Java-based type. */
public void compileForeignType(String name, String language, String src) {
if (!language.equals("Java"))
throw new ExceptionSemantic("RS0009: " + language + " is not recognised as a foreign language.");
String body = src.replace("\n", "\n\t");
src = "package " + RelDatabase.getRelUserCodePackage() + ";\n\n" +
"import " + getPackagePrefix() + ".vm.*;\n" +
"import " + getPackagePrefix() + ".types.*;\n" +
"import " + getPackagePrefix() + ".types.builtin.*;\n" +
"import " + getPackagePrefix() + ".types.userdefined.*;\n" +
"import " + getPackagePrefix() + ".values.*;\n" +
"import " + getPackagePrefix() + ".generator.Generator;\n\n" +
"public class " + name + " extends ValueTypeJava {\n" +
body +
"\n}";
compileForeignCode(generator.getDatabase(), generator.getPrintStream(), name, src);
try {
Class<?> typeClass = generator.getDatabase().loadClass(name);
if (typeClass == null)
throw new ExceptionSemantic("RS0010: Despite having been compiled, " + name + " could not be loaded.");
notify("> Java class " + typeClass.getName() + " loaded to implement type " + name);
Constructor<?> ctor = typeClass.getConstructor(new Class[] {Generator.class});
if (ctor == null)
throw new ExceptionSemantic("RS0011: Unable to find a constructor of the form " + name + "(Generator generator)");
generator.addTypeInProgress(name, (Type)ctor.newInstance(generator));
addOperatorsForClass(typeClass);
} catch (Throwable thrown) {
throw new ExceptionSemantic("RS0012: Creation of type " + name + " failed: " + thrown.toString());
}
}
/** Compilation of foreign operator. */
public OperatorDefinition compileForeignOperator(OperatorSignature signature, String language, String src) {
if (!language.equals("Java"))
throw new ExceptionSemantic("RS0013: " + language + " is not recognised as a foreign language.");
// Modify src here to include class definition and appropriate methods
// One of these must be 'public static void execute(Session s)'
String comp =
"package " + RelDatabase.getRelUserCodePackage() + ";\n\n" +
"import " + getPackagePrefix() + ".types.*;\n" +
"import " + getPackagePrefix() + ".types.builtin.*;\n" +
"import " + getPackagePrefix() + ".types.userdefined.*;\n" +
"import " + getPackagePrefix() + ".values.*;\n" +
"import " + getPackagePrefix() + ".vm.Context;\n\n" +
"public class " + getStrippedClassname(signature.getClassSignature()) + " {\n" +
"\tprivate static final " + getJavaTypeForType(generator, signature.getReturnType()) + " " +
"do_" + getStrippedName(signature.getName()) + "(" + getJavaMethodParmsForParameters(generator, signature) + ") {\n" +
"\t\t" + src.replace("\n", "\n\t\t") + "\n\t}\n\n" +
"\tpublic static final void execute(Context context) {\n";
String args = "context";
Type[] parmTypes = signature.getParameterTypes();
for (int i=parmTypes.length - 1; i >= 0; i--) {
comp += "\t\t" + getJavaTypeForType(generator, parmTypes[i]) + " " + "p" + i + " = " + getJavaPopForType(generator, parmTypes[i]) + ";\n";
args += ", p" + (parmTypes.length - i - 1);
}
if (signature.getReturnType() != null)
comp += "\t\tcontext.push(" + "do_" + getStrippedName(signature.getName()) + "(" + args + "));\n";
else
comp += "\t" + "do_" + getStrippedName(signature.getName()) + "(" + args + ");\n";
comp += "\t}\n}";
// compile source
compileForeignCode(generator.getDatabase(), generator.getPrintStream(), signature.getClassSignature(), comp);
notify("> Java class " + signature.getClassSignature() + " compiled to implement OPERATOR " + signature);
// construct operator definition
OperatorDefinitionNative o;
Method method = generator.getDatabase().getMethod(signature);
if (signature.getReturnType() != null)
o = new OperatorDefinitionNativeFunctionExternal(signature, method);
else
o = new OperatorDefinitionNativeProcedureExternal(signature, method);
o.setSourceCode(signature.getOperatorDeclaration() + " " + language + " FOREIGN " + src + "\nEND OPERATOR;");
return o;
}
}