diff --git a/beehive-jdbc-mapper/src/main/java/com/moparisthebest/classgen/Compiler.java b/beehive-jdbc-mapper/src/main/java/com/moparisthebest/classgen/Compiler.java new file mode 100644 index 0000000..eda2988 --- /dev/null +++ b/beehive-jdbc-mapper/src/main/java/com/moparisthebest/classgen/Compiler.java @@ -0,0 +1,175 @@ +package com.moparisthebest.classgen; + + +import javax.tools.*; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * This let's you take compile and then run Java source code at runtime, this class only allows you to compile 1 class + * at a time. + *

+ * This is not thread safe, though it could be extended and made thread safe, or locked around. + * + * @author moparisthebest + * @see MultiCompiler for compiling multiple classes at once + *

+ * The original idea was taken from: + * http://mindprod.com/jgloss/javacompiler.html#SAMPLECODE + */ +public class Compiler { + + private final JavaCompiler compiler; + private final MemoryJavaFileManager mjfm; + //private final ClassLoader classLoader; + private final List singleton = Arrays.asList(new JavaFileObject[1]); + + public Compiler() { + this(false); + } + + protected Compiler(final boolean multi) { + compiler = ToolProvider.getSystemJavaCompiler(); + if (compiler == null) + throw new RuntimeException("tools.jar needs to be on classpath to compile code at runtime"); + final JavaFileManager fileManager = compiler.getStandardFileManager(null, null, null); + mjfm = multi ? new MultiMemoryJavaFileManager(fileManager) : new SingleMemoryJavaFileManager(fileManager); + //classLoader = new MemoryClassLoader(mjfm); + } + + protected ClassLoader compile(final Iterable source) { + //final MemoryJavaFileManager mjfm = new MemoryJavaFileManager(compiler.getStandardFileManager(null, null, null)); + final JavaCompiler.CompilationTask task = compiler.getTask(null, mjfm, null, null, null, source); + return task.call() ? + new MemoryClassLoader(mjfm) + //classLoader + : null; + } + + protected ClassLoader compile(final JavaFileObject... source) { + return compile(Arrays.asList(source)); + } + + public ClassLoader compile(final JavaFileObject source) { + singleton.set(0, source); + return compile(singleton); + } + + protected T compile(final String className, final Iterable source) { + // compile item + final ClassLoader cl = compile(source); + if (cl == null) + throw new RuntimeException("Error compiling class, aborting..."); + return instantiate(cl, className); + } + + protected T compile(final String className, final JavaFileObject... source) { + return compile(className, Arrays.asList(source)); + } + + public T compile(final String className, final JavaFileObject source) { + singleton.set(0, source); + return compile(className, singleton); + } + + public T compile(final String className, final CharSequence code) { + return compile(className, new StringJavaFileObject(className, code)); + } + + public T instantiate(final ClassLoader cl, final String className) { + try { + // Load class and create an instance. + final Class calcClass = cl.loadClass(className); + @SuppressWarnings("unchecked") final T ret = (T) calcClass.newInstance(); + return ret; + } catch (Exception e) { + throw new RuntimeException("Error finding or instantiating class, exiting...", e); + } + } + +} + +class MemoryJavaFileObject extends ForwardingJavaFileObject { + + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + MemoryJavaFileObject(JavaFileObject fileObject) { + super(fileObject); + } + + @Override + public OutputStream openOutputStream() throws IOException { + return baos; + } +} + +interface MemoryJavaFileManager extends JavaFileManager { + MemoryJavaFileObject getMemoryJavaFileObject(final String name); +} + +class SingleMemoryJavaFileManager extends ForwardingJavaFileManager implements MemoryJavaFileManager { + + private MemoryJavaFileObject classes = null; + + SingleMemoryJavaFileManager(JavaFileManager fileManager) { + super(fileManager); + } + + @Override + public MemoryJavaFileObject getMemoryJavaFileObject(final String name) { + final MemoryJavaFileObject ret = classes; + classes = null; + return ret; + } + + @Override + public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException { + final MemoryJavaFileObject jfo = new MemoryJavaFileObject(super.getJavaFileForOutput(location, className, kind, sibling)); + //System.out.printf("MemoryJavaFileManager.getJavaFileForOutput(%s, %s, %s, %s): %s\n", location, className, kind, sibling, jfo); + classes = jfo; + return jfo; + } +} + +class MultiMemoryJavaFileManager extends ForwardingJavaFileManager implements MemoryJavaFileManager { + + private Map classes = new HashMap(); + + MultiMemoryJavaFileManager(JavaFileManager fileManager) { + super(fileManager); + } + + @Override + public MemoryJavaFileObject getMemoryJavaFileObject(final String name) { + return classes.remove(name); + } + + @Override + public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException { + final MemoryJavaFileObject jfo = new MemoryJavaFileObject(super.getJavaFileForOutput(location, className, kind, sibling)); + //System.out.printf("MemoryJavaFileManager.getJavaFileForOutput(%s, %s, %s, %s): %s\n", location, className, kind, sibling, jfo); + classes.put(className, jfo); + return jfo; + } +} + +class MemoryClassLoader extends ClassLoader { + final MemoryJavaFileManager jfm; + + MemoryClassLoader(MemoryJavaFileManager jfm) { + this.jfm = jfm; + } + + public Class findClass(String name) throws ClassNotFoundException { + final MemoryJavaFileObject jfo = jfm.getMemoryJavaFileObject(name); + if (jfo == null) + throw new ClassNotFoundException("Class '" + name + "' cannot be found in the MemoryJavaFileManager"); + final byte[] b = jfo.baos.toByteArray(); + return defineClass(name, b, 0, b.length); + } +} diff --git a/beehive-jdbc-mapper/src/main/java/com/moparisthebest/classgen/MultiCompiler.java b/beehive-jdbc-mapper/src/main/java/com/moparisthebest/classgen/MultiCompiler.java new file mode 100644 index 0000000..be555f3 --- /dev/null +++ b/beehive-jdbc-mapper/src/main/java/com/moparisthebest/classgen/MultiCompiler.java @@ -0,0 +1,35 @@ +package com.moparisthebest.classgen; + +import javax.tools.JavaFileObject; + +/** + * This lets you compile multiple source files at once + */ +public class MultiCompiler extends Compiler { + + public static Compiler instance = new MultiCompiler(); + + public MultiCompiler() { + super(true); + } + + @Override + public ClassLoader compile(final Iterable source) { + return super.compile(source); + } + + @Override + public ClassLoader compile(final JavaFileObject... source) { + return super.compile(source); + } + + @Override + public T compile(final String className, final JavaFileObject... source) { + return super.compile(className, source); + } + + @Override + public T compile(final String className, final Iterable source) { + return super.compile(className, source); + } +} diff --git a/beehive-jdbc-mapper/src/main/java/com/moparisthebest/classgen/StringJavaFileObject.java b/beehive-jdbc-mapper/src/main/java/com/moparisthebest/classgen/StringJavaFileObject.java new file mode 100644 index 0000000..946effb --- /dev/null +++ b/beehive-jdbc-mapper/src/main/java/com/moparisthebest/classgen/StringJavaFileObject.java @@ -0,0 +1,22 @@ +package com.moparisthebest.classgen; + +import javax.tools.SimpleJavaFileObject; +import java.net.URI; + +/** + * For sending java source as strings to a Compiler + * @see Compiler + */ +public class StringJavaFileObject extends SimpleJavaFileObject { + private final CharSequence code; + + public StringJavaFileObject(final String name, final CharSequence code) { + super(URI.create("string:///" + name.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE); + this.code = code; + } + + @Override + public CharSequence getCharContent(boolean ignoreEncodingErrors) { + return code; + } +} diff --git a/beehive-jdbc-mapper/src/test/java/com/moparisthebest/classgentest/Calculator.java b/beehive-jdbc-mapper/src/test/java/com/moparisthebest/classgentest/Calculator.java new file mode 100644 index 0000000..40431d2 --- /dev/null +++ b/beehive-jdbc-mapper/src/test/java/com/moparisthebest/classgentest/Calculator.java @@ -0,0 +1,10 @@ +package com.moparisthebest.classgentest; + +/** + * Created by mopar on 5/15/17. + */ +public interface Calculator { + public double calc(double a, double b); + + public java.util.Date whenGenerated(); +} diff --git a/beehive-jdbc-mapper/src/test/java/com/moparisthebest/classgentest/CalculatorCompiler.java b/beehive-jdbc-mapper/src/test/java/com/moparisthebest/classgentest/CalculatorCompiler.java new file mode 100644 index 0000000..df9fd0a --- /dev/null +++ b/beehive-jdbc-mapper/src/test/java/com/moparisthebest/classgentest/CalculatorCompiler.java @@ -0,0 +1,126 @@ +package com.moparisthebest.classgentest; + +import com.moparisthebest.classgen.Compiler; +import com.moparisthebest.classgen.MultiCompiler; +import com.moparisthebest.classgen.StringJavaFileObject; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * This is a sample program that shows how to generate java source code, compile, and run it, all at runtime and + * without ever touching the filesystem. It can be used for much more complicated (and usefull) things, perhaps + * like creating Pojo instances from ResultSets. :) + *

