木曜日 5 23, 2013

Nashorn から JavaFX を呼び出す

はじめに

Nashorn から JavaFX を呼び出す方法が決まった様です。サンプルコードとともにご覧下さい。

おことわり

以下は jlaskey による Nashorn Blog への投稿の翻訳です。原文は https://blogs.oracle.com/nashorn/entry/jjs_fx でご覧頂けます。翻訳文の URL は https://blogs.oracle.com/nashorn_ja/entry/jjs_fx です。

訳文

JavaFX の開発者達と話し合った結果、JavaFX と Nashorn シェルの起動スクリプトの連携方法について良い案が見つかりました。この案では jjs コマンドに -fx フラグを付けるとjavafx.application.Application を使って起動します。その後は Nashorn から JavaFX の呼び出しはとても簡単です。

基本的なコマンドラインは jjs -fx fxscript.js の様な形になります。そこに -scripting オプションや -- オプションを付けて jjs -fx -scripting fxscript.js -- my script args の様に実行することも出来ます。

以下のサンプルコードの処理内容は過去にこの Blog に載せたサンプルから取っています。jjs に渡すスクリプトに JavaFX 用の init, start, stop 関数を含める事も可能です。以前と異なるのは、それら JavaFX 用の関数を定義せずに、いきなりスクリプトを書き始めることが出来ることです。元々の Hello World プログラムはこうでした。

var Button    = javafx.scene.control.Button;
var StackPane = javafx.scene.layout.StackPane;
var Scene     = javafx.scene.Scene;

function start(stage) {
    stage.title = "Hello World!";
    var button = new Button();
    button.text = "Say 'Hello World'";
    button.onAction = function() print("Hello World!");
    var root = new StackPane();
    root.children.add(button);
    stage.scene = new Scene(root, 300, 250);
    stage.show();
}

これを次の様に start 関数を定義しない形で書く事が出来ます。

var Button    = javafx.scene.control.Button;
var StackPane = javafx.scene.layout.StackPane;
var Scene     = javafx.scene.Scene;

$STAGE.title = "Hello World!";
var button = new Button();
button.text = "Say 'Hello World'";
button.onAction = function() print("Hello World!");
var root = new StackPane();
root.children.add(button);
$STAGE.scene = new Scene(root, 300, 250);
$STAGE.show();

stage 変数は $STAGE グローバル変数に代わり、start() 関数の引数として渡す必要はなくなりました。

また、利便性を上げるために JavaFX のクラスをまとめてロードするための仕組みも用意しました。推奨されるのは(オブジェクトの生成と静的なフィールドアクセスに)必要なクラスだけをロードすることですが、プロトタイピングなどで手早く実装を進めたい場合には、まとめてロード出来るととても便利です。

その仕組みを利用すると、先ほどの Hello World プログラムは次の様に書き換えられます。

load("fx:base.js");
load("fx:controls.js");
load("fx:graphics.js");

$STAGE.title = "Hello World!";
var button = new Button();
button.text = "Say 'Hello World'";
button.onAction = function() print("Hello World!");
var root = new StackPane();
root.children.add(button);
$STAGE.scene = new Scene(root, 300, 250);
$STAGE.show();

この方法で読み込めるクラスは以下の通りです。

fx:base.js
    javafx.stage.Stage
    javafx.scene.Scene
    javafx.scene.Group
    javafx/beans
    javafx/collections
    javafx/events
    javafx/util

fx:graphics.js
    javafx/animation
    javafx/application
    javafx/concurrent
    javafx/css
    javafx/geometry
    javafx/print
    javafx/scene
    javafx/stage

fx:controls.js
    javafx/scene/chart
    javafx/scene/control

fx:fxml.js
    javafx/fxml

fx:web.js
    javafx/scene/web

fx:media.js
    javafx/scene/media

fx:swing.js
    javafx/embed/swing

fx:swt.js
    javafx/embed/swt

もう少しサンプルコードをご紹介します。

// fx3d.js

load("fx:base.js");
load("fx:controls.js");
load("fx:graphics.js");

