Thursday, April 10, 2008

Instant evaluation of Java code in OpenOffice

At the end of last year I had agreed to update a 4-day Java learning course to reflect the changes in Java 5 and 6. The problems with that were:


  • The course material was long- over 300 pages already
  • I know what you're thinking- that's 75 pages per freakin' day (not counting the exercises). All right, next time I'll publish a book
  • There was a deadline
  • ...tight as it usually is, considering it had to be done in off-work hours.
  • My first son was going to be born before the deadline
  • Uh-oh, now that should have made me worry.
  • The document was in Word .doc format
  • A big no-no. Maybe you have your reasons, but I have mine- I will never, ever write or maintain a significant piece of documentation like that (or insignificant, for that matter) ever again. And I mean it.


I really considered moving the contents to another format- be it LaTeX, docbook or a lightweight markup language like reStructuredText or asciidoc (a topic for another post). Now being out of time meant that I couldn't.

I should have also updated each and every single example for the new language syntax (where relevant) and test that it works. Now last I counted that was 254 snippets of code. OK, it could happen that one example was split across several snippets, but that's nonetheless tons of work- copy the code, paste it in a text editor, save it, compile it, run it, see if the results are the ones expected. If any step fails, rinse and repeat. Dirty work.

The reason I miss simple text formats is that it's so much easier to automate things. For instance, in the document you could include all your source files, which are located separately and tested automatically in one go.

But I didn't have this option. I needed to find some way to automate this. And the answer came from the very material I was going to present- Java Compiler API.

A frequently neglected feature in Java 6, Compiler API provided language libraries to control the process of compiling right in your Java code. So far you needed to save into a file and invoke a separate process to do that- really a workaround. If you've ever used eval in scripting language, you're going to miss instant evaluation sorely. With the new compiler API, well, you're still gonna miss it, but at least compiling is no longer a hack. Besides, control of the compiler now meant that your performance is going to be much better as you can even compile from a string in memory.

The idea began to form- I could define a BeanShell macro in OpenOffice. If we're using OpenOffice integrated with Java 6, then BeanShell will have access to the compiler APIs as well. The macro would compile a class from the selected text in the document (in memory) and load it (still in memory), then run it and display the results. This would certainly make testing the examples faster.

I'm usually lazy enough to first search for a similar solution (even if finding that solution takes more effort than writing it). The first source I came across about in-memory compilation was in the API documentation of JavaCompiler. It was a good start, it used SimpleJavaFileObject to read source from a String, but the class file was still compiled and saved to disk.

Along the same lines was the detailed article in JavaBeat- it showed how to compile from String using a SimpleJavaFileObject.

I knew there had to be more you need to do. The class file always appeared on disk. I was not very keen on the idea of writing a file to disk (implicitly or explicitly). It's slower, it's less secure and often more effort. I found what I was looking for in the velocityreviews forums. Bot I really struck gold with this really detailed document, which described almost exactly what I wanted to do. It's about visualizing the Java bytecode by the way.

There was one more point. I was wondering which graphical widget to use, and it turns out I could have a popup box using both the OpenOffice APIs or normal Java AWT/Swing. I chose OpenOffice, because the look and feel was better integrated- and because it was different than the Java libraries, which I already knew. I had to read a bit in the OpenOffice developer's guide, but it finally worked out.

So my final macro began to take shape. I first had to create the familiar SimpleJavaFileObject. I had to construct it with a String as an argument. The crucial point was to override the getCharContent method so it returns the class field with the String.


class JavaObjectFromString extends SimpleJavaFileObject{
private String contents = null;
public JavaObjectFromString(String className, String contents) throws Exception{
super(new URI(className + Kind.SOURCE.extension), Kind.SOURCE);
this.contents = contents;
}
public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
return contents;
}
}


What I discovered in the last two sources was that I would need to implement a file manager, preferrably by extending ForwardingJavaFileManager. However, it needs another reimplementation of SimpleJavaFileObject, but this time for the class data itself, not the source. The important thing here is to override openInputStream and openOutputStream to correct the notion of the class about where its data is located:


