Java Chat Application

If you look around the Kaazing open source web site, you will see a lot of information about delivering real-time solutions on the Web. This is not by accident – we a strong believers in the future of web standards. You can also use Kaazing Gateway with other platforms as well, including Java, Android, and iOS. In this post, we will take a quick look at building a chat application using Java.

Desktop

As a cross-platform technology, Java can run in many places. Indeed, Kaazing Gateway itself is built on Java and intended to run on the server. Since we are talking about a chat application here, we will be focusing on the client side library usage. To avoid the complexity of mobile deployment, we will be developing a desktop Java application.

The goal of this Java desktop chat application is to integrate with the web standards chat application from a previous blog post. You can read the article and even use the live application (desktop or mobile). This means that Kaazing Gateway brings real-time cross-platform communication to your projects. I will be following this post up by bringing the chat application over to Android, and eventually iOS.

Libraries

As an open source project, you have access to all the client library code you could want. Many of the repositories include build instructions as well. However, if you are just getting started exploring Kaazing Gateway features by building this chat client, then I am guessing that you are not ready to commit code to the project. Our first step then is to get the libraries we need in a form that is ready to drop into our IDE.

As it turns out, the build processes of the open source repositories use Apache Maven for project management. This means that we can head on over to The Central Repository and pick up the ready-to-use JAR files for our chat client. There are four different libraries that we will need.

The first library we will need is Gateway client (GitHub). This effectively implements a WebSocket client, which is the baseline for communication to Kaazing Gateway. You will also need Gateway client transport (GitHub). This library implements the transport layer for Kaazing Gateway WebSocket Java client library. Next up is an AMQP client (GitHub) implementation. I wrote extensively about AMQP in an earlier post.

Since the Web client from the previous post uses JSON (JavaScript Object Notation) to communication messages, we also need the ability to handle JSON in Java. The Glassfish JSON library is an open source implementation of JSR-353, which provides a Java API for JSON processing. Paired with the Kaazing libraries, we have everything we need for our chat client.

User Interface

Building Java desktop user interfaces is something that most people remember either as a painful part of learning Java, or as an empowering art form. I actually fall into the later group, and as such have made the Java desktop client look and behave exactly like the Web client. In the interest of brevity, and perhaps sanity, I will not be covering all the details of JList and custom cell renderers, or other fun Swing internals.

You might now be breathing a sigh of relief, but if you really want to take a look under the covers, the entire project (minus the libraries) is available on my Kaazing GitHub repository.

Connecting

The first thing we need to do, is to establish a connection to Kaazing Gateway. Do not worry if you do not have a build running locally, or on some server in your DMZ, we have a publicly available instance for you to use.

[sourcecode language=”java”]
// Establish connection
private void initConnection()
{
// Factory
factory = AmqpClientFactory.createAmqpClientFactory();

try {
// Client
client = factory.createAmqpClient();

// Connection listeners
client.addConnectionListener( new ConnectionListener() {

// Connecting
public void onConnecting( ConnectionEvent ce )
{
EventQueue.invokeLater( new Runnable() {
public void run()
{
System.out.println( "Connecting…" );
}
} );
}

// Error
public void onConnectionError( ConnectionEvent ce )
{
EventQueue.invokeLater( new Runnable() {
public void run()
{
System.out.println( "Connection error." );
}
} );
}

// Open
public void onConnectionOpen( ConnectionEvent ce )
{
EventQueue.invokeLater( new Runnable() {
public void run()
{
System.out.println( "Connection open." );

// Setup publisher
doClientOpen();
}
} );
}

// Close
public void onConnectionClose( ConnectionEvent ce )
{
EventQueue.invokeLater( new Runnable() {
public void run()
{
System.out.println( "Connection closed." );
}
} );
}
} );

// Connect to server
client.connect(
"wss://demos.kaazing.com/amqp",
"/",
"guest",
"guest"
);
} catch( Exception e ) {
e.printStackTrace();
}
}
[/sourcecode]

In connecting, there are many event listeners you can use. For example, you might use the open and close event listeners to show a visual indicator in the user interface. Right now, all we are interested in knowing is when the connection is ready to use which is handled in the “onConnectionOpen” event.

Channels

To publish and consume messages, we will create an AMQP channel for each operation. Similar to opening the connection, there are many event listeners that you can have. We are going to start with the publish channel, which effectively boils down to declaring the exchange. An exchange sits on the message broker (server), and acts as a bucket for incoming messages.