var material = new PhongMaterial();
material.diffuseColor = Color.LIGHTGREEN;
material.specularColor = Color.rgb(30, 30, 30);

var meshView = Java.toJavaArray([
    new Box(200, 200, 200),
    new Sphere(100),
    new Cylinder(100, 200)
], "javafx.scene.shape.Shape3D");

for (var i = 0; i != 3; i++) {
    meshView[i].material = material;
    meshView[i].translateX = (i + 1) * 220;
    meshView[i].translateY = 500;
    meshView[i].translateZ = 20;
    meshView[i].drawMode = DrawMode.FILL;
    meshView[i].cullFace = CullFace.BACK;
};

var pointLight = new PointLight(Color.WHITE);
pointLight.translateX = 800;
pointLight.translateY = -200;
pointLight.translateZ = -1000;

var root = new Group(meshView);
root.children.add(pointLight);

var scene = new Scene(root, 800, 800, true);
scene.fill = Color.rgb(127, 127, 127);
scene.camera = new PerspectiveCamera(false);
$STAGE.scene = scene;
$STAGE.show();

// ColorfulCircles.js

load("fx:base.js");
load("fx:controls.js");
load("fx:graphics.js");

var WIDTH = 500;
var HEIGHT = 600;
var animation;

function setup(primaryStage) {
    var root = new Group();
    primaryStage.resizable = false;
    var scene = new Scene(root, WIDTH, HEIGHT);
    scene.title = "Colourful Circles";
    primaryStage.scene = scene;

    // 1 つ目の Circle のリスト
    var layer1 = new Group();
    for(var i = 0; i < 15; i++) {
        var circle = new Circle(200, Color.web("white", 0.05));
        circle.strokeType = StrokeType.OUTSIDE;
        circle.stroke = Color.web("white", 0.2);
        circle.strokeWidth = 4;
        layer1.children.add(circle);
    }

    // 2 つ目の Circle のリスト
    var layer2 = new Group();
    for(var i = 0; i < 20; i++) {
        var circle = new Circle(70, Color.web("white", 0.05));
        circle.strokeType = StrokeType.OUTSIDE;
        circle.stroke = Color.web("white", 0.1);
        circle.strokeWidth = 2;
        layer2.children.add(circle);
    }

    // 3 つ目の Circle のリスト
    var layer3 = new Group();
    for(var i = 0; i < 10; i++) {
        var circle = new Circle(150, Color.web("white", 0.05));
        circle.strokeType = StrokeType.OUTSIDE;
        circle.stroke = Color.web("white", 0.16);
        circle.strokeWidth = 4;
        layer3.children.add(circle);
    }

    // それぞれのレイヤーに blur エフェクトを掛ける
    layer1.effect = new BoxBlur(30, 30, 3);
    layer2.effect = new BoxBlur(2, 2, 2);
    layer3.effect = new BoxBlur(10, 10, 3);

    // ウィンドウと同じ大きさの矩形を作成し、カラーグラデーションを設定する
    var colors = new Rectangle(WIDTH, HEIGHT,
            new LinearGradient(0, 1, 1, 0, true, CycleMethod.NO_CYCLE,
                               new Stop(0,    Color.web("#f8bd55")),
                               new Stop(0.14, Color.web("#c0fe56")),
                               new Stop(0.28, Color.web("#5dfbc1")),
                               new Stop(0.43, Color.web("#64c2f8")),
                               new Stop(0.57, Color.web("#be4af7")),
                               new Stop(0.71, Color.web("#ed5fc2")),
                               new Stop(0.85, Color.web("#ef504c")),
                               new Stop(1,    Color.web("#f2660f"))));
    colors.blendMode = BlendMode.OVERLAY;

    // メインのコンテンツを作成
    var group = new Group(new Rectangle(WIDTH, HEIGHT, Color.BLACK),
                          layer1, 
                          layer2,
                          layer3,
                          colors);
    var clip = new Rectangle(WIDTH, HEIGHT);
    clip.smooth = false;
    group.clip = clip;
    root.children.add(group);

    // 全ての円を含むリストを作成
    var allCircles = new java.util.ArrayList();
    allCircles.addAll(layer1.children);
    allCircles.addAll(layer2.children);
    allCircles.addAll(layer3.children);

    // allCircles に入っている円がランダムに移動するアニメーションを作成
    animation = new Timeline();
    for each (var circle in allCircles) {
        animation.getKeyFrames().addAll(
              new KeyFrame(Duration.ZERO, // 開始時間を 0 秒に設定
                           new KeyValue(circle.translateXProperty(), Math.random() * WIDTH),
                           new KeyValue(circle.translateYProperty(), Math.random() * HEIGHT)),
              new KeyFrame(new Duration(20000), // 終了時間を 20 秒に設定
                           new KeyValue(circle.translateXProperty(), Math.random() * WIDTH),
                           new KeyValue(circle.translateYProperty(), Math.random() * HEIGHT))
              );
    }
    animation.autoReverse = true;
    animation.cycleCount = Animation.INDEFINITE;
}