static class RAMJavaFileObject extends SimpleJavaFileObject {

RAMJavaFileObject(String name, Kind kind) {
super(URI.create("string:///" + name.replace('.','/') + Kind.SOURCE.extension), kind);
}

ByteArrayOutputStream baos;

public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException, IllegalStateException, UnsupportedOperationException {
throw new UnsupportedOperationException();
}

public InputStream openInputStream() throws IOException, IllegalStateException, UnsupportedOperationException {
return new ByteArrayInputStream(baos.toByteArray());
}

public OutputStream openOutputStream() throws IOException, IllegalStateException, UnsupportedOperationException {
return baos = new ByteArrayOutputStream();
}

}


Now we have everything necessary to extend on our ForwardinJavaFileManager so that it uses our implementation of in-memory class data. Note that we create a HashMap to be used later:


output = new HashMap();

class RAMFileManager extends ForwardingJavaFileManager{
RAMFileManager (JavaCompiler compiler){
super(compiler.getStandardFileManager(null,null,null));
}
public JavaFileObject getJavaFileForOutput(Location location, String name, Kind kind, FileObject sibling) throws java.io.IOException {
JavaFileObject jfo = new RAMJavaFileObject(name, kind);
output.put(name, jfo);
return jfo;
}
}


The last thing I define is a small utility method which just returns the String of the class. As you can see, I've made it convenient to select code fragments and the macro will create the necessary framework around them- a container class and a main method, if need be:


SimpleJavaFileObject getJavaFileContentsAsString(String outside, String inClass, String inMethod){
StringBuilder javaFileContents = new StringBuilder(outside +
"\npublic class TestClass{\n" +
inClass +
"\n public void testMethod() throws Throwable {\n" +
inMethod +
"try{this.getClass().getMethod(\"main\", String[].class).invoke(null, new Object[] {new String[]{}});} catch (NoSuchMethodException nsme) {}" +
"\n}\n" +
"}");
JavaObjectFromString javaFileObject = null;
try{
javaFileObject = new JavaObjectFromString("TestClass", javaFileContents.toString());
}catch(Exception exception){
exception.printStackTrace();
}
return javaFileObject;
}


Now that we have the main infrastructure in place it is time for the action to unfold. This means creating the compiler object, a new task and invoking the task. I do not really want to parse the java fragments in the document to find where they belong, so I use a trick. I assume the fragment belongs either outside of the class I'm generating (import statements, other classes), in the class definition (fields, methods) or in the main method (instructions). I try to put each selection (you haven't forgotten that you can have multiple selections in OpenOffice, right?) in one of these three areas and invoke the task to see if it compiles without error. If it does, I go ahead with the next one:


JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
JavaFileManager jfm = new RAMFileManager (compiler);

outside = inClass = inMethod = "";
for(i=0;i<count;i++) {
xTextRange = (XTextRange) UnoRuntime.queryInterface(XTextRange.class, xIndexAccess.getByIndex(i));
newText = xTextRange.getString();

JavaFileObject javaObjectFromString = getJavaFileContentsAsString(outside + newText, inClass, inMethod);
Iterable fileObjects = Arrays.asList(new Object[] {javaObjectFromString});
out = new StringWriter();
CompilationTask task = compiler.getTask(out, jfm, null, null, null, fileObjects);
Boolean result = task.call();

if(result){
outside += newText;
} else {
javaObjectFromString = getJavaFileContentsAsString(outside, inClass + newText, inMethod);
fileObjects = Arrays.asList(new Object[] {javaObjectFromString});
task = compiler.getTask(out, jfm, null, null, null, fileObjects);
result = task.call();
if (result){
inClass += newText;
} else {
javaObjectFromString = getJavaFileContentsAsString(outside, inClass, inMethod + newText);
fileObjects = Arrays.asList(new Object[] {javaObjectFromString});
task = compiler.getTask(out, jfm, null, null, null, fileObjects);
result = task.call();
if (result){
inMethod += newText;
} else {
message = "Compilation fails:\n" + out.toString();
msgtype = "errorbox";
title = "Compilation error";
showMessage();
return 0;
}
}
}
}


