Thursday, October 11, 2007

Java Firefox Extension



I know, you won't find this as the first search result on Google, but anyway...

Because I promised some (many) posts ago and because recently I found the time to create a decent example (actually, I HAD to find the time), here's the long waited demo of a Java Firefox extension, i. e. Firefox extension that uses Java code. I first got this idea from Simile's Piggybank extension, and then I got their example from http://simile.mit.edu/wiki/Java_Firefox_Extension. I found that to be quite complicated so I created a new one, simpler (and optimized in some points) and, I think, easier to understand.
I assume you are familiar with the Firefox extension development. If you're not, have a look at http://developer.mozilla.org/en/docs/Extensions.
What we already have in Firefox:


  • LiveConnect

  • that allows us to connect to Java code from javascript code. We can write stuff like var x = new java.lang.String("test") and x will hold a reference to a Java object. We can then call all x's functions from javascript and get the results back. We can not send javascript objects other than numbers (integer, double), strings and arrays to Java. I mean, we can, but we'll get a JSObject object which is kind of empty and useless (no functions, no members).


  • XPCOM

  • that allows us to build a COM-like component for Firefox, platform independent (because it is written in javascript).


We will create a XPCOM component that will use LiveConnect to get to the Java code.

Why can't we do it only with LiveConnect? Because XPCOM allows a single instantiation of the component through all the browser instances (shared). Now you decide for yourself why you might need that (I personally needed it because I had some native libraries loaded by the Java code and I had to be sure that happenned only once since I would get an error otherwise).
And it's pretty cool to learn how to do both the Java access and the XPCOM.
Now get the code from this temporary deposit (change extension in xpi and drag it to a Firefox window to install) and follow me:
Any XPCOM component must implement an interface, at least nsISupports, which is the most general so that it can be managed by the Firefox component manager. The trick is that we don't need any function from the interface, we will get the javascript object that implements the interface and call its functions, so we should implement an interface with no functions at all -- we can create our own (like simile's example does) and go through all the mess of compiling the idl file in a xpt interface definition file or we can simply implement nsISupports.
Once that is done (see the GreeterComponent.js file in /components) we can simply call:


Components.classes["@lucaa.students.info.uaic.ro/je-greeter;1"]
.getService(Components.interfaces.nsISupports);