function stop() {
    animation.stop();
}

function play() {
    animation.play();
}

function start(primaryStage) {
    setup(primaryStage);
    primaryStage.show();
    play();
}

追記 : この仕組みは CCC に承認され確定しました。

水曜日 4 10, 2013

Nashorn から JavaFX へのアクセスの実装比較

はじめに

Nashorn の開発チームでは Nashorn から JavaFX にアクセスする仕組みを幾つか検討している様です。現在検討されている実装とその pros / cons について紹介している記事がありましたので、翻訳してみました。ご参照下さい。

おことわり

以下は jlaskey による Nashorn Blog への投稿の翻訳です。原文は https://blogs.oracle.com/nashorn/entry/to_shell_or_not_to でご覧頂けます。翻訳文の URL は https://blogs.oracle.com/nashorn_ja/entry/to_shell_or_not_to です。

訳文

Nashorn から JavaFX にアクセスする方法を検討しています。二種類の実装を試していますが、そのどちらもそれぞれ解決しないといけない問題があります。

問題のポイントは JavaFX と JDK と Nashorn の依存関係にあります。JavaFX は JDK と一緒に配布されていますが、JDK の一部ではありません。ビルドサイクルも JDK とは異なります。一方、Nashorn は JDK の一部です。Nashorn が JDK の一部であるという事は、JDK の外にある JavaFX に依存するコードは含められない事になります。さらに、JavaFX のプログラムを作る際には JavaFX に依存したコードを書く必要があります。具体的には javafx.application.Application クラスのサブクラスのインスタンスから処理を開始する様にプログラムを記述します。その為 Nashorn と JavaFX を橋渡しするプログラムを実装する際も JavaFX に依存したコードを書く必要がある事になりますが、そうするとそれは先ほどの依存関係があるため、JavaFX のコードは JDK には含められず、独立した場所に置いておく必要があります。

JavaFX へのアクセスを実装する一つ目の方法は jjs と同じようなシェルプログラムを用意する方法です。シェルプログラムの中で JavaFX の Application クラスの init, start, finish メソッドをオーバーライドしておき、外部スクリプトから即座に JavaFX の機能が使える様にします。このシェルプログラムの実装は Nashorn のソースコードリポジトリの中の nashorn/tools/fxshell に入れてあります。実装は以下の様になっています。

 /*
 * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package jdk.nashorn.tools;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.application.Application;
import javafx.stage.Stage;
import javax.script.Invocable;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import jdk.nashorn.api.scripting.NashornScriptEngineFactory;
/**
 * このシェルは Nashorn JavaScript 向けに書かれたアプリケーションから JavaFX を実行する為のプログラムです
 */

public class FXShell extends Application {
    /**
     * スクリプトエンジンマネージャ
     */
    private ScriptEngineManager manager;
    /**
     * Nashorn スクリプトエンジンファクトリー
     */
    private NashornScriptEngineFactory factory;
    /**
     * Nashorn スクリプトエンジンのメインインスタンス
     */
    private ScriptEngine engine;
    /**
     * FX ランチャーがこのクラスのインスタンスを作成する際に必要
     */

