Nashorn in the Twitterverse, Continued
- by jlaskey
After doing the Twitter example, it seemed reasonable to try graphing the result with JavaFX. At this time the Nashorn project doesn't have an JavaFX shell, so we have to go through some hoops to create an JavaFX application. I thought showing you some of those hoops might give you some idea about what you can do mixing Nashorn and Java (we'll add a JavaFX shell to the todo list.)
First, let's look at the meat of the application. Here is the repackaged version of the original twitter example.
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 (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;
}
Instead of just printing out tweets, getTrendingData tallies "tweets per date" during the sample period (since "2012-11-21", the date "New Project: Nashorn" was posted.) getTrendingData then returns the resulting tally object.
Next, use JavaFX BarChart to display that data.
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 = XYChart.Series;
var Data = 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);
}
I should point out that there is a lot of subtlety going on in the background. For example;
stage.scene = new Scene(root) is equivalent to stage.setScene(new Scene(root)). If Nashorn can't find a property (scene), then it searches (via Dynalink) for the Java Beans equivalent (setScene.) Also note, that Nashorn is magically handling the generic class FXCollections. Finally, with the call to observableArrayList(dates), Nashorn is automatically converting the JavaScript array dates to a Java collection. It really is hard to identify which objects are JavaScript and which are Java. Does it really matter?
Okay, with the meat out of the way, let's talk about the hoops.
When working with JavaFX, you start with a main subclass of javafx.application.Application. This class handles the initialization of the JavaFX libraries and the event processing. This is what I used for this example;
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"));
}
}
}
To initialize Nashorn, we use JSR-223's javax.script.
private static final ScriptEngineManager MANAGER = new ScriptEngineManager();
private final ScriptEngine engine = MANAGER.getEngineByName("nashorn");
This code sets up an instance of the Nashorn engine for evaluating scripts.
The load method reads a script into memory and then gets engine to eval that script. Note, that load also returns the result of the eval.
Now for the fun part. There are several different approaches we could use to communicate between the Java main and the script. In this example we'll use a Java interface. The JavaFX main needs to do at least start and stop, so the following will suffice as an interface;
public interface Trending {
public void start(Stage stage) throws Exception;
public void stop() throws Exception;
}
At the end of the example's script we add;
(function newTrending() {
return new Packages.Trending() {
start: function(stage) {
var data = getTrendingData();
graph(stage, data);
stage.show();
},
stop: function() {
}
}
})();
which instantiates a new subclass instance of Trending and overrides the start and stop methods. The result of this function call is what is returned to main via the eval.
trending = (Trending) load("Trending.js");
To recap, the script Trending.js contains functions getTrendingData, graph and newTrending, plus the call at the end to newTrending. Back in the Java code, we cast the result of the eval (call to newTrending) to Trending, thus, we end up with an object that we can then use to call back into the script.
trending.start(stage);
Voila.
?