はじめに
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);
これで完成です。
訳者補足
Nashorn のリンカーの動きはhttp://www.myexpospace.com/JavaOne2012/SessionFiles/CON5251_PDF_5251_0001.pdfをご参照下さい。 Dynalink に付きましてはhttps://github.com/szegedi/dynalinkをご参照下さい。