Three Steps to Build a Killer WebSocket App with JavaFX

As part of my prep for the talk we give at JavaOne 2012, I built a WebSocket app using JavaFX 2.2 front-end with NetBeans 7.2 and the brand new JavaFX Scene Builder 1.0. The tools were a pleasant surprise, they were pretty straight-forward to use. Most of the Oracle tutorials were helpful too, although I couldn’t find signs of an active and extensive JavaFX developer community out there.

The app I wanted to build consumes the same data source as the lightning fast Kaazing portfolio demo.

This video demonstrates what it looks like in the development environment, as well as running, side-by-side with the aforementioned JavaScript implementation of the Kaazing portfolio demo.

Step 1 – Creating a JavaFX App

First, I created a new project: JavaFX > JavaFX FXML Application.

Step 2 – Defining the UI

Then, using the new JavaFX Scene Builder, I created the grid layout I wanted to render. The JavaFX Scene Builder can be invoked by double-clicking on your fxml file in NetBeans. If you want to edit the XML in NetBeans directly, there’s a context menu that allows you to do so.

Here’s the source for my Sample.fxml file:

[sourcecode language=”xml”]
<?xml version="1.0" encoding="UTF-8"?>

<?import fxmltableview.*?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.collections.*?>
<?import javafx.geometry.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.control.cell.*?>
<?import javafx.scene.image.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>
<?import javafxapplication4.*?>

<AnchorPane id="AnchorPane" prefHeight="443.999755859375" prefWidth="656.9998779296875" xmlns:fx="http://javafx.com/fxml" fx:controller="javafxapplication4.SampleController">
<children>
<BorderPane prefHeight="706.0" prefWidth="717.0" snapToPixel="false" AnchorPane.bottomAnchor="14.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="14.0" AnchorPane.topAnchor="0.0">
<right>
<TableView fx:id="tableView" editable="true" prefHeight="393.999755859375" prefWidth="555.9998779296875">
<columns>
<TableColumn prefWidth="240.0" text="Company" fx:id="company">
<cellValueFactory>
<PropertyValueFactory property="company" />
</cellValueFactory>
</TableColumn>
<TableColumn prefWidth="100.0" text="Ticker" fx:id="ticker">
<cellValueFactory>
<PropertyValueFactory property="ticker" />
</cellValueFactory>
</TableColumn>
<TableColumn prefWidth="100.0" text="Price" fx:id="price">
<cellValueFactory>
<PropertyValueFactory property="price" />
</cellValueFactory>
</TableColumn>
<TableColumn prefWidth="100.0" text="Change" fx:id="change">
<cellValueFactory>
<PropertyValueFactory property="change" />
</cellValueFactory>
</TableColumn>
</columns>
<BorderPane.margin>
<Insets bottom="50.0" left="50.0" right="50.0" top="50.0" />
</BorderPane.margin>
</TableView>
</right>
<top>
<Pane prefHeight="32.0" prefWidth="745.0">
<children>
<Label alignment="CENTER" contentDisplay="CENTER" layoutX="14.0" layoutY="5.0" prefWidth="614.9998779296875" text="WebSocket Portfolio Demo" textAlignment="CENTER" textFill="#cc6200">
<font>
<Font name="System Bold" size="24.0" />
</font>
</Label>
</children>
</Pane>
</top>
</BorderPane>
</children>
</AnchorPane>
[/sourcecode]

 

Then, I defined the data model driving my TableView. I created a JavaBean, called Stock, with four private variables. The first three: company, ticker, and price, represent the first three columns of my TableView. The fourth one is a computed value, displaying the change of the price of a stock.

[sourcecode language=”Java”]
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package javafxapplication4;

import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

/**
*
* @author pmoskovi
*/
public class Stock {

private SimpleStringProperty company;
private SimpleStringProperty ticker;
private SimpleStringProperty price;
private SimpleStringProperty change;

public Stock () {
setCompany("");
setTicker("");
setPrice("");
}

public Stock(String company, String ticker, String price, String shares, String value) {
setCompany(company);
setTicker (ticker);
setPrice (price);
}

public String getCompany() {
return companyProperty().get();
}

public void setCompany(String company) {
companyProperty().set(company);
}

public StringProperty companyProperty() {
if (company == null) {
company = new SimpleStringProperty(this, "company");
}
return company;
}

public String getTicker() {
return tickerProperty().get();
}

public void setTicker(String ticker) {
tickerProperty().set(ticker);
}

public StringProperty tickerProperty() {
if (ticker == null) {
ticker = new SimpleStringProperty(this, "ticker");
}
return ticker;
}

public String getPrice() {
return priceProperty().get();
}

public void setPrice(String price) {
priceProperty().set(price);
}

public StringProperty priceProperty() {
if (price == null) {
price = new SimpleStringProperty(this, "price");
}
return price;
}

public String getChange() {
return changeProperty().get();
}

public void setChange(String change) {
changeProperty().set(change);
}

public StringProperty changeProperty() {
if (change == null) {
change = new SimpleStringProperty(this, "change");
}
return change;
}
}
[/sourcecode]