+ * You need tools.jar on your classpath, and this class in the same package as well: + * public interface Calculator { + * public double calc(double a, double b); + * public java.util.Date whenGenerated(); + * } + *

+ * The original idea was taken from: + * http://mindprod.com/jgloss/javacompiler.html#SAMPLECODE + * + * @author moparisthebest + */ +public class CalculatorCompiler { + + @Test + public void testSingle() throws InterruptedException { + final Compiler compiler = new Compiler(); + final Calculator mult1 = genCalc(compiler, "Multiply", "a * b"); + final Calculator hyp = genCalc(compiler, "Hypotenuse", "Math.sqrt( a*a + b*b )"); + final Calculator mult2 = genCalc(compiler, "Multiply", "a * b * 2"); + final Calculator mult3 = genCalc(compiler, "Multiply", "a * b * 3"); + final Calculator mult4 = genCalc(compiler, "Multiply", "a * b * 4"); + assertEquals(12.0, mult1.calc(3.0, 4.0), 0.01); + assertEquals(5.0, hyp.calc(3.0, 4.0), 0.01); + assertEquals(24.0, mult2.calc(3.0, 4.0), 0.01); + assertEquals(48.0, mult3.calc(4.0, 4.0), 0.01); + assertEquals(48.0, mult4.calc(3.0, 4.0), 0.01); + } + + @Test + public void testMulti() throws InterruptedException { + final MultiCompiler compiler = new MultiCompiler(); + final Calculator mult1 = genCalc(compiler, "Multiply", "a * Var.var", "Var", "4.0"); + final Calculator hyp = genCalc(compiler, "Hypotenuse", "Math.sqrt( a*a + Var.var*Var.var )", "Var", "4.0"); + final Calculator mult2 = genCalc(compiler, "Multiply2", "a * Var.var * 2", "Var", "4.0"); + final Calculator mult3 = genCalc(compiler, "Multiply3", "a * Var.var * 3", "Var", "4.0"); + final Calculator mult4 = genCalc(compiler, "Multiply", "a * Var.var * 4", "Var", "4.0"); + assertEquals(mult1.calc(3.0, 4.0), 12.0, 0.01); + assertEquals(hyp.calc(3.0, 4.0), 5.0, 0.01); + assertEquals(mult2.calc(3.0, 4.0), 24.0, 0.01); + assertEquals(mult3.calc(4.0, 4.0), 48.0, 0.01); + assertEquals(48.0, mult4.calc(3.0, 4.0), 0.01); + } + + private static String writeCalculator(String className, String expression) { + String packageName = null; + final int lastIndex = className.lastIndexOf("."); + if (lastIndex != -1) { + packageName = className.substring(0, lastIndex); + className = className.substring(lastIndex + 1); + } + + return (packageName == null ? "" : "package " + packageName + ";\n") + + "import java.util.Date;\n" + + "public final class " + className + " implements " + Calculator.class.getName() + " {\n" + + " public double calc(double a, double b) {\n" + + " return " + expression + ";\n" + + " }\n" + + " public Date whenGenerated() {\n" + + " return new Date(" + System.currentTimeMillis() + "L);\n" + + " }\n" + + "}\n"; + } + + private static StringJavaFileObject writeCalculatorOb(String className, String expression) { + return new StringJavaFileObject(className, writeCalculator(className, expression)); + } + + private static StringJavaFileObject writeVar(String className, String expression) { + String packageName = null; + final int lastIndex = className.lastIndexOf("."); + if (lastIndex != -1) { + packageName = className.substring(0, lastIndex); + className = className.substring(lastIndex + 1); + } + + return new StringJavaFileObject(className, (packageName == null ? "" : "package " + packageName + ";\n") + + "public final class " + className + " {\n" + + " public static final double var = " + expression + ";\n" + + "}\n"); + } + + public static Calculator genCalc(final Compiler compiler, final String className, final String expression) { + // compose text of Java program on the fly. + final String calc = writeCalculator(className, expression); + /* + System.out.println("calc:"); + System.out.println(calc); + */ + return compiler.compile(className, calc); + } + + public static Calculator genCalc(final MultiCompiler compiler, final String className, final String expression, final String varClass, final String varExpression) { + // compose text of Java program on the fly. + final StringJavaFileObject calc = writeCalculatorOb(className, expression); + final StringJavaFileObject var = writeVar(varClass, varExpression); + /* + System.out.println("calc:"); + System.out.println(calc.getCharContent(true)); + System.out.println("var:"); + System.out.println(var.getCharContent(true)); + */ + return compiler.compile(className, calc, var); + } + + public static double runCalc(final String className, final String expression, final double a, final double b) { + final Calculator calc = genCalc(new Compiler(), className, expression); + double ret = calc.calc(a, b); + System.out.printf("%s.calc( %f, %f ) is : %f\n", className, a, b, ret); + System.out.printf("%s generated on: %s\n", className, calc.whenGenerated()); + return ret; + } +} diff --git a/pom.xml b/pom.xml index 1d37dfa..e3829f7 100644 --- a/pom.xml +++ b/pom.xml @@ -241,8 +241,8 @@ maven-compiler-plugin 3.1 - 1.5 - 1.5 + 1.6 + 1.6 true