    public FXShell() {
    }
    /**
     * メインエントリーポイント。実際には使用されない
     * @param args コマンドライン引数
     */
    public static void main(String[] args) {
        launch(args);
    }
    /*
     * アプリケーション側でオーバーライドする
     */
    @Override
    public void init() throws Exception {
        // スクリプトエンジンマネージャ
        this.manager = new ScriptEngineManager();
        // Nashorn スクリプトエンジンファクトリーの取得。引数の処理に必要。
        for (ScriptEngineFactory engineFactory : this.manager.getEngineFactories()) {
             if (engineFactory.getEngineName().equals("Oracle Nashorn") &&
                 engineFactory instanceof NashornScriptEngineFactory) {
                this.factory = (NashornScriptEngineFactory)engineFactory;
            }
        }
        // 取得出来なかった場合
        if (this.factory == null) {
            System.err.println("Nashorn script engine not available");
            System.exit(1);
        }
        // コマンドラインと Java Network Launch Protocol パラメータの取得
        final Parameters parameters = getParameters();
        // スクリプトの場所とコマンドライン引数の格納用
        final List<String> paths = new ArrayList<>();
        final List<String> args = new ArrayList<>();
        // 適切な JNLP 名前付きパラメータの取得
        final Map<String, String> named = parameters.getNamed();
        for (Map.Entry<String, String> entry : named.entrySet()) {
            final String key = entry.getKey();
            final String value = entry.getValue();
            if ((key.equals("cp") || key.equals("classpath")) && value != null) {
                args.add("-classpath");
                args.add(value);
            } else if (key.equals("source") && value != null &&
                       value.toLowerCase().endsWith(".js")) {
                paths.add(value);
            }
        }
        // コマンドライン引数として適切な値の取得
        boolean addNextArg = false;
        boolean addAllArgs = false;
        for (String parameter : parameters.getUnnamed()) {
            if (addAllArgs || addNextArg) {
                args.add(parameter);
                addNextArg = false;
            } else if (parameter.equals("--")) {
                args.add(parameter);
                addAllArgs = true;
            } else if (parameter.startsWith("-")) {
                args.add(parameter);
                addNextArg = parameter.equals("-cp") || parameter.equals("-classpath");
            } else if (parameter.toLowerCase().endsWith(".js")) {
                paths.add(parameter);
            }
        }
        // 取得したパラメータで Nashorn スクリプトエンジンを作成する
        engine = factory.getScriptEngine(args.toArray(new String[args.size()]));
        // 外部スクリプトの読み込み
        for (String path : paths) {
            load(path);
        }
        // 外部スクリプトに init 関数が定義されていた場合は実行する
        try {
            ((Invocable) engine).invokeFunction("init");
        } catch (NoSuchMethodException ex) {
            // init 関数は存在していなくても良い
        }
    }
    @Override
    public void start(Stage stage) throws Exception {
        // 外部スクリプトに start 関数が定義されていた場合は実行する
        try {
            ((Invocable) engine).invokeFunction("start", stage);
        } catch (NoSuchMethodException ex) {
            // start 関数は存在していなくても良い
        }
    }
    @Override
    public void stop() throws Exception {
        // 外部スクリプトに stop 関数が定義されていた場合は実行する
        try {
            ((Invocable) engine).invokeFunction("stop");
        } catch (NoSuchMethodException ex) {
            // stop 関数は存在していなくとも良い
        }
    }
    /**
     * 指定された JavaScript ファイルの読み込みと実行
     * @param path JavaScript ファイルの UTF-8 でエンコードされたパス名
     * @return 最後に評価された関数の返り値(使用されない)
     */
    private Object load(String path) {
        try {
            FileInputStream file = new FileInputStream(path);
            InputStreamReader input = new InputStreamReader(file, "UTF-8");
            return engine.eval(input);
        } catch (FileNotFoundException | UnsupportedEncodingException | ScriptException ex) {
            ex.printStackTrace();
        }
        return null;
    }
}

このプログラムのコンパイルは、まず Nashorn のリポジトリをダウンロードし、(cd make ; ant build-fxshell) を実行します。すると nashorn/dist/nashornfx.jar ファイルが作成されますので、java -cp dist/nashornfx.jar jdk.nashorn.tools.FXShell で実行出来ます。JDK に詳しい方は jdk/makefiles/CompileLaunchers.gmk の jjs エントリーの下に定義を書き加える事で、独自のランチャーを作成する事も可能です。

このシェルプログラム方式の優位点は、JavaFX の実行に必要な処理の殆ど全てをシェルプログラム側が引き受けてくれることです。このシェルプログラムを使ってコードを書く場合は、自分のスクリプトには start メソッドと幾つかのクラスの定義を記述するだけで済みます。このシェルプログラムを JDK に含ませる事が出来れば良いのですが、先ほどの依存関係により JDK に JavaFX に依存したコードを入れることが出来ません。

JavaFX へのアクセスのもう一つの実装方法は、jjs プログラムを利用する方法です。Nashorn リポジトリにある最新の Java.extend では、javafx.application.Application クラスのサブクラスを作成することが出来ます。これを利用して JavaFX にアクセスするプログラムを書くことが出来ます。プログラムの制御が JavaFX 側で管理されること、JavaFX の初期化処理を行う場所に制約があることに注意が必要です。

この実装方法の簡易的なプロトタイプとして fxinit.js を作成しました。見慣れない部分もあるかもしれませんが、中で実装している処理はとても簡単です。

GLOBAL = this;
javafx = Packages.javafx;
com.sun.javafx.application.LauncherImpl.launchApplication(
(Java.extend(javafx.application.Application, {
    init: function() {
        // FX のパッケージとクラスはこれより以前には使用出来ないため、
        // ここで定義する必要がある
        Stage          = javafx.stage.Stage;
        scene          = javafx.scene;
        Scene          = scene.Scene;
        Group          = scene.Group;
        chart          = scene.chart;
        control        = scene.control;
        Button         = control.Button;
        StackPane      = scene.layout.StackPane;
        FXCollections  = javafx.collections.FXCollections;
        ObservableList = javafx.collections.ObservableList;
        Chart          = chart.Chart;
        CategoryAxis   = chart.CategoryAxis;
        NumberAxis     = chart.NumberAxis;
        BarChart       = chart.BarChart;
        XYChart        = chart.XYChart;
        Series         = chart.XYChart$Series;
        Data           = chart.XYChart$Data;
        TreeView       = control.TreeView;
        TreeItem       = control.TreeItem;
        if (GLOBAL.init) {
            init();
        }
    },
    start: function(stage) {
        if (GLOBAL.start) {
            start(stage);
        }
    },
    stop: function() {
        if (GLOBAL.stop) {
            stop();
        }
    }
})).class, new (Java.type("java.lang.String[]"))(0));

このプログラムの使用方法は簡単です。JavaFX の HelloWorld.java サンプル を Nashorn 向けに JavaScript で書き直したプログラムがこちらです。

function start(stage) {
    stage.title = "Hello World!";
    var button = new Button();
    button.text = "Say 'Hello World'";
    button.onAction = function() print("Hello World!");
    var root = new StackPane();
    root.children.add(button);
    stage.scene = new Scene(root, 300, 250);
    stage.show();
}
load("fxinit.js");

load("fxinit.js") で fxinit.js を読み込む場所には注意が必要です。fxinit.js を読み込んだ時点でプログラムの制御が JavaFX 側に移ります。それより後ろに書いたコードはアプリケーション終了時に実行されることになってしまいます。

それから、JavaFX クラスの一部は JavaFX の実行が開始されていないと初期化する事が出来ません。その為 JavaScript プログラムのトップレベルでこれらのクラスを使用することは出来ません。必ず JavaFX の実行が開始されてから呼び出されるメソッドの中で使わなくてはいけません。この様に、fxinit.js の様な実装方法は、プログラムの書き方に制約が生じてしまいます。

これら以外に三番目の実装方法も検討しています。コマンドラインが複雑になりますが、jjs を使って jjs fxinit.js -- myscript.js -- my scripts args と実行する様なイメージです。ここでの -- は、引き続いて引数が出現する事を意味しています。この場合は jjs によってまず fxinit.js が読み込まれ、JavaFX が起動してから myscript.js が実行されます。この様にすれば、myscript.js の部分のプログラムはこれまで見てきたような制約が無く記述する事が出来ます。コマンドラインの記述が複雑になることだけが問題です。

皆さまのご意見をお聞かせ下さい。

木曜日 12 06, 2012

Twitter 上の Nashorn への反響(続き)

はじめに

Nashorn と Java を組み合わせて Twitter にアクセスするサンプルの続きです。今回は JavaFX を使用してグラフを表示します。

おことわり

以下は jlaskey による Nashorn Blog への投稿の翻訳です。原文は https://blogs.oracle.com/nashorn/entry/nashorn_in_the_twitterverse_continued でご覧頂けます。

訳文

Twitter にアクセスするサンプルが完成しましたので、今度は JavaFX を使用してグラフ化してみましょう。この記事を書いている時点では Nashorn には JavaFX 用のシェルがありませんので、JavaFX アプリケーションを作成するには少し工夫が必要になります。今回ご紹介する方法で、Nashorn と Java を相互に呼び出すプログラムのイメージを掴んで頂けるのではないかと思います(JavaFX 用のシェルは今後の実装予定に加える予定です)。

まずはアプリケーションの中で実質的な処理を行う部分を見て行きましょう。こちらが前回ご紹介した Twitter にアクセスするサンプルを書き直したプログラムです。

var twitter4j      = Packages.twitter4j;
var TwitterFactory = twitter4j.TwitterFactory;
var Query          = twitter4j.Query;

function getTrendingData() {
    var twitter = new TwitterFactory().instance;
    var query   = new Query("nashorn OR nashornjs");
    query.since("2012-11-21");
    query.count = 100;
    var data = {};

    do {
        var result = twitter.search(query);
        var tweets = result.tweets;
        for each (var tweet in tweets) {
            var date = tweet.createdAt;
            var key = (1900 + date.year) + "/" +
                      (1 + date.month) + "/" +
                      date.date;
            data[key] = (data[key] || 0) + 1;
        }
    } while (query = result.nextQuery());

    return data;
}

今回は、ツイートを表示する代わりに、getTrendingData() 関数の中でサンプリング期間中(このプログラムでは、Nashorn のプロジェクトが OpenJDK に申請された 2012 年 11 月 21 日以降)の日別のツイート数を計算し、結果をオブジェクトに格納して返しています。

続いて、JavaFX の BarChart でデータを表示します。

var javafx         = Packages.javafx;
var Stage          = javafx.stage.Stage
var Scene          = javafx.scene.Scene;
var Group          = javafx.scene.Group;
var Chart          = javafx.scene.chart.Chart;
var FXCollections  = javafx.collections.FXCollections;
var ObservableList = javafx.collections.ObservableList;
var CategoryAxis   = javafx.scene.chart.CategoryAxis;
var NumberAxis     = javafx.scene.chart.NumberAxis;
var BarChart       = javafx.scene.chart.BarChart;
var XYChart        = javafx.scene.chart.XYChart;
var Series         = javafx.scene.chart.XYChart.Series;
var Data           = javafx.scene.chart.XYChart.Data;

function graph(stage, data) {
    var root = new Group();
    stage.scene = new Scene(root);
    var dates = Object.keys(data);
    var xAxis = new CategoryAxis();
    xAxis.categories = FXCollections.observableArrayList(dates);
    var yAxis = new NumberAxis("Tweets", 0.0, 200.0, 50.0);
    var series = FXCollections.observableArrayList();
    for (var date in data) {
        series.add(new Data(date, data[date]));
    }
    var tweets = new Series("Tweets", series);
    var barChartData = FXCollections.observableArrayList(tweets);
    var chart = new BarChart(xAxis, yAxis, barChartData, 25.0);
    root.children.add(chart);
}