Step 3 – Feeding the application with data through WebSockets

Lastly, by simply following the How to Build Java Client Using Kaazing WebSocket Gateway I created the WebSocket connection, subscribed to the stock data feed, and updated my model which in turn refreshed the TableView in the screen.

The app uses the Kaazing WebSocket Gateway, allowing you to invoke JMS APIs directly in the JavaFX application code.

[sourcecode language=”Java”]
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package javafxapplication4;

import java.net.URL;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
import java.util.Properties;
import java.util.ResourceBundle;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Label;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.ExceptionListener;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageConsumer;
import javax.jms.MessageListener;
import javax.jms.Session;
import javax.jms.TextMessage;
import javax.jms.Topic;
import javax.naming.Context;
import javax.naming.InitialContext;

/**
*
* @author pmoskovi
*/
public class SampleController implements Initializable {

private ObservableList data;
@FXML
private Label label;
@FXML
private TableView tableView;
@FXML
private TableColumn&lt;Stock, String&gt; change;
ListIterator it;

public void startRendering() {

addStock("3mCo", "MMM", "", "", "");
addStock("AT&amp;T Inc", "T", "", "", "");
addStock("Boeing Co", "BA", "", "", "");
addStock("Citigroup", "C", "", "", "");
addStock("Hewlett-Packard Co", "HPQ", "", "", "");
addStock("Intel Corporation", "INTC", "", "", "");
addStock("International Business Machines", "IBM", "", "", "");
addStock("McDonald’s Cororation", "MCD", "", "", "");
addStock("Microsoft Corporation", "MSFT", "", "", "");
addStock("Verizon Communications", "VZ", "", "", "");
addStock("Wal-Mart Stores Inc", "WMT", "", "", "");

doConnect();
}

protected void addStock(String company, String ticker, String price, String shares, String value) {
ObservableList data = tableView.getItems();
data.add(new Stock(company, ticker, price, shares, value));
}

@Override
public void initialize(URL url, ResourceBundle rb) {
startRendering();
}

protected void doConnect() {
Properties props = new Properties();
props.put(InitialContext.INITIAL_CONTEXT_FACTORY,
"com.kaazing.gateway.jms.client.stomp.StompInitialContextFactory");

//WebSocket end-point
props.put(Context.PROVIDER_URL, "ws://demo.kaazing.com/jms");
InitialContext ctx;
final ObservableList data = tableView.getItems();

try {
ctx = new InitialContext(props);
ConnectionFactory connectionFactory;
connectionFactory = (ConnectionFactory) ctx.lookup("ConnectionFactory");

//Creating WebSocket connection
Connection connection = connectionFactory.createConnection(null, null);
connection.setExceptionListener(new ExceptionListener() {
@Override
public void onException(JMSException jmse) {
jmse.printStackTrace();
}
});

//Creating JMS session
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);

//Creating JMS topic
Topic topic = (Topic) ctx.lookup("/topic/portfolioStock");

//Creating JMS consumer
MessageConsumer consumer = session.createConsumer(topic);
consumer.setMessageListener(new MessageListener() {
@Override
public void onMessage(Message msg) {
it = data.listIterator();

try {
// Creating a String array out of the incoming message: ticker, company, price
String[] stockMsgArr = parseList(((TextMessage) msg).getText(), ":");

// System.out.println(stockMsgArr[0] + ":" + stockMsgArr[1] + ":" + stockMsgArr[2]);
// Loop through all the elements of the viewTable model
while (it.hasNext()) {
// st holds the current row of tableView
Stock st = (Stock)it.next();
// System.out.println("st.getTicker() " + st.getTicker() + " | " + "stockMsgArr[1] " + stockMsgArr[1]);

if (st.getTicker().equals(stockMsgArr[1])) {
Float oldValue, newValue;
newValue = Float.parseFloat(stockMsgArr[2]);
oldValue = Float.parseFloat((st.getPrice().equals("")) ? "0" : st.getPrice());
// System.out.println ("oldValue: " + oldValue + " | newValue: " + newValue);
((Stock)(it.previous())).setChange(new DecimalFormat("#.##").format((newValue-oldValue)));
st.setPrice (newValue.toString());
break;
}
}
} catch (JMSException e) {
e.printStackTrace();
}
}
});
connection.start();
} catch (Exception ex) {
Logger.getLogger(SampleController.class.getName()).log(Level.SEVERE, null, ex);
}
}

public static String[] parseList(String list, String delim) {
List result = new ArrayList();
StringTokenizer tokenizer = new StringTokenizer(list, delim);
while (tokenizer.hasMoreTokens()) {
result.add(tokenizer.nextToken());
}
return (String[]) result.toArray(new String[0]);
}

}
[/sourcecode]

If you’re attending JavaOne this year, come to our session for goodies and cool demos on WebSockets.

To learn more about the JMS APIs exposed to Web clients, or if you want to see step-by-step instructions on building a true HTML5 WebSocket app using the JavaScript API, check out these Kaazing tutorials.