Calling Java from JavaScript

Prerequisites

Since Sun's Java 6u12 and Firefox 3.6, the old buggy LiveConnect implementation has been replaced by a more reliable one. So if you do not want to run into weird bugs, you should use at least Firefox 3.6b5 and java 6u17 together with its java plugin2 for Firefox. It can be that older versions work as well, but Gemse has not been tested with them.

If you use Debian Lenny (which is the stable version at the time of writing), you can just install the sun-java6-bin, sun-java6-jre, sun-java6-jdk, sun-java6-plugin from Debian unstable or Debian testing, since Debian Lenny already meets all dependencies. Also, it is easy to compile Firefox 3.6, since all required libraries can be found in some Debian package for Debian Lenny.

LiveConnect in general

LiveConnect is a technology that allows you to call Java code from JavaScript and vice versa. It is available in Firefox and Sun's Java plugin since a long time. However it had some critical bugs, like JavaScript not being able to catch exceptions thrown in Java. In Firefox 3.6 and Java 6u12 a new implementation of LiveConnect has been introduced that should fix most of the long standing bugs. This new implementation handles most of the LiveConnect functionality on the Java side.

LiveConnect is supposed to work across many browsers, although most of them probably don't implement it. Roughly it works like this: You include in your XHTML page an applet. Let's say, its id attribute is set to app. Now you can access the instance of you applet from JavaScript using the global object app. This object also allows you to access any class, not only the ones related with your applet. As an effect, you can also use a dummy applet which is just a subclass of the applet class an doesn't do anything.

Note that before the new implementation, it was possible to access Java classes in java.* via the global JavaScript object java and any Java class vio the global Packages. This is no longer posible for various reasons, you have to use an applet in any case. As an exception, it is still possible in JavaScript code in or linked from XUL code inside a Firefox extension (or XULRunner application). Indeed, Gemse makes use of the global java.

Sun provides a short specification of LiveConnect.

Locating the java libraries

Unfortunately, it is not enough to know the chrome URL of the java library files we want to load in addition to the default ones, because the Java VM does not know about the chrome. As for now, they are not even in the chrome, but in the folder java in the installation directory of the extension or XULRunner application. We have to find out the absolute path of the folder. How one is supposed to do that, I don't know. How it is done in Gemse at the moment you can look up in the source.

Loading libraries using a class loader

The class loader used by Firefox is of course unaware of our own Java libraries we want to load. (Note that when using LiveConnect via an applet this should be less a problem, since you can indicate the sources in the XHTML code for the applet directly.) We have to create our own class loader. Unfortunately, I don't know of a file class loader which can be given filenames. There is only a URLClassLoader which works with URLs. Since we do not really want to write our own class loader, we use an instance of URLClassLoader. We have to give its constructor all URLs to load classes from.

First we turn the JavaScript array of strings into a JavaScript array of Java URLs (extensionPath is the installation directory of Gemse as nsIFile):

Create a new instance of URLClassLoader.

Now, if we want to obtain a class, we have to choose one of the following strategies:

It is important to understand that a class loader gives you an instance of java.lang.Class. Therefore it is a Java object, not a class! Its methods and fields are not the ones of the class, but the ones defined by java.lang.Class. One example is obtaining a RendererFactory of the JOMDoc rendering architecture:

The above code indeed calls the method newInstance from the class represented by the object RendererFactory. On the other hand, RendererFactory.newInstance() does something else, it uses the default constructor of the represented class and returns the new object.

There is one more problem you have to be aware of: When you load a class with a custom class loader, the system class loader probably has no clue about the class path of the class you load. If this class loads a resource from its class path (for example a file from the jar package it is contained in) using the system class loader, it will fail. For example org.omdoc.jomdoc.ntn.coll.ntn.B loads the notations bundled with JOMDoc. At the time of writing, it does that via the system class loader and therefore fails (JOMDoc Bug #603). If it used the class loader it has been loaded with, it would work. In java, the class loader that has been used to load a class MyClass can be obtained by MyClass.class.getClassLoader().

Elevating the privileges of the Java code

According to my experiments, files in the path of the URL class loader can be accessed. In theory, this means JOMDoc should be able to load the notations bundled with it right away. But it seems that some of the libraries used by JOMDoc (namely Saxon) need additonal rights. Also, if JOMDoc can access all files, then it can be used to load additional notations using all its collection strategies. So, as a temporary solution, we give the loaded Java libraries full privileges. This could be done by writing our own class loader or our own policy. I chose to write my own policy.

There is already an implementation of such a policy by SIMILE (jar without source, source). Unfortunately, there server does not respond. Many projects contain it as library, but without source. I was able to locate a copy via Google code search. The policy used in Gemse is based on SIMILE's policy.

We have to implement a class extending java.security.Policy. For a CodeSource (which mainly contains the URL the code originates from) it has to return a list of permissions. Before a piece of code is run, the security management first asks the policy which permissions should be granted to this code. At any time, there is only one policy in effect. If we want to change it, we have to register our new Policy. Our policy works like this: It has a list of URLs and a set of permissions. If the code in questions comes from an URL in the list, then it receives all permissions of the set. If it is not, then we ask the policy that was used before we registered our own one and return its answer. The implementation of our policy (inspired by SIMILE's implementation) roughly looks like this:

   urls = new HashSet(); // Or should we use HashSet?

    public PermissionCollection getPermissions(CodeSource codesource) {
        PermissionCollection pc = outerPolicy != null ?
                outerPolicy.getPermissions(codesource) :
                new Permissions();
        
        URL url = codesource.getLocation();
        if (url != null) {
            String s = url.toExternalForm();
            if (urls.contains(s) || "file:".equals(s)) {
                Enumeration e = permissions.elements();
                while (e.hasMoreElements()) {
                    pc.add(e.nextElement());
                }
            }
        }
        
        return pc;
    }
    
    public void setOuterPolicy(Policy policy) {
        outerPolicy = policy;
    }
    
    public void addPermission(Permission permission) {
        permissions.add(permission);
    }
    
    public void addURL(URL url) {
        urls.add(url.toExternalForm());
    }
}]]>

Accessing methods, fields and constructors of classes

When you have an instance of java.lang.Class, using the static methods, fields and constructors of the class is a little cumbersome. Let c be such an instance.

Calling the default constructor, which has no arguments, is done by calling newInstance(). If you want to use another constructor, use the method getConstructor, it expects an array of classes, indicating the types of the values you want to pass. An object of type java.lang.reflect.Constructor is returned. The constructor can then be called by invoking its newInstance method:

For methods it is similar. Use getMethod to obtain an instance of java.lang.reflect.Method and then call its invoke. Note that invoke expects the object you want to call the method on as first argument. Since you want to call the method on the class, not on a perticular object, pass null as first argument.

javaClasses.someClass.getMethod("foo", [javaClasses.BarClass]).invoke(null,[instanceOfBarClass]);

Finally, getting or setting fields is done using getField, whose only argument is the name of the field as string. It returns an instance of java.lang.reflect.Field.

Working with objects

Working with Java objects is as easy as it can be. You can use fields and call methods just as they were JavaScript objects. LiveConnect even does conversion of datatypes, which allows you to pass JavaScript Strings or even Arrays to Java methods. Furthermore, LiveConnect handles overloaded Java methods transparently. However, you still have to keep in mind that JavaScript is a loosely typed language and Java is heavily typed.

In the NTNView we have to recreate the equation using XOM, since the renderer of JOMDoc expects a XOM node. An example:

Further documentation