Search code examples
javajqueryjavafxjquery-callbackjavafx-webengine

Executing multiple scripts with WebEngine (or a script that depends on another script)


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:

  1. Browser - a class that wraps the WebEngine and serves as a browser that load the web page and execute scripts.
  2. JQueryFunction - a class that lists a couple of functions to be used with jQuery callback functions and could be implemented in multiple ways. (Basically they should be functional interfaces that the user implements his way - but for simplicity I made it normal functions).
  3. Test - a class with main method that executes two scripts.

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?


Solution

  • 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();
    
        }