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 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.
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.
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:
java.*
, Packages.*
:
It is possible to use JavaScript's new
operator to create an instance of such a class. It is also possible to
access fields and methods of the class directly. For overloaded
methods and contructors, the right one is chosen automatically.
One can not hand over a java.*
of Packages.*
as classes to methods
expecting a class as an argument, since LiveConnect treats them as Java
namespaces and does not convert them to java.lang.Class
. Using
something like java.lang.String.class
in order to obtain
a class object is not possible, since this
is a feature of the Java
language itself, class
is not a field of the class.java.lang.Class.forName("name.of.class")
: Uses the system class
loader to load the class if not already done so. This method returns
an object of type java.lang.Class
. Therefore it can be handed to methods expecting a
class and it is not possible to access fields, methods and constructors
(new operator) of the class directly.java.lang.Class.forName("name.of.class", true, classLoader)
loads
the class using the given classLoader. It returns an instance
java.lang.Class
as well.ClassLoader.getSystemClassLoader()
. Using the method
loadClass
of this class loader has the same effect as using
java.lang.class.forName("name.of.class")
. It is unlikely that one wants to
do it that way, but the class ClassLoader has some other static
methods which can be used to retrieve arbitrary files from the class
paths. So, if you want to open a file located in the jar file of your
class, do not use the system class loader as it may be that it does
not know about your jar file.getClassLoader()
of the class. In
order to get the class of an object obj
, you can use the
method getClass
, so in a non-static method, you
can do something like obj.getClass().getClassLoader()
.
But probably you want to name the class explicitely (like
MyClass.class.getClassLoader()
) because getClass
could return
a subclass instead of the class in which you implement your
method.classLoader.loadClass("name.of.class")
, which returns an
instance of java.lang.Class
.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()
.
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()); } }]]>
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 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: