Granite Data Services » Real-Time Messaging Tutorial with JavaFX 2.2

Real-Time Messaging Tutorial with JavaFX 2.2

One of the key features of GraniteDS is its real-time messaging stack, relying on and long-polling. While GraniteDS 3.0.0.M1 introduces an experimental WebSocket support for JavaFX and Flex, this tutorial will focus on a sample JavaFX chat application based on a more conservative setup of Servlet 3.0 asynchronous processing.

For the sake of simplicity, this sample doesn’t require any server side setup: it connects to a Tomcat 7 server, located at the URL, where you will find an existing Flex 4 Chat application.

The architecture of this sample is straightforward: both JavaFX and Flex client applications connect to a GraniteDS asynchronous servlet, which simply dispatch user inputs to all connected users. Both applications rely on the binary AMF3 protocol for exchanging structured data, even if this basic sample only sends and receives Strings.

This tutorial will guide you through the key steps to setup, build and run a Chat JavaFX client application connected to a GraniteDS backend.

1. Requirements

  • (Java client librairies and dependencies)
  • (tested with Juno SR1 64bits, but it should work with prior versions)

2. Setting up the Chat / JavaFX Project

Start Eclipse and create a new Java project named “chat-javafx” (accept all default settings).

Create a new folder in the project and name it “libs”. From the granite 3.0.0.M1 distribution, add the following jars, located in the libraries/java-client directory:

  • granite-client.jar
  • granite-java-client.jar
  • commons-codec-1.6.jar
  • commons-logging-1.1.1.jar
  • httpasyncclient-4.0-beta3.jar
  • httpclient-4.2.1.jar
  • httpcore-4.2.2.jar
  • httpcore-nio-4.2.2.jar

Even if we are creating a GraniteDS / JavaFX project here, we don’t need to include granite-javafx-client.jar, which is only required for advanced data management. The last six librairies come from the project and are required for HTTP asynchronous calls to the GraniteDS backend.

Now, add those libraries to the build path of your project: select all height libraries, right click on them, then select “Build Path” -> “Add to Build Path”.

Finally, you need to complete the build path of your project by adding the jfxrt.jar library, which is not part of the JavaSE-1.7 installed JRE under Eclipse: right click on “Referenced Libraries” in the project and select “Build Path” -> “Configure Build Path…”. Click on the “Add External JARs…” button and locate the jfxrt.jar in your JavaSE-1.7 installation (under jre/lib). Select it and click on “Ok”.

Your project is now fully configured for that basic GraniteDS / JavaFX Chat project and should look as follow:

Make sure the selected JRE System Library is JavaSE-1.7 and that jfxrt.jar is in your References Libraries (its location can of course differ from the one on the picture, depending on your platform).

3. Writing the Chat class

Create a new HelloWorld class in the “src” source directory with its package set to “org.granite.client.examples.helloworld”. Then, copy-paste the following code:

package org.granite.examples.javafx.chat;



import java.net.URI;

import java.util.concurrent.ExecutionException;

import java.util.concurrent.TimeoutException;



import javafx.application.Application;

import javafx.event.EventHandler;

import javafx.geometry.Insets;

import javafx.scene.Group;

import javafx.scene.Scene;

import javafx.scene.control.TextArea;

import javafx.scene.control.TextField;

import javafx.scene.input.KeyEvent;

import javafx.scene.layout.VBox;

import javafx.stage.Stage;



import org.granite.client.messaging.Consumer;

import org.granite.client.messaging.Producer;

import org.granite.client.messaging.TopicMessageListener;

import org.granite.client.messaging.channel.MessagingChannel;

import org.granite.client.messaging.channel.amf.AMFMessagingChannel;

import org.granite.client.messaging.events.TopicMessageEvent;

import org.granite.client.messaging.transport.HTTPTransport;

import org.granite.client.messaging.transport.apache.ApacheAsyncTransport;



public class Chat extends Application {



    private HTTPTransport transport;

    private MessagingChannel channel;

    private Consumer consumer;

    private Producer producer;

    

    private TextField input;

    private TextArea output;

    

    public static void main(String[] args) {

        Application.launch(args);

    }



    @Override

    public void init() throws Exception {

        super.init();



        transport = new ApacheAsyncTransport();

        transport.start();

        

        URI uri = new URI("http://demo.graniteds.org/chat/gravity/amf");

        channel = new AMFMessagingChannel(transport, "graniteamf", uri);

        

        consumer = new Consumer(channel, "gravity", "discussion");

        producer = new Producer(channel, "gravity", "discussion");

    }



    @Override

    public void start(Stage stage) {

        

        final String username = System.getProperty("user.name");

        

        stage.setTitle("Chat GraniteDS/JavaFX");

        stage.setResizable(false);



        Group root = new Group();

        stage.setScene(new Scene(root));

        

        VBox box = new VBox();

        box.setPadding(new Insets(8.0));

        box.setSpacing(8.0);

        root.getChildren().add(box);

        

        output = new TextArea();

        box.getChildren().add(output);

        output.setEditable(false);

        output.setStyle("-fx-border-style: none");

        output.setFocusTraversable(false);

        

        input = new TextField();

        box.getChildren().add(input);

        input.setOnKeyTyped(new EventHandler<KeyEvent>() {

            

            private int index = 1;

            

            @Override

            public void handle(KeyEvent event) {

                if ("r".equals(event.getCharacter()) || "n".equals(event.getCharacter())) {

                    producer.publish("[" + username + " #" + (index++) + "] " + input.getText().trim());

                    input.setText("");

                }

            }

        });



        stage.show();

        input.requestFocus();



        consumer.addMessageListener(new TopicMessageListener() {

            

            @Override

            public void onMessage(TopicMessageEvent event) {

                output.appendText((String)event.getData() + "n");

            }

        });

        try {

            consumer.subscribe().get();

        } catch (InterruptedException | ExecutionException | TimeoutException e) {

            throw new RuntimeException(e);

        }



        producer.publish("[" + username + " has just connected]");

    }



    @Override

    public void stop() throws Exception {

        if (consumer != null && consumer.isSubscribed())

            consumer.unsubscribe().get();

        

        transport.stop();



        super.stop();

    }

}

The Chat project is now ready and should compile without any issues.

3. Running the Application

Right click on the “Chat.java” class in your Eclipse Package Explorer and select “Run As” -> “Java Application”. The application should show up, saying that you are connected as “JavaFX” and letting you entering some input, followed by the <enter> or <return> key:

Chatting with yourself isn’t very exiting so you had better to find a friend running the application at the same time. Note that the server being public you could end up chatting with other testers from different locations.

4. Understanding the Highlighted Code

After having (a lot) of fun with the application, it’s time to understand what is going on under the hood.

Let’s look at the first part of the highlighted code (lines 45-52):

transport = new ApacheAsyncTransport();

transport.start();



URI uri = new URI("http://demo.graniteds.org/chat/gravity/amf");

channel = new AMFMessagingChannel(transport, "graniteamf", uri);



consumer = new Consumer(channel, "gravity", "discussion");

producer = new Producer(channel, "gravity", "discussion");

This part of code is executed at initialization time and proceed as follow:

  1. A new ApacheAsyncTransport instance is created and stored into a class variable of the HttpTransport type (which is an interface). This transport is then started and ready to send asynchronous HTTP requests.
  2. A new AMFMessagingChannel instance is created and bound to the transport, a channel id (“graniteamf”) and the URI of a GraniteDS servlet handling AMF3 asynchronous requests. You’ll need to have a closer look at the server-side to fully understand the meaning of these settings (see below).
  3. Then, a Consumer instance is created, bound to the channel, a destination id (“gravity”) which identifies the real-time messaging service on the server-side and a topic (“discussion”).
  4. Finally, a Producer is created, bound to the same channel, destination id and topic.

What we get at the end of this initialization is basically an object (producer) from which we can publish messages in the “discussion” topic and another object (consumer) which will receive all messages published on that same topic.

The second part of the highlighted code is responsible of publishing the user input:

producer.publish("[" + username + " #" + (index++) + "] " + input.getText().trim());