to get an instance to our component ("@lucaa.students.info.uaic.ro/je-greeter;1" is the component's contract id, specified in the implementation).

But what's the trick to access the Java code? One might say that we have LiveConnect's java object and that is enough. Well, not quite, since our own classes ar not loaded and we can not access them. We need to load them dynamically through our own class loader (this is done by the component upon initialization):

var greeterURL = new this._java.io.File(greeterJarLocation.path).toURL();
//create the classLoader
var classLoader = new this._java.net.URLClassLoader([greeterURL]);
//get an instance of the GreetingGenerator through reflection
var greetingGeneratorClass = this._java.lang.Class.forName(
"ro.uaic.info.students.lucaa.je.GreetingGenerator",
true, classLoader);
this._greeter = greetingGeneratorClass.newInstance();

Then, the call to the java functions of the GreetingGenerator can be simply done through:

this._greeter.generateGreeting()

Remember that the java LiveConnect object is not provided by default to the components environment and we must pass it from the calling code, upon initializing the wrappedJSObject of the component:

//get a component
var greetComponent = Components.classes["@lucaa.students.info.uaic.ro/je-greeter;1"]
.getService(Components.interfaces.nsISupports);
//initialize the wrappedJSObject
greetComponent.wrappedJSObject.initialize(java, true);

In the example, the GreetingGenerator class in Java holds a counter incremented by each call to the generateGreeting function. Go to the Tools menu, the "Greet!" menu item and get your greeting. Now open a new browser instance and do the same. The counter goes on, as a sign that it's the exact same object called.

Starting from this, you can do all sorts of stuff: you've just created yourself an unique entrypoint in the Java code, making sure that if you have configurations to set up, servers to start, etc, etc, those will happen exactly once.

Happy coding!

19 comments:

Warsaw said...

In a belated comment (looks like you posted this ages ago), rockin! Thanks, very easy to follow and saves me from (shudder) javascript, as I'm just not man enough to spend too much time there.

Abo said...

I'm trying to use the same mechanism to create my own extension using the java libraries i need. Everything works fine until I call normal methods, but whenever i'm trying to access most complex classes, i can't because of the following exception:
Error in loading class: Error calling method on NPObject! [plugin exception: java.lang.reflect.InvocationTargetException].
that is thrown whenever i'm trying to create an instance of that class.
Any suggestion?

Filip said...

Hi, this java access point works only in xul layer. But I need to load some java methods whenever another page is loaded or tab opened.
I have tried to inject the script (in this extension overlay.js) into each page that is loaded in FF, but get always XPCOM exception alert message :o( Any idea why does it not work?

anca luca said...

Filip: I haven't tested it, but it's only normal to fail, because it's a different context in which the js is executed therefore different security constraints (it's treated like a script in the page, not like something that you injected because it cannot know). js in pages cannot do as much as js in xuls, because if it could, imagine a script in a webpage (not written by you, but by its malicious author) could access a running java on your machine and set its SecurityManager to null. You don't want that!
Now, if you want to do things whenever a tab loads, checkout the documentation (on MDC or MozillaZine), there is a method to listen to these loading events from xul, and run your code in the listener on the xul overlay side.
Hope this helps,
happy coding

Filip said...

Hi, can you give me some advice how to extend your minimal extension to load multiple jars? I am trying to do this couple of hours and starting to be desperate... thx

anca luca said...

as described in http://java.sun.com/j2se/1.4.2/docs/api/java/net/URLClassLoader.html, the URLClassLoader gets an array of URLs as a constructor parameter. So you just create your urls toward those jars (similar with the url created in the example) and put them in the array passed to the URLClassLoader's constructor and you're done!
Happy coding!

Filip said...

It is not that simple. I put the jars as URL into an array but only the first one was loaded. So I think it is due to the incompatibility of Jscript and java arrays. I tried to use a method to convert from jscript array to java array also used in extensions as firegoose, piggy bank or xqueme extensions. Problem is that they are using a little different approaches for example istead of URLClassLoader(javaArray)
they use URLClassLoader.newInstance(javaArray) but none of them is working for me ;o( Is there any possiblity to show you my code?

anca luca said...

This is how I used it in my realworld application where I needed more than one jar:

this._classLoader = new java.net.URLClassLoader([url1, url2, url3, url4, url5])

my classes were in a single jar (from the five), but they needed the other jars to run.
There might be some details that I am missing, but that worked fine when i built it (in FF2) and it works fine in FF3 also (iirc).

mzatanoskas said...

Hey Anca I'm in dire need of some java + firefox extension experts to put me out of my misery!

I'm trying to access the main method in a custom class from an extension but I'm having problems. I've worked out with help how to do it for a named method that doesn't take parameters but can't get it to work with main or when I want to pass parameters.

I'm a complete beginner so I'm sure my problem isn't that complicated. I've got a thread with code details going here:
http://forums.sun.com/thread.jspa?messageID=10608253

Anyway I was just hoping you might be able to help!

mzatanoskas said...

Luca thanks so much for your help over at the sun forums. I've got the basic java and javascript connection working beautifully but I'm still having problems with the bootstrap security permissions. I added the URLSetPolicy.class to my jar and I've been trying to call it with: var policyClass = java.lang.Class.forName(
"URLSetPolicy",
true,
cl
);
But I'm just getting an invocation.Target.Exception error like Abo above... This security business is the only thing stopping me from completing my extension so I'd be so grateful if you could help!

mzatanoskas said...

Just to say sorry I must have missed your reply on the forums before! I've added some details on the problem now. I'd be really stuck without your help, so I really appreciate it!

James said...

Could you please tell me how to make multiple instances of the xpcom component? I tried changing the 'singleton' bool to false and changed je-greeter;1"].getService to .createInstance, but each window is still connected to the same instance.

Ben said...

Thanks for very useful information.
Free web design

Anonymous said...

you have a nice site. thanks for sharing this site. there are various kinds of ebooks are available here

http://feboook.blogspot.com

Drew said...

Argh! I really want to use this but the link to the zip is dead.

Any chance you can sort it out? Thanks,

anca luca said...

@Drew: fixed

Filip said...

Hi, I have written a extension using java for Firefox 3.0.x.
It works nice under MS Windows XP, Vista and 7, but I can't get it work under linux and OS X. I am not mac or linux user, but more people have ried my extension wothout success. Is there any known issue on linux or mac with XPCOM or LiveConnect or Java with Firefox 3.0.x? Any help will be much appreciated. I have used the same concept as in your mini extension for calling java within Firefox.
Best regards, Filip

anca luca said...

@Filip

I have run it on Linux and it worked fine, not with ff 3.0 indeed, but I strongly believe it should work. Make sure it's not a messed up path you're setting somewhere, with the microsoft \ separator instead of the standard / separator (first idea for debugging, since you say it only works on windows...)

Filip said...

Thank you for your fast reply :)
I have tried your mini extension (to make sure there aren't any separators issues) few minutes ago on Mac with FF 3.0.12 and it didn't work. I will try it on some linux versions too in next few days. But I can't understand, why it works well in 3.0.x on Windows platform and not on other platforms. It seems as FF 3 is not the same on different platforms.