[sourcecode language=”java”]
private void doClientOpen()
{
// Send messages
publish = client.openChannel();

// Channel listeners
publish.addChannelListener( new ChannelAdapter() {
// Close
public void onClose( ChannelEvent ce )
{
EventQueue.invokeLater( new Runnable() {
public void run()
{
System.out.println( "Publish closed." );
}
} );
}

// Error
public void onError( ChannelEvent ce )
{
EventQueue.invokeLater( new Runnable() {
public void run()
{
System.out.println( "Publish error." );
}
} );
}

// Declare exchange
public void onDeclareExchange( ChannelEvent ce )
{
EventQueue.invokeLater( new Runnable() {
public void run()
{
System.out.println( "Exchange declared." );

// Setup consumer
doPublishReady();
}
} );
}

// Open
public void onOpen( ChannelEvent ce )
{
EventQueue.invokeLater( new Runnable() {
public void run()
{
System.out.println( "Publish open." );

// Declare exchange
publish.declareExchange(
"exchange_WLRNhKKM7d",
"direct",
false,
false,
false,
null
);
}
} );
}
} );
}
[/sourcecode]

With a publish exchange created, we can now move onto creating a consumer queue and binding it to the exchange. The consume channel is the first place your message will arrive in the Java client. AMQP is a binary protocol, so we get the bytes first. Since we are working with JSON from the Web client, we will want the String equivalent of the message payload. We can then process the content however we feel best fit – more on that in the next section.

[sourcecode language=”java”]
private void doPublishReady()
{
// Consume
consume = client.openChannel();

// Channel listeners
consume.addChannelListener( new ChannelAdapter() {
// Bind queue
public void onBindQueue( ChannelEvent ce )
{
EventQueue.invokeLater( new Runnable() {
public void run()
{
System.out.println( "Queue bound." );
}
} );
}

// Close
public void onClose( ChannelEvent ce )
{
EventQueue.invokeLater( new Runnable() {
public void run()
{
System.out.println( "Consume closed." );
}
} );
}

// Consume
public void onConsumeBasic( ChannelEvent ce )
{
EventQueue.invokeLater( new Runnable() {
public void run()
{
System.out.println( "Consuming…" );

// Open user interface for sending messages
doConsumeReady();
}
} );
}

// Declare queue
public void onDeclareQueue( ChannelEvent ce )
{
EventQueue.invokeLater( new Runnable() {
public void run()
{
System.out.println( "Queue declared." );
}
} );
}

// Flow
public void onFlow( ChannelEvent ce )
{
try {
final boolean isActive = ce.isFlowActive();

EventQueue.invokeLater( new Runnable() {
public void run()
{
System.out.println( "Flow is " + ( isActive ? "on" : "off" ) + "." );
}
} );
} catch( Exception e ) {
e.printStackTrace();
}
}

// Message
public void onMessage( ChannelEvent ce )
{
byte[] bytes;

bytes = new byte[ce.getBody().remaining()];
ce.getBody().get( bytes );

final Long tag = ( Long )ce.getArgument( "deliveryTag" );
final String value = new String( bytes, Charset.forName( "UTF-8" ) );

EventQueue.invokeLater( new Runnable() {
public void run()
{
AmqpChannel channel = null;

System.out.println( "Message: " + value );

// Place in user interface
doMessageArrived( value );

// Acknowledge
channel = ce.getChannel();
channel.ackBasic( tag.longValue(), true );
}
} );
}

// Open
public void onOpen( ChannelEvent ce )
{
EventQueue.invokeLater( new Runnable() {
public void run()
{
System.out.println( "Consume open." );

// Declare queue
// Bind queue to exchange
// Start consuming
consume.declareQueue(
"queue_AND_123",
false,
false,
false,
false,
false,
null
).bindQueue(
"queue_AND_123",
"exchange_WLRNhKKM7d",
"chat_topic",
false,
null
).consumeBasic(
"queue_AND_123",
"start_tag",
false,
false,
false,
false,
null
);
}
} );
}
} );
}
[/sourcecode]

Because of the way we have setup our connection, messages must be acknowledged. This is the last bit of code in the message event handler. Without this, the exchange on the broker will effectively hold onto the messages. This is actually a desired behavior for the purposes of message persistence and guaranteed delivery. Long-term however this can mean that your server fills up and runs out of memory. If you are not interested in persistence and guaranteed delivery, you can configure the exchange to not require message acknowledgement.

