Communicating between JavaScript and JavaFX with WebEngine

Content by Peter Zhelezniakov

JavaFX 2 has introduced the WebEngine and WebView classes to support modern Web standards such as JavaScript, CSS, SVG, and a subset of HTML5.

Besides browsing Web pages, WebEngine can also serve as a container to host Web applications. Running standalone Web applications inside JavaFX is not very exciting though. You can do the same with any browser. What makes it interesting is the fact that the a Web application can communicate with its hosting JavaFX application, enabling a two-way communication channel. This article describes how this channel works in the JavaFX 2.1 Developer Preview.

Invoking JavaScript from JavaFX

The Java application can pass arbitrary scripts to the JavaScript engine of a WebEngine object by calling the WebEngine.executeScript() method:

webEngine.executeScript("history.back()");

The script is executed within the context of the current page. The result of the script invocation is converted to a Java type and returned. For the primitive types, the conversion is straightforward: integer values are converted to Integer, strings to String, etc. Most JavaScript objects are wrapped as instances of the netscape.javascript.JSObject class well-known to LiveConnect developers. Its methods are shown below:

public Object call(String methodName, Object... args);
public Object eval(String s);
public Object getMember(String name);
public void setMember(String name, Object value);
public void removeMember(String name);
public Object getSlot(int index);
public void setSlot(int index, Object value);

So, here is another way to go back one history item in a browser:

JSObject history = (JSObject) webEngine.executeScript("history");
history.call("back");

This example shows an interesting aspect of extending the WebEngine functionality. The WebEngine API, as of writing this, is deliberately limited to just a few methods that are considered critically important. However, the WebEngine class supports the much broader JavaScript API. You can use the executeScript() and JSObject methods to enable this second layer of API and get access to the functionality you miss. So, while there's no a Java method like goBack(), a similar JavaScript method exists and can be invoked as in the above example.

The JSObject methods apply the same conversion rules to the values they return as executeScript(). For example, the following method returns an instance of java.lang.Integer:

history.getMember("length");

A special case is when a JavaScript call returns a DOM Node. In this case, the result is wrapped in an instance of JSObject that also implements org.w3c.dom.Node.

Element p = (Element) webEngine.executeScript("document.getElementById('para')");
p.setAttribute("style", "font-weight: bold");

In this example, the script result is an Element object, and it is wrapped as org.w3c.dom.Element instance.


Making Upcalls from JavaScript to JavaFX

Since we are talking about a two-way communication channel, what about making calls in the opposite direction: from a Web application into JavaFX? On the JavaFX side, you need to create an interface object (of any class) and make it known to JavaScript by calling JSObject.setMember(). Having performed this, you can call public methods from JavaScript and access public fields of that object.

The code below shows how to set up an interface object:

class Bridge {
    public void exit() {
        Platform.exit();
    }
}
...
JSObject jsobj = (JSObject) webEngine.executeScript("window");
jsobj.setMember("java", new Bridge());

First we need a JSObject to attach our interface object to. The above code uses the JavaScript window object but any other object would work as well. Note that a cast is necessary. Then we create an interface object and add it as a new member of that JSObject. It becomes known to JavaScript under the name window.java, or just java, and its only method can be called from JavaScript as java.exit(). The upcall into Java is synchronous and occurs on the JavaFX Application thread. The following HTML code enables exiting the JavaFX application by clicking on a link:

<p>Click
<a href="" onclick="java.exit();">here</a>
to exit the application

Once you no longer need an interface object, you may want to call the JSObject.removeMember() method to make JavaScript "forget" it.

A Note about Security

Please be careful about functionality you open to JavaScript. Remember, there is no sandbox for standalone applications. Methods called by JavaScript on the interface object are invoked directly, as if they were called from your JavaFX code. If your application enables browsing arbitrary Web pages, a malicious script may take advantage of the ability to run Java methods with the user's permissions. So you probably do not want to write

jsobj.setMember("filesys", new File("/"));

as this would let scripts browse about the whole filesystem. By carefully designing the interface object, you can always make sure that only safe functionality is exposed. Another idea is to install and configure a security manager in your application.