At this stage, we already have compiled the class, but how to execute it? We need to define a class loader which knows specifically where to find and how to define the class (note the HashMap we used to store the classes):


ClassLoader cl = new ClassLoader() {
protected Class findClass(String name) throws ClassNotFoundException {
JavaFileObject jfo = output.get(name);
if (jfo != null) {
byte[] bytes = ((RAMJavaFileObject) jfo).baos.toByteArray();
return defineClass(name, bytes, 0, bytes.length);
}
return super.findClass(name);
}

};


Now that we have the class, we can get the method and run it. Here I'm also doing some hocus-pocus in order to kidnap the standard output and show it in the message box- I am really interested in what actually is printed and if there are any exceptions:


Class c = Class.forName("TestClass", true, cl);
constructor = c.getConstructor(new Class[]{});
object = constructor.newInstance(new Object[]{});
method = c.getMethod("testMethod", new Class[]{});

sysOut = System.out;
sysErr = System.err;
// System.setOut(out);
PipedOutputStream pout = new PipedOutputStream();
BufferedReader brout = new BufferedReader(new InputStreamReader(new PipedInputStream(pout)));
System.setOut(new PrintStream(pout));

PipedOutputStream perr = new PipedOutputStream();
BufferedReader brerr = new BufferedReader(new InputStreamReader(new PipedInputStream(perr)));
System.setErr(new PrintStream(perr));

message = "Compiled and ran successfully:\n";
msgtype = "infobox";
title = "Success";

try {
method.invoke(object, new Object[]{});
} catch (Throwable t) {
message = "Runtime error:\n" + t;
msgtype = "errorbox";
title = "Runtime error";
}
sb = new StringBuilder();
while (brout.ready()) {
sb.append((char) brout.read());
}
message += (sb.length() == 0 ? "" : "\nOutput:\n") + sb.toString();

sb = new StringBuilder();
while (brerr.ready()) {
sb.append((char) brerr.read());
}
message += (sb.length() == 0 ? "" : "\nError:\n") + sb.toString();

System.setOut(sysOut);
System.setErr(sysErr);

showMessage();


And that's it! What's that you ask? We don't know anything about the showMessage method? You're really curious, aren't you? OK, here it is, mostly taken from the OpenOffice UNO interface:


showMessage() {
import com.sun.star.awt.XToolkit;
import com.sun.star.awt.XMessageBoxFactory;
import com.sun.star.awt.XMessageBox;
import com.sun.star.awt.XWindowPeer;

m_xContext = XSCRIPTCONTEXT.getComponentContext();
m_xMCF = m_xContext.getServiceManager();

Object oToolkit = m_xMCF.createInstanceWithContext("com.sun.star.awt.Toolkit", m_xContext);
XToolkit xToolkit = (XToolkit) UnoRuntime.queryInterface(XToolkit.class, oToolkit);

windowPeer = xTextDoc.currentController.frame.containerWindow;
XWindowPeer xWindowPeer = (XWindowPeer) UnoRuntime.queryInterface(XWindowPeer.class, windowPeer);

com.sun.star.awt.Rectangle aRectangle = new com.sun.star.awt.Rectangle();

XMessageBoxFactory xMessageBoxFactory = (XMessageBoxFactory) UnoRuntime.queryInterface(XMessageBoxFactory.class, oToolkit);
XMessageBox xMessageBox = xMessageBoxFactory.createMessageBox(xWindowPeer, aRectangle, msgtype, com.sun.star.awt.MessageBoxButtons.BUTTONS_OK, title, message);
if (xMessageBox != null){
short nResult = xMessageBox.execute();
}
}


What's missing is a couple of import statements and standard initializing lines found in every OpenOffice macro template, but this blog post is already too long. And it's better that you ask if you need it rather than get bored and not even get to the end.

No comments: