I want to use the WebEngine (of JavaFX) to execute some JavaScript (and jQuery) on web pages and process the results (with Java code).
I have a problem with callback functions that execute some script by themselves.
For the purpose of illustrating my problem as simply as possible, I made a minimized code that shows the undesired result (it is only a part of a bigger project).
So, I created three classes:
Browser
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Worker.State;
import javafx.scene.Scene;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import netscape.javascript.JSObject;
public class Browser extends Application {
private static WebEngine webEngine;
private static JSObject window;
@Override
public void start(Stage primaryStage) throws Exception {
WebView browser = new WebView();
webEngine = browser.getEngine();
webEngine.getLoadWorker().stateProperty().addListener(
new ChangeListener<State>() {
@Override
public void changed(ObservableValue<? extends State> observable, State oldState, State newState) {
if (newState == State.SUCCEEDED) {
window = (JSObject) webEngine.executeScript("window;");
// The following lines inject jQuery (for pages that don't use already)
webEngine.executeScript("var script = document.createElement(\"script\");");
webEngine.executeScript("script.src = \"http://code.jquery.com/jquery-1.12.0.min.js\";");
webEngine.executeScript("document.getElementsByTagName(\"body\")[0].appendChild(script);");
}
}
});
primaryStage.setScene(new Scene(browser));
primaryStage.show();
//Platform.setImplicitExit(false);
}
public static JSObject getWindow() {
return window;
}
public static Object executeScript(String script) throws InterruptedException, ExecutionException {
FutureTask<Object> task = new FutureTask<>(new Callable<Object>() {
@Override
public Object call() throws Exception {
return webEngine.executeScript(script);
}
});
Platform.runLater(task);
return task.get();
}
public static void load(String url) {
Platform.runLater(new Runnable() {
@Override
public void run() {
webEngine.load(url);
}
});
}
public static void start() {
new Thread(new Runnable() {
@Override
public void run() {
try {
Application.launch(Browser.class);
} catch (IllegalStateException e) {}
}
}).start();
}
}
JQueryFunction
import java.util.concurrent.ExecutionException;
public class JQueryFunction {
public String function1(int index, String text) {
return (index+1) + ": " + text;
}
public String function2(int index, String text) throws InterruptedException, ExecutionException {
return (String) Browser.executeScript("$($(\".BlueArrows:eq(2) li\").find(\"a:first\")[index]).text();");
}
}
Test
public class Test {
public static void main(String[] args) throws Exception {
Browser.start();
Thread.sleep(1000); // Only used here to simplify the code
Browser.load("https://docs.oracle.com/javase/tutorial/");
Thread.sleep(3500); // Only used here to simplify the code
Browser.getWindow().setMember("javaApp", new JQueryFunction());
Browser.executeScript("$(\".BlueArrows:eq(0) li\").find(\"a:first\").text(function(index, text) { return javaApp.function1(index, text); });");
Browser.executeScript("$(\".BlueArrows:eq(0) li\").find(\"a:first\").text(function(index, text) { return javaApp.function2(index, text); });");
}
}
When I run the test, the first executeScript
runs as expected and change the text of some elements (prepending index number).
But the second executeScript
is stuck forever and stick the GUI and actually the whole JavaFX application.
I understand why this happens...
The executeScript
method calls the WebEngine (via Platform.runLater
) to execute a jQuery that iterates over the elements and invokes a Java function (with different parameters each time).
The first execution (which invokes function1) gets the returned String from function1 and applies it on the element's text. (perfectly as expected)!
The second execution (which invokes function2) executes the jQuery function that invokes the Java function, but the Java function needs to execute some more JavaScript (or jQuery), but the WebEngine won't execute until the first execution is done.
But the first execution won't finish because it depends on the result of the second execution.
WebEngine is programmed in a way that he will execute in one thread only (within the FX thread) one task after another (serially).
Is there any way to solve it?
Why is WebEngine restricted to work only under JavaFX application?
And why does it have to work only with one thread?
One possible way to avoid the "deadlock" you are experiencing is not to call Platform.runLater
when you're already running in the GUI Thread (JavaFX Application Thread). Something like:
public static Object executeScript(String script) throws InterruptedException, ExecutionException {
if(Platform.isFxApplicationThread()) {
return webEngine.executeScript(script);
}
FutureTask<Object> task = new FutureTask<>(new Callable<Object>() {
@Override
public Object call() throws Exception {
return webEngine.executeScript(script);
}
});
Platform.runLater(task);
return task.get();
}