このサンプルプログラムの中には興味深い点が沢山あります。例えば、stage.scene = new Scene(root) は stage.setScene(new Scene(root)) と同じ処理をより簡潔に記述出来ています。Nashorn が stage オブジェクトに scene プロパティを見つけられなかった場合は、(Dynalink ライブラリを通して)Java Beans の命名規則的に同等となるメソッド (setScene()) を探して来るので、こういう記述が可能になっています。もう一つ、Nashorn は FXCollections 等の総称クラスのハンドリングも自動で行ってくれます。そして、observableArrayList(dates) 呼び出しの部分では、Nashorn が JavaScript の配列 (dates) を Java のコレクションに自動で変換しています。どのオブジェクトが JavaScript のオブジェクトで、どのオブジェクトが Java のオブジェクトであるかを判定するのはとても難しい問題ですが、それをプログラマが明示的に指定しなくても良い様になっています。

プログラムの本質的な処理の部分の説明は以上で終わりましたので、次は JavaFX と連携するための仕組みについて見て行きましょう。

JavaFX のプログラムを作成する際は、javafx.application.Application クラスのサブクラスをメインクラスにします。このクラスが JavaFX ライブラリの初期化とイベント処理を行います。こちらがこのサンプルプログラム用に作成したコードです。

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import javafx.application.Application;
import javafx.stage.Stage;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

public class TrendingMain extends Application {

    private static final ScriptEngineManager
                                        MANAGER = new ScriptEngineManager();
    private final ScriptEngine engine = MANAGER.getEngineByName("nashorn");
    private Trending trending;

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

    @Override
    public void start(Stage stage) throws Exception {
        trending = (Trending) load("Trending.js");
        trending.start(stage);
    }

    @Override
    public void stop() throws Exception {
        trending.stop();
    }

    private Object load(String script) throws IOException, ScriptException {
         try (final InputStream is = TrendingMain.class.getResourceAsStream(script)) {
            return engine.eval(new InputStreamReader(is, "utf-8"));
         }
    }
}

ここでは Nashorn の初期化の為に JSR-223 の javax.script を使用しています。

private static final ScriptEngineManager MANAGER = new ScriptEngineManager();
private final ScriptEngine engine = MANAGER.getEngineByName("nashorn");

コードのこの部分は JavaScript を処理するための Nashorn エンジンのインスタンスを作成しています。

load メソッドで外部スクリプトをメモリ上に読み込み、engine でそのスクリプトを評価します。load は評価した結果を返します。

ここからが特に面白い部分です。Java のメインクラスと外部スクリプトの間で相互にデータをやりとりする方法は何通りかありますが、このサンプルでは Java のインターフェイスを使用します。JavaFX のメインクラスは start メソッドと stop メソッドを実行する必要がありますので、この様なインターフェイスを作成します。

public interface Trending {
    public void start(Stage stage) throws Exception;
    public void stop() throws Exception;
}

そして、サンプルスクリプトの最後に、次のコードを追加します。

function newTrending() {
    return new Packages.Trending() {
        start: function(stage) {
            var data = getTrendingData();
            graph(stage, data);
            stage.show();
        },

        stop: function() {
        }
    }

}

newTrending();

このコードは Trending クラスのサブクラスのインスタンスを作成し、start メソッドと stop メソッドをオーバーライドしています。この関数から返されたオブジェクトが eval を通して Java のメインメソッドに返されます。

trending = (Trending) load("Trending.js");

全体の動きを簡単にまとめますと、Trending.js スクリプトには getTrendingData 関数の定義と、一番最後に newTrending 関数の呼び出しが実装されています。そこから Java のコードに戻って、newTrending 関数を評価している eval メソッドの返り値を Trending クラスにキャストしています。そして、その返ってきたオブジェクトを使用して、次のコードでスクリプトの実行を行います。

trending.start(stage);

これで完成です。

twitterverse

訳者補足

Nashorn のリンカーの動きは http://www.myexpospace.com/JavaOne2012/SessionFiles/CON5251_PDF_5251_0001.pdf をご参照下さい。 Dynalink に付きましては https://github.com/szegedi/dynalink をご参照下さい。

About

JavaVM 用 JavaScript エンジンの Nashorn について情報発信しているブログです。Nashorn の読み方はナズホーンです。

Search

Archives
« 4月 2014
  
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