For a given username (say “johndoe”) and input (say “Bla bla”), the producer will publish the message “[johndoe #1] Bla bla”, the index (“#1″) being an incremental counter of the successive messages sent.

The third part of the highlighted code is setting up the consumer handler, subscription to the topic and publishing an initial message:

consumer.addMessageListener(new TopicMessageListener() {

            

    @Override

    public void onMessage(TopicMessageEvent event) {

        output.appendText((String)event.getData() + "n");

    }

});

try {

    consumer.subscribe().get();

} catch (InterruptedException | ExecutionException | TimeoutException e) {

    throw new RuntimeException(e);

}



producer.publish("[" + username + " has just connected]");

A TopicMessageListener is attached to the consumer and displays received messages in the text area at the top of the application. Then, the consumer subscribes to the “discussion” topic, waiting for the subscription completion (consumer.subscribe().get()): the subscribe method is asynchronous and returns a Future which can be used to wait for an acknowledgment message from the server.

Finally, a first message is published by the producer, announcing a new connection to the chat topic (eg. “[johndoe has just connected]“).

The final part of the highlighted code unsubscribes the consumer and stops the transport when the application is closed:

if (consumer != null && consumer.isSubscribed())

    consumer.unsubscribe().get();

        

transport.stop();

Again, the unsubscribe call is asynchronous and the get() call is blocking until the consumer receives an acknowledgment from the server.

All resources allocated by the transport are then released, in particular the underlying thread pool managing asynchronous requests: failing to call the transport stop method can lead to improper thread closing, leaving the application in a kind of daemon state.

4. A Look at the Server Code

In the “samples” directory of the GraniteDS 3.0.0.M1 distribution, you will find a file named “sample_projects.zip”. You can unzip it at location of your choice or import it as an existing projets archive under Eclipse.

Let’s look at the file that explains the above configuration of the Consumer and Producer (chat/war/WEB-INF/flex/services-config.xml):

<services-config>



    <services>

        <service id="messaging-service"

            class="flex.messaging.services.MessagingService"

            messageTypes="flex.messaging.messages.AsyncMessage">

            <adapters>

                <adapter-definition id="default" class="org.granite.gravity.adapters.SimpleServiceAdapter" default="true"/>

            </adapters>



            <destination id="gravity">

                <channels>

                    <channel ref="gravityamf"/>

                </channels>

            </destination>

        </service>

    </services>



    <channels>

        <channel-definition id="gravityamf" class="org.granite.gravity.channels.GravityChannel">

            <endpoint

                uri="http://{server.name}:{server.port}/{context.root}/gravity/amf"

                class="flex.messaging.endpoints.AMFEndpoint"/>

        </channel-definition>

    </channels>



</services-config>

Let’s start with the channel definition: an asynchronous real-time channel identified by “gravityamf” is bound to an uri which resolves to “http://demo.graniteds.org/chat/gravity/amf”. If you look at the web.xml file, this is where the GraniteDS (Gravity) servlet is handling incoming AMF messages. The configuration of the AMFMessagingChannel in the JavaFX Chat class reflects these settings.

Then, jump to the beginning of the file and look at the service definition: a destination “gravity” is declared, bound to the “gravityamf” channel. This explains the configuration of our Consumer and Producer as well.

The last thing to understand is that we don’t need to declare any predefined topic on the server-side: Gravity, the real-time messaging engine of GraniteDS, has a built-in support for basic topic creation and handling. That’s why we can define the “discussion” topic in the client code, without any further configuration on the server. You can change it to whatever you want (say “my-private-discussion”) and initiate a private chat on this new topic.

Tags:

Author: Franck

This post has 2 Comments

  1. on November 29, 2012 at 9:08 am Reply

    Nice just a minor note. If you are using Eclipse and want a plugin to support you with your JavaFX development you should take a look at which provides an eclipse plugin to make development easier.

    E.g. at the moment your project setting can’t be shared with other users because you are directly pointing to the jfxrt.jar in your JRE.

    • William on November 29, 2012 at 7:51 pm Reply

      Hi Tom, thanks for the comment.
      We definitely have to take a look to the existing Eclipse tooling for JavaFX, notably efxclipse.

Leave a Comment