Friday, May 30, 2008

Write Java Application Using JavaScript

JDK 6.0 introduces the Java Scripting API and includes a Javascript scripting engine adapted from Mozilla Rhino project. With Scripting API, it is possible to write applications that end users can extend and customize. Java program can invoke scripting code and vice versa. Here I show an simple application that is primarily written in Javascript.

The application consists of two parts, one is written in Java and the other in Javascript. Here is the Java source code:

import javax.script.ScriptEngineManager;
import javax.script.ScriptEngine;
import javax.script.ScriptException;
import javax.swing.AbstractAction;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.FileReader;
import java.io.IOException;

public class test
{
public static void main(String[] args)
{
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("js");
if (engine != null) {
try {
engine.eval(new FileReader("test.js"));
} catch (IOException e) {
e.printStackTrace();
} catch (ScriptException e) {
e.printStackTrace();
}
}
}
}

class JSAction extends AbstractAction
{
private ActionListener actionListener;
public JSAction(ActionListener listener, String name)
{
super(name);
actionListener = listener;
}

public void actionPerformed(ActionEvent e)
{
actionListener.actionPerformed(e);
}
}

The code in main is straitforward. It simply find the Javascript scripting engine and run the script. I will explain the purpose JSAction later. The following the Javascript code:

importPackage(java.lang);
importClass(java.awt.event.ActionListener);
importClass(java.awt.BorderLayout);
importClass(javax.swing.JFrame);
importClass(javax.swing.JTextArea);
importClass(javax.swing.JMenuBar);
importClass(javax.swing.JMenu);
importClass(javax.swing.text.DefaultEditorKit);
importClass(Packages.JSAction);

frame = new JFrame;
frame.setSize(320, 240);

text = new JTextArea;
text.setLineWrap(true);

content = frame.getContentPane();
content.add(text, BorderLayout.CENTER);

menubar = new JMenuBar();
file = new JMenu("File");

method = {
actionPerformed: function() { System.exit(0); }
}
exitAction = ActionListener(method);
// JSAction extends AbstractAction. See Java code above.
file.add(new JSAction(exitAction, "Exit"));

edit = new JMenu("Edit");
edit.add(text.getActionMap().get(DefaultEditorKit.cutAction));
edit.add(text.getActionMap().get(DefaultEditorKit.copyAction));
edit.add(text.getActionMap().get(DefaultEditorKit.pasteAction));

menubar.add(file);
menubar.add(edit);
frame.setJMenuBar(menubar);

frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);

The importPackage function imports a package, while the importClass function imports a Java class. To import an non-JDK class, prefix it with Packages, e.g., Packages.JSAction. As one can see from the example, calling Java APIs from JavaScript is quite easy. The only tricky part is to implement a Java interface or extend a Java class in JavaScript. While Rhino engine allows the JavaScript to implement interface and extend class, JDK 6 JavaScript engine only allows the implementation of a single interface using the syntax:

InterfaceName(MethodMap)

The MethodMap is a JavaScript associate array, with the method names of the interface as the keys, and the function definitions as values. The above JavaScript code shows how to implement an ActionListener interface.

The problem here is that we need to add an extended class of AbstractAction to the file menu with our own actionPerformed method to exit the application. However, it is impossible to do so in JDK 6 scripting engine. The hack here is to implement a Java class JSAction that extends AbstractAction, with the actionPerformed method delegated to an instance of ActionListener implemented by JavaScript. According to Sun, the need to extend a class or implement multiple interface is very rare, which I do not necessary agree. I hope this restriction can be relieved in the future release.

No comments: