Piloter une tâche asynchrone Java/Groovy depuis un composant WebView JavaFX avec JavaScript

Le composant JavaFX WebView réunit deux mondes : celui de Java et d'HTML5, et offre une interaction Java/JavaScript à double sens. D'une part, il est possible d'exécuter du code JavaScript dans le contexte de la page web chargée par le composant WebView, et d'autre part, on peut exécuter du code Java présent dans l'application JavaFX à partir du code JavaScript (voir la documentation de la classe WebEngine).

Dans ce dernier cas, plus précisément, lorsque l'on appelle une méthode sur la référence de l'objet Java passé au travers d'une propriété de l'objet JavaScript window, l'appel est synchrone. Cependant, il pourrait être intéressant de pouvoir déclencher un traitement asynchrone côté Java, depuis du code JavaScript de la page web, et recevoir le résultat de ce traitement une fois celui-ci terminé.

C'est ce que je vais démontrer dans le reste de cet article, en utilisant GroovyFX pour créer l'application JavaFX sous la forme d'un script Groovy. Pour la partie web, qui consiste en un fichier .html chargé dans un composant JavaFX WebView, je recours à AngularJS et son service $q qui implémente les promises. Il serait tout à fait possible d'utiliser jQuery et son implémentation des promises à la place.

L'idée principale est celle-ci : à partir du code JavaScript, appeler une méthode d'objet Java et lui transmettant l'objet JavaScript deferred créé via le service $q. Dans l'implémentation de la méthode Java (ou Groovy), un objet de type Task est créé puis démarré pour réaliser le traitement asynchrone ; selon l'issu traitement, on appelle resolve ou reject sur l'objet deferred, ce qui fait que le code JavaScript de la page web n'est pas bloqué et peut réagir en fonction de la réalisation de la promesse. Voici le script GroovyFX AngularFX.groovy (disponible en fichier joint) qui, exécuté via une commande groovy, permet de démarrer l'application JavaFX comprenant un composant WebView :

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
@Grab('org.codehaus.groovyfx:groovyfx:0.4.0')
import static groovyx.javafx.GroovyFX.start

import javafx.beans.value.ChangeListener
import javafx.event.EventHandler
import static javafx.concurrent.Worker.State.*
import javafx.concurrent.Task


class ProcessTask extends Task {
    def expression

    ProcessTask(expression) {
        this.expression = expression
    }

    @Override protected String call() throws Exception {
        Eval.me(this.expression)
    }
}

class FX {
    def process(expression, deferred) {
        def task = new ProcessTask(expression)

        task.messageProperty().addListener( { s, o, n ->
            println n.value
        } as ChangeListener)

        task.onSucceeded = { event ->
            deferred?.call('resolve', task.value) 
        } as EventHandler

        task.onFailed = { event ->
            deferred?.call('reject', task.exception.message) 
        } as EventHandler        
     
        new Thread(task).start()
    }
}

start {
    stage(title: 'AngularFX', visible: true) {
        scene(width: 800, height: 600) {            
            borderPane  {
                vbox {
                    webView = webView()
                }                  
            } 
        }
    }

    webView.engine.loadWorker.stateProperty().addListener({ t, o, n ->
        if (n == SUCCEEDED) {
            webView.engine.executeScript("window").setMember("FX", new FX())
        }
    } as ChangeListener)

    webView.engine.onError = { event ->
        println 'error: ' + event.message
    } as EventHandler

    // Capture alerts
    webView.engine.onAlert = { event ->
        println 'alert: ' + event.data.toString()
    } as EventHandler

    webView.engine.load("file:///${System.getProperty('user.dir')}/angularfx.html")
}

Le code est relativement simple : tout d'abord, on charge la page web angularfx.html supposée se trouver dans le même répertoire que le script. Lorsque le page est complétement chargée, on injecte la propriété FX dans l'objet JavaScript window avec une référence vers un objet de type FX. Cette classe comprend une seule méthode, process, qui sera invoquée depuis du code JavaScript de la page angularfx.html. La méthode process a la responsabilité de créer la tâche -une instance de ProcessTask héritant de la classe JavaFX Task- dont l'exécution se fera dans un thread séparé, mais également, en fonction du résultat du traitement, de satisfaire et de rejeter la promesse (appel à resolve ou reject sur l'objet deferred JavaScript).

Notez qu'à titre d'exemple, la tâche asynchrone se contente d'évaluer le code Groovy qu'on lui transmet dans son constructeur, dans la méthode call. En cas de réussite, on appelle resolve sur l'objet deferred en transmettant le résultat de l'évaluation ; en cas d'échec, c'est reject qui est appelé sur l'objet deferred, en passant le message de l'exception à l'origine de l'échec (méthode process de la classe FX).

La page web angularfx.html comprend une application AngularJS permettant l'évaluation (asynchrone) de code Groovy saisi dans une zone d'édition, et d'en afficher le résultat dans la page web.

Voici le code source d'angularfx.html (également disponible en fichier joint) :

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
<input type="text" ng-model="ctrl.expression" placeholder="Enter an expression">
<button ng-click="ctrl.process()">
    Evaluate
</button>
<h1>Evaluate <span ng-bind="ctrl.expression"></span></h1>
<h2 ng-bind="ctrl.message"></h2>

<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.10/angular.min.js"></script>
<script type="text/javascript">
    angular.module('angularFX', [])
        .controller('MainCtrl', function($q) {
            var self = this;

            self.process = function() {
                var deferred = $q.defer();
                deferred.promise.then(
                    // success
                    function (response) {
                        self.message = response
                    },
                    // failure
                    function (response) {
                        // can be something else                             
                        self.message = response
                    }
                );                 
                FX.process(self.expression, deferred);
            };                    
        });
</script>

Un click sur le bouton Evaluate déclenche l'exécution de la méthode process du contrôleur MainCtrl. Cette méthode crée un objet deferred au moyen du service injecté $q d'AngularJS, puis définit par deux fonctions JavaScript, se qui va se passer en cas de succès et d'échec (appel deferred.promise.then()) : dans notre exemple, on se contente d'affecter la propriété message du contrôleur avec le résultat de l'évaluation opéré côté Java ou avec un message d'exception. La valeur de message sera automatiquement affichée dans l'élément h2 de la page du fait de la liaison sur la propriété ctrl.message.

Le fait de pouvoir invoquer du code Java/Groovy, qu'il soit asynchrone ou non, depuis un composant WebView placé dans une scène JavaFX, ouvre des perspectives intéressantes. C'est un peu comme être dans une application hybride dans laquelle on peut combiner la puissance et l'étendue du langage Java avec la souplesse de JavaScript au sein d'une présentation HTML5.

Nul doute pour moi de continuer à explorer une telle combinaison... en allant par exemple plus loin avec cette idée d'évaluation de code Groovy.


Fichier(s) :