Parsing JSON

As previously mentioned, messages from the Web client are in JSON format. This means we need to parse the message content into an equivalent Java data type. Parsing JSON in Java reminds me of parsing XML in Java using SAX. The parser effectively rips through the content, while your code looks for specific elements that you are interested in further processing.

[sourcecode language=”java”]
private void doMessageArrived( String body )
{
ChatMessage message = null;
Event e = null;
InputStream stream = null;
JsonParser parser = null;

// String to InputStream
stream = new ByteArrayInputStream(
body.getBytes( StandardCharsets.UTF_8 )
);
parser = Json.createParser( stream );

// New chat message
message = new ChatMessage();
message.raw = body;

// Parse JSON
while( parser.hasNext() )
{
e = parser.next();

if( e == Event.KEY_NAME )
{
switch( parser.getString() )
{
case "color":
parser.next();
message.color = parseRgb( parser.getString() );
break;

case "message":
parser.next();
message.content = parser.getString();
break;

case "user":
parser.next();
message.user = parser.getString();
break;
}
}
}

history.addElement( message );
}
[/sourcecode]

In order to hold the message content, and render it in a JList, I have created a custom data type called ChatMessage. It simply has a few public properties on it to hold the specific pieces of data. Color from the Web client is in CSS format of “rgb( 255, 255, 255 )”. This is further parsed into a Java Color object. The JList in turn has a custom cell renderer to show the message in the color provided by the sending client.

Publish

Publishing a message that can be consumed by a Web client effectively means encoding our Java data types into their corresponding JSON format. Again, the process is very similar to SAX. The JSR provides for a builder object. Properties are added to the builder. To get the String format of the JSON data, we use the JSR-provided writer object.

[sourcecode language=”java”]
public void keyReleased( KeyEvent ke )
{
AmqpProperties properties = null;
ByteBuffer buffer = null;
JsonObject result = null;
JsonObjectBuilder builder = null;
StringWriter sw = null;
Timestamp stamp = null;

// There is a message to send
if( ke.getKeyCode() == 10 && field.getText().trim().length() > 0 )
{
// Build JSON object
// Interacting with the web
builder = Json.createObjectBuilder();
builder.add( "message", field.getText().trim() );
builder.add(
"color",
"rgb( " + style.getRed() +
", " + style.getGreen() +
", " + style.getBlue() +
" )"
);
builder.add( "user", "user_" + now );

result = builder.build();

// Java JSON object to String
sw = new StringWriter();

try( JsonWriter writer = Json.createWriter( sw ) ) {
writer.writeObject( result );
}

// Here is what we are going to send
System.out.println( "Sending: " + sw.toString() );

// Encode for AMQP
buffer = ByteBuffer.allocate( 512 );
buffer.put( sw.toString().getBytes( Charset.forName( "UTF-8" ) ) );
buffer.flip();

stamp = new Timestamp( System.currentTimeMillis() );

// Publish parameters
properties = new AmqpProperties();
properties.setMessageId( "1" );
properties.setCorrelationId( "4" );
properties.setAppId( "java_chat" );
properties.setUserId( "user_" + now );
properties.setContentType( "text/plain" );
properties.setContentEncoding( "UTF-8" );
properties.setPriority( 6 );
properties.setDeliveryMode( 1 );
properties.setTimestamp( stamp );

// Send
publish.publishBasic(
buffer,
properties,
"exchange_WLRNhKKM7d",
"chat_topic",
false,
false
);

// Clear text just sent
field.setText( "" );
}
}
[/sourcecode]

Once we have the JSON representation of the outgoing chat message, we create the AMQP message proper. This largely consists of setting various properties the correspond to how the broker should handle the message. After that, we use our previous instantiated publish channel and send the message itself, and the properties, to the exchange on the broker.

Next Steps

The event handlers in the code can seem overwhelming at first – there are so many of them. Remember however that they are predominantly a convenience to provide a better behaving application. Do not let them get in your way of opening your favorite Java IDE and giving the chat client a try.

Once you have the Java client running, you can head over to the Kaazing open source web site and run the live chat demonstration. The two clients will be able to communicate with one another in real-time. The cross-platform goodness does not stop there either. I will write more in the future on using Kaazing Gateway with Android, iOS, and IoT.