Comments:

I'm using javafx 2.1 and I'm trying this example on osx:
- javafx to javascript works
- javscript to javafx doesn't seem to work: I'm getting a javascript error "ReferenceError: Can't find variable: java".

I'm currenlty gearing up to try the example on linux to see if the problem persists or not (Need help)

Posted by Steve Nyemba on March 07, 2012 at 07:32 PM PST #

Hi,
I'd like to make Upcalls from JavaScript to JavaFX2 but after clicking "here" nothing happens.
Where should
JSObject jsobj = (JSObject) webEngine.executeScript("window");
jsobj.setMember("java", new Bridge());

Is there any special place where instructions such as
JSObject jsobj = (JSObject) webEngine.executeScript("window");
jsobj.setMember("java", new Bridge());
should go? E.g. before/after page
webEngine.load(s);
I'd appreciate if you could you please provide a complete example of such communication.

Regards
-Peter

Posted by guest on March 29, 2012 at 07:02 AM PDT #

Hi Steve and Peter,

setMember() should be called after the page has been loaded. The reason is, JavaScript world is recreated each time a new page is loaded. The newly created window object won't have any custom members installed. A fine place to call setMember() is from a listener attached to WebEngine.getLoadWorker().stateProperty().

We're currently working on an update for the JavaFX tutorial: http://docs.oracle.com/javafx/2.0/webview/jfxpub-webview.htm . We plan to have some demo code there, too.

Thanks!

Posted by guest on March 29, 2012 at 10:46 PM PDT #

I have tried your example in the javafx tutorial but webEngine.executeScript("window");
win.setMember("app", new JavaApp());
does not work. When I click exit application nothing happens except the page goes blank.

Posted by guest on July 25, 2012 at 12:38 AM PDT #

Could you please try running the project from the tutorial?
It worked fine for me -- I was running it with the latest javafx 2.2 build.

Posted by peterz on July 26, 2012 at 02:45 AM PDT #

I believe the tutorial Peter is making referebce to is at http://docs.oracle.com/javafx/2/webview/jfxpub-webview.htm

Posted by Nicolas Lorain on July 26, 2012 at 02:34 PM PDT #

Hi,

Calling from JavaScript to JavaFX does not work.
1. I register the Java object after page was loaded.
2. I wrap the JavaScript code with try/catch block, and print alerts to see if there is en error. I print alerts info by adding alerts handler to my JavaFX code. See example code below.

3. Result: alert handling messages shows that
- script is invoked with no error.
- you can even see that the java object is recognized in the DOM (see printing)
- The code within Java object is not invoked!!! Also debugger does not stop there.

Current workaround seems to be: Using the alerts handler for getting calls from within JavaScript. Use some special syntax your application knows in order to distinguish regular alerts from query messages. For example:

alert('hello') is regular alert
alert('@code[{class:MyObjClass, method:doSomeWork, args:{a:1, b:"some string"}}]')

So your application knows to parse the Json object and use reflection in order invoke some method. This is just an example. The syntax can be different.

If someone succeed making the original feature of calling from JavaScript to JavaFX work please inform.
P.S JavaScript to Apple work great.

function testCallToJava() {
alert("testCallToJava start: '" + window.javaObj.test + "'");
try {
window.javaObj.test();
alert("testCallToJava end");
}catch(ex) {
alert("Error call to java: " + ex);
}
}

Print:
WebEvent [source = javafx.scene.web.WebEngine@7ab03, eventType = WEB_ALERT, data = testCallToJava start: 'function test() {
[native code]
}']

WebEvent [source = javafx.scene.web.WebEngine@7ab03, eventType = WEB_ALERT, data = testCallToJava end]

As you can see: start/end alerts was invoked. So what in the middle was invoked too but no effect.

Posted by guest on August 11, 2012 at 04:34 AM PDT #

I didn't run your test since I don't have the complete code. However, the simple test below works for me: a message is printed when I click into the beige area. Could you try this test too please?

import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Worker;
import javafx.concurrent.Worker.State;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.web.*;
import javafx.stage.Stage;
import netscape.javascript.JSObject;

public class WebLauncher extends Application {

final class Bridge {
public void test() {
System.out.println("test() called");
}
}

Scene createScene() {
final WebView webView = new WebView();
final WebEngine webEngine = webView.getEngine();
webEngine.getLoadWorker().stateProperty().addListener(
new ChangeListener<Worker.State>() {
public void changed(ObservableValue<? extends State> p, State oldState, State newState) {
if (newState == Worker.State.SUCCEEDED) {
JSObject win = (JSObject) webEngine.executeScript("window");
win.setMember("javaObj", new Bridge());
System.out.println("set up");
}
}
});
webEngine.loadContent(
"<div style='width: 100; height: 100; background: beige;' " +
"onclick='javaObj.test();' />"
);

Scene scene = new Scene(new Group(webView));
return scene;
}

@Override public void start(Stage stage) {
stage.setScene(createScene());
stage.sizeToScene();
stage.show();
}

public static void main(String[] args) {
Application.launch(args);
}
}

Posted by guest on August 17, 2012 at 01:43 AM PDT #

Hi,

Ok I have some resolution now:
1. With jfxrt.jr of JavaFX version JavaFX 2.0 SDK: Not working
2. With jfxrt.jr of JavaFX version: JavaFX 2.1 Runtime: Work fine!
3. With jfxrt.jr of JavaFX version: JavaFX 2.1 SDK: Work Fine!

Regards,
Gilad.

Posted by Gilad Tiram on August 17, 2012 at 02:46 PM PDT #

Yes, JavaScript to Java communication vie the WebEngine was only introduced in JavaFX 2.1. This article was published a little earlier, as this feature was already available in the JavaFX 2.1 Developer Preview.

Posted by Nicolas Lorain on August 17, 2012 at 04:11 PM PDT #

Hello, i would like this Communicate with Javascript But my SDK is JavaFX 2.2 and won't work before my Application won't call back from Javascript. I have copied from Posting "Posted by guest on August 17, 2012 at 01:43 AM PDT"

I have tried this code:

import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Worker;
import javafx.concurrent.Worker.State;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.web.*;
import javafx.stage.Stage;
import netscape.javascript.JSObject;

public class WebLauncher extends Application {
final class Bridge {
public void test() {
System.out.println("test() called");
}

public void exit() {
Platform.exit();
}
}

Scene createScene() {
final WebView webView = new WebView();
final WebEngine webEngine = webView.getEngine();
webEngine.getLoadWorker().stateProperty().addListener(
new ChangeListener<Worker.State>() {
public void changed(ObservableValue<? extends State> p, State oldState, State newState) {
if (newState == Worker.State.SUCCEEDED) {
JSObject win = (JSObject) webEngine.executeScript("window");
win.setMember("javaObj", new Bridge());
System.out.println("set up");
JSObject win2 = (JSObject) webEngine.executeScript("window");
win2.setMember("app", new Bridge());
}
}
});
webEngine.loadContent(
"<div onclick='javaObj.test();' width='100' height='50'>"+
"Test"+
"</div><br /><div onClick='app.exit();' width='100' height='50'>"+
"Exit"+
"</div>"
);

Scene scene = new Scene(new Group(webView));
return scene;
}

@Override public void start(Stage stage) {
stage.setScene(createScene());
stage.sizeToScene();
stage.show();
}

public static void main(String[] args) {
Application.launch(args);
}
}

Now i would like to call from WebLauncher but it doesn't work because it get unusabled?

What does it happen?

Posted by SourceSKyboxer on March 02, 2013 at 10:03 AM PST #

Hi,

Starting from 2.2.5 you need to declare class Bridge public. This is a security measure. Think of JavaScript as being in another package relative to your Java code.

Posted by Peter on March 04, 2013 at 10:19 PM PST #

Post a Comment:
  • HTML Syntax: NOT allowed
About

This blog is maintained by Nicolas Lorain, Java Client Product Manager. The views expressed on this blog are my own & do not necessarily reflect the views of Oracle.

Search

Categories
Archives
« April 2014
SunMonTueWedThuFriSat
  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
   
       
Today