Java Web Application sending JSON messages through WebSocket to HTML5 browser application for real time push

5
Share this on .. Tweet about this on TwitterShare on LinkedIn1Share on Facebook10Share on Google+0Email this to someoneShare on Tumblr0Buffer this page

imageThis article describes a Java EE 7 web application that exposes a REST service that handles HTTP POST requests with JSON payload. Any message received is sent through a Web Socket to the web socket (server) endpoint that is published by a Java Class deployed as part of the web application. A static HTML page with two associated JavaScript libraries is opened in a web browser and has opened a web socket connection to this same end point. The message sent from the REST service endpoint to the web socket endpoint is pushed through the web socket to the browser and used to instantly update the web page.

The specific use case that is implemented is a simple web dashboard to monitor a movie theater: the current number of people in each of the four rooms of this movie theater is observed. The REST service receives the actual spectator count and through the two web socket interactions, this count ends up in the browser and in the visual presentation.

Below you will find a step by step instruction for implementing this use case including all required source code. The implementation uses only standard technologies: Java EE 7 (including JAX-RS and Web Socket ) and plain HTML5 and JavaScript – no special libraries are involved. The code is developed in Oracle JDeveloper (12c) and deployed to Oracle WebLogic  (12c). However, given that only standard components are used, the same code should work equally well on other containers and from other IDEs.

Note: for the use case presented in this article, a somewhat simpler solution would be possible using Server Sent Events (SSE) – a simpler and lighter weight approach than the use of web sockets. SSE is uni-directional (server to client push only) and that of course is exactly what I am doing in this particular example.

The steps will be:

  • Implement the REST service to handle json payloads in HTTP Post requests
  • Implement the WebSocket endpoint
  • Interact from REST service endpoint with WebSocket (endpoint)
  • Implement the HTML and JavaScript web application to present the live status for the movie theater based on the web socket messages

The starting point is a basic Java EE web application – with no special setup in web.xml or other files.

The final application looks like this:

image

For JDeveloper 12c users: the required libraries are JAX-RS Jersey Jettison (Bundled), JAX-RS Jersey 2.x, WebSocket, Servlet Runtime.

Implement the REST service to handle json payloads in HTTP Post requests

Publishing a REST service from Java (EE) is done using JAX-RS. In an earlier post, I described how to expose a REST service from Java SE (out of Java EE containers, leveraging the HTTP Server in the JVM). Publishing from within a Java EE container is very similar – and even easier. All we need is a single class with the right annotations.

image

The class is shown below. It is called MovieEvent (although the Class name does not matter at all). The class is annotated with the @Path annotation that is part of the JAX-RS specification. Because of this annotation, the class is published as a REST resource. The string parameter in this annotation (“cinemaevent”) defines the resource name as used in the URL for this REST service. The entire URL where this service can be invoked will be http://host:port/<web application root>/resources/cinemaevent. Note the segment resources that comes between the web application root and the name of the resource. That one had me confused for some time. The web application root for this application is set to CinemaMonitor by the way.

package nl.amis.cinema.view;

import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Request;

//invoke at : http://localhost:7101/CinemaMonitor/resources/cinemaevent
@Path("cinemaevent")
public class MovieEvent {

public MovieEvent() {
}

@POST
@Consumes("application/json")
@Produces("text/plain")
public String postMovieEvent(@Context Request request, String json) {
System.out.println("received event:" + json);
return "event received " + json;
}

@GET
@Produces("text/plain")
public String getMovieEvent(@Context Request request) {
return "nothing to report from getMovieEvent";
}
}

Method postMovieEvent is annotated with @POST – making it the handler of POST requests sent to the REST resource (at the URL discussed above). However, the annotation @Consumes(“application/json”) ensures that only HTTP requests with their content-type appropriately set to application/json will indeed be delivered to this method. The JSON payload of these requests is passed in the json input parameter. Note that not its name is relevant but the fact that it is the first String input parameter to this method without special annotations such as @Context.

At the present, the method does nothing useful – it write the JSON payload to the system output and returns a fairly bland confirmation message. Before too long, we will extend both this method and the entire class to interact with the web socket.

The REST service can be tested, for example from SoapUI or a Java client program by sending requests such as this one:

image

The corresponding output in the console (from the running Java EE container):

image

Implement the WebSocket endpoint

Implementing a WebSocket endpoint in Java EE is defined through the JSR-356 specification. A very good overview of how to use web sockets in Java applications is provided in this article.

Turning a Java Class into a WebSocket endpoint is actually quite simple. Use a few annotations, and we are in business. It is important to realize that even though the class that acts as the Web Socket server (endpoint) is deployed in this case as part of a Java EE web application, it stands quite apart from the rest of that application. The web socket endpoint can be accessed, not just from browsers but from Java clients as well. But there is no instance of the Class that is accessible for direct Java calls, nor does the WebSocket endpoint hook into EJBs, JMS destinations or JSF managed beans. It is a rather isolated component within the Java EE application. It does however have the potential to consume CDI Events (as described by Bruno Borges in this excellent article that inspired me to write this piece).

Create a Class called CinemaEventSocketMediator. Add the following annotation: @ServerEndpoint(“/cinemaSocket/{client-id}”). This turns the class into a web socket endpoint that exposes a Web Socket [channel]at ws://host:port/<web application context root>/cinemaSocket (in this case that will be ws://localhost:7101/CinemaMonitor/cinemaSocket). The final segment of the URL (/{client-id}) introduces a path parameter. The address of the Web Socket ends with the segment cinemaSocket. Anything that is added behind it is interpreted as an additional parameter that can be leveraged in the onOpen, onClose and onMessage methods through the @PathParam annotation – as we will see next.

A collection of peers is defined in which each client that starts a web socket connection will be retained.

The onOpen method – or rather the method annotated with @OnOpen – is invoked when a new client starts communications over the web socket channel. This method saves the session to the peers collection and returns a welcoming message to the new contact. Note how through the @PathParam annotated input parameter the method knows a little bit more about the client, provided the client did indeed add some content after the ‘regular’ Web Socket URL.

The method decorataed with @OnMessage is triggered when a message arrives on the Web Socket [channel]. In this case, the message is received and instantiated as a JSONObject. This would allow us to perform JSON style operations on the message (extract nested data elements, manipulate and add data). However, at the present, all we do is pass the message to each of the peers, regardless where the message came from (client-id) or what contents it contains. Note that the method would have to handle an exception if the message were not correct JSON data.

Finally the method with @OnClose handles clients closing their web socket channel connection. These clients are removed from the peers collection.

This particular WebSocket endpoint does not do anything that is special for the use case at hand. There are no references no movies, cinema events or whatever in this class. There could be logic that interprets messages, routes based on their content for example, but there does not need to be such business specific logic.

 

package nl.amis.cinema.view.push;

import java.io.IOException;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;

import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;

@ServerEndpoint("/cinemaSocket/{client-id}")
public class CinemaEventSocketMediator {

private static Set peers = Collections.synchronizedSet(new HashSet());

@OnMessage
public String onMessage(String message, Session session, @PathParam("client-id") String clientId) {
try {
JSONObject jObj = new JSONObject(message);
System.out.println("received message from client " + clientId);
for (Session s : peers) {
try {
s.getBasicRemote().sendText(message);
System.out.println("send message to peer ");
} catch (IOException e) {
e.printStackTrace();
}

}
} catch (JSONException e) {
e.printStackTrace();
}
return "message was received by socket mediator and processed: " + message;
}

@OnOpen
public void onOpen(Session session, @PathParam("client-id") String clientId) {
System.out.println("mediator: opened websocket channel for client " + clientId);
peers.add(session);

try {
session.getBasicRemote().sendText("good to be in touch");
} catch (IOException e) {
}
}

@OnClose
public void onClose(Session session, @PathParam("client-id") String clientId) {
System.out.println("mediator: closed websocket channel for client " + clientId);
peers.remove(session);
}
}

Interact from REST service endpoint with WebSocket (endpoint)

The JSON messages received by the REST service exposed by class MovieEvent should be pushed through the Web Socket to the (external) clients, i.e. the web browser. There are two main options to hand these messages from class MovieEvent to the CinemaEventSocketMediator. One is through the use of CDI Events (as was mentioned above) and the other is by making class MovieEvent another client of the Web Socket [channel]exposed by CinemaEventSocketMediator. In this case, we opt for the latter strategy. Note that this means that there is no need for the MovieEvent class and the CinemaEventSocketMediator class to be in the same web application; their only interaction takes place across the web socket and they have no dependencies. I have them included in the same application for easy deployment. The same applies by the way to the client side of this article: the HTML and JavaScript that are loaded by the browser to present the dashboard to the end user. This too is currently included in the same web application and it too only has interaction over the web socket. There is no real reason for it to be part of the same application.

Using an excellent description on StackOverflow, I have created class MovieEventSocketClient with the @ClientEndpoint annotation. This class acts as a client to the Web Socket. It is the counterpart of the CinemaEventSocketMediator that is more or less the host or server for the web socket. The constructor for this class has two important steps: through the ContainerProvider (Provider class that allows the developer to get a reference to the implementation of the WebSocketContainer) a reference to the WebSocketContainer is retrieved (this is an implementation provided object that provides applications a view on the container running it. The WebSocketContainer container various configuration parameters that control default session and buffer properties of the endpoints it contains. It also allows the developer to deploy websocket client endpoints by initiating a web socket handshake from the provided endpoint to a supplied URI where the peer endpoint is presumed to reside. ) Using this container reference, through the connectToServer method, the client endpoint MovieEventSocketClient is connectedto its server. (This method blocks until the connection is established, or throws an error if either the connection could not be made or there was a problem with the supplied endpoint class.)

Class MovieEventSocketClient has methods annotated with @OnOpen, @OnClose and @OnMessage with more or less the same role as the counterparts in CinemaEventSocketMediator (and in the JavaScript client as we will see later). Note how in the @OnOpen annotated method the input parameter of type Session is retained and how in the method sendMessage this user session is used to send a message across the web socket.

package nl.amis.cinema.view.push;

import java.net.URI;

import javax.websocket.ClientEndpoint;
import javax.websocket.CloseReason;
import javax.websocket.ContainerProvider;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.WebSocketContainer;

// based on http://stackoverflow.com/questions/26452903/javax-websocket-client-simple-example

@ClientEndpoint
public class MovieEventSocketClient {
public MovieEventSocketClient(URI endpointURI) {
try {
WebSocketContainer container = ContainerProvider.getWebSocketContainer();
container.connectToServer(this, endpointURI);
} catch (Exception e) {
throw new RuntimeException(e);
}
}

Session userSession = null;

@OnOpen
public void onOpen(Session userSession) {
System.out.println("client: opening websocket ");
this.userSession = userSession;
}

/**
* Callback hook for Connection close events.
*
* @param userSession the userSession which is getting closed.
* @param reason the reason for connection close
*/
@OnClose
public void onClose(Session userSession, CloseReason reason) {
System.out.println("client: closing websocket");
this.userSession = null;
}

/**
* Callback hook for Message Events. This method will be invoked when a client send a message.
*
* @param message The text message
*/
@OnMessage
public void onMessage(String message) {
System.out.println("client: received message "+message);
}

public void sendMessage(String message) {
this.userSession.getAsyncRemote().sendText(message);
}

}

Next we extend Class MovieEvent – the REST service that receives the JSON messages as HTTP POST requests – to interact with MovieEventSocketClient to pass the JSON messages to the Web Socket.

Method initializeWebSocket is added to instantiate MovieEventSocketClient with the address of the web socket. In postMovieEvent – the method annotated with @POST that handles the HTTP POST requests – a call is added to sendMessageOverSocket that hands the JSON message to MovieEventSocketClient  (after initializing it) to send it across the web socket (where it will be received in class CinemaEventSocketMediator).

 

package nl.amis.cinema.view;

import java.net.URI;
import java.net.URISyntaxException;

import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Request;

import nl.amis.cinema.view.push.MovieEventSocketClient;

//invoke at : http://localhost:7101/CinemaMonitor/resources/cinemaevent

@Path("cinemaevent")
public class MovieEvent {

private MovieEventSocketClient client;

private final String webSocketAddress = "ws://localhost:7101/CinemaMonitor/cinemaSocket";

public MovieEvent() {
}

private void initializeWebSocket() throws URISyntaxException {
//ws://localhost:7101/CinemaMonitor/cinemaSocket/
System.out.println("REST service: open websocket client at " + webSocketAddress);
client = new MovieEventSocketClient(new URI(webSocketAddress + "/0"));
}

private void sendMessageOverSocket(String message) {
if (client == null) {
try {
initializeWebSocket();
} catch (URISyntaxException e) {
e.printStackTrace();
}
}
client.sendMessage(message);

}

@POST
@Consumes("application/json")
@Produces("text/plain")
public String postMovieEvent(@Context Request request, String json) {
System.out.println("received event:" + json);
sendMessageOverSocket(json);
return "event received " + json;
}

@GET
@Produces("text/plain")
public String getMovieEvent(@Context Request request) {
return "nothing to report from getMovieEvent";
}
}

At this point, the application can be deployed. JSON messages sent to the REST service exposed by MovieEventSocketClient should be sent onward to the Web Socket (end point), leading to a message being written to the system output from onMessage in class CinemaEventSocketMediator.

Implement the HTML and JavaScript web application for the Cinema Monitor

The final piece in the puzzle discussed in this article is the client application to present the live status for the movie theater based on the web socket messages. It runs in a relatively modern browser – all standard browsers have HTML5 support which includes Web Socket interactions – and consists of an HTML page and two JavaScript libraries.

image

 

The HTML itself is relatively straightforward and boring.

image

Important are the <script> statements that import the JavaScript libraries that interact with the web socket and handle messages received over the web socket. The four rooms in the movie theater that are being monitored are represented by four TD elements with their id values set to room1..room4. These id values will be used in the JavaScript to locate the HTML element to update when a JSON message is received on the web socket for a particular room.

The imported JavaScript library websocket.js initializes the web socket connection to the end point ws://localhost:7101/CinemaMonitor/cinemaSocket. It configures JavaScript handlers for onOpen, onClose and onMessage. The latter is the most important one: any messages received on the web socket are checked for the string room. If the string is found, the message is handed off to the function updateRoomDetails(). This function is defined in the second JavaScript library moviemonitor.js.

var wsUri = "ws://" + document.location.host + "/CinemaMonitor/cinemaSocket/5";
var websocket = new WebSocket(wsUri);

websocket.onmessage = function(evt) { onMessage(evt) };
websocket.onerror = function(evt) { onError(evt) };
websocket.onopen = function(evt) { onOpen(evt) };

function onMessage(evt) {
console.log("received over websockets: " + evt.data);
console.log("looked for room index of: "+ evt.data.indexOf("room"));
var index = evt.data.indexOf("room");
writeToScreen(evt.data);
if (index&gt;1) {
console.log("found room index of: "+ evt.data.indexOf("room"));
updateRoomDetails( evt.data);
}
}

function onError(evt) {
writeToScreen('<span style="color: red;">ERROR:</span> ' + evt.data);
}

function onOpen() {
writeToScreen("Connected to " + wsUri);
}

// For testing purposes
var output = document.getElementById("output");

function writeToScreen(message) {
if (output==null)
{output = document.getElementById("output");}
//output.innerHTML += message + "
";
output.innerHTML = message + "
";
}

function sendText(json) {
console.log("sending text: " + json);
websocket.send(json);
}

The function updateRoomDetails in moviemonitor.js does not do a whole lot. It parses the input parameter from plain text with JSON format to a JavaScript memory structure. The function handleRoomUpdate is invoked with that JavaScript data structure – an object with properties room and occupation. The function handleRoomUpdate locates the TD element with its id set to room# where # corresponds wit the room property in the roomDetails input argument. It then sets the innerHTML of this element to the value of the occupation property. The result is an instant update of the room occupation value displayed in the user interface.

function updateRoomDetails( json) {
var roomDetails = JSON.parse(json);
handleRoomUpdate(roomDetails);
}

function handleRoomUpdate( roomDetails) {
var roomId = roomDetails.room;
var occupation = roomDetails.occupation;

var roomCell = document.getElementById("room"+roomId);
roomCell.innerHTML = occupation;

document.getElementById("message").innerHTML = roomDetails;
}

 

The screenshot shows the situation after a number of JSON messages have been received over the web sockets and the user interface has been updated accordingly.

 

image

 

Resources

Download the source code for the example discussed in this article: Zip File.

Share this on .. Tweet about this on TwitterShare on LinkedIn1Share on Facebook10Share on Google+0Email this to someoneShare on Tumblr0Buffer this page

About Author

Lucas Jellema, active in IT (and with Oracle) since 1994. Oracle ACE Director and Oracle Developer Champion. Solution architect and developer on diverse areas including SQL, JavaScript, Docker, Machine Learning, Java, SOA and microservices, events in various shapes and forms and many other things. Author of the Oracle Press books: Oracle SOA Suite 11g Handbook and Oracle SOA Suite 12c Handbook. Frequent presenter on community events and conferences such as JavaOne, Oracle Code and Oracle OpenWorld.

5 Comments

  1. Shahe Masoyan on

    Hello Lucas, good article! I did exactly as you did in your post but I am not able to make it work. It keeps giving me javascript error during WebSocket handshake: unexpected response code 404

  2. hello, i’m not sure how the messages are sent and where are the messages sent from ? Because I didn’t see the function sendText is called anywhere. Thank you !

  3. 2016-04-24T01:02:43.321+0530|Warning: StandardWrapperValve[jersey-servlet]: Servlet.service() for servlet jersey-servlet threw exception
    java.lang.NullPointerException
    at com.wipro.cto.ngRecruit.Test.SessionRefreshSocketClient.sendRefreshMessage(SessionRefreshSocketClient.java:67)
    at com.wipro.cto.recoengine.ngrecruit.rest.service.InvokeProctorService.Hello(InvokeProctorService.java:1213)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:497)
    at org.glassfish.jersey.server.model.internal.ResourceMethodInvocationHandlerFactory$1.invoke(ResourceMethodInvocationHandlerFactory.java:81)
    at org.glassfish.jersey.server.model.internal.AbstractJavaResourceMethodDispatcher.invoke(AbstractJavaResourceMethodDispatcher.java:125)
    at org.glassfish.jersey.server.model.internal.JavaResourceMethodDispatcherProvider$ResponseOutInvoker.doDispatch(JavaResourceMethodDispatcherProvider.java:152)
    at org.glassfish.jersey.server.model.internal.AbstractJavaResourceMethodDispatcher.dispatch(AbstractJavaResourceMethodDispatcher.java:91)
    at org.glassfish.jersey.server.model.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:346)
    at org.glassfish.jersey.server.model.ResourceMethodInvoker.apply(ResourceMethodInvoker.java:341)
    at org.glassfish.jersey.server.model.ResourceMethodInvoker.apply(ResourceMethodInvoker.java:101)
    at org.glassfish.jersey.server.ServerRuntime$1.run(ServerRuntime.java:224)
    at org.glassfish.jersey.internal.Errors$1.call(Errors.java:271)
    at org.glassfish.jersey.internal.Errors$1.call(Errors.java:267)
    at org.glassfish.jersey.internal.Errors.process(Errors.java:315)
    at org.glassfish.jersey.internal.Errors.process(Errors.java:297)
    at org.glassfish.jersey.internal.Errors.process(Errors.java:267)
    at org.glassfish.jersey.process.internal.RequestScope.runInScope(RequestScope.java:317)
    at org.glassfish.jersey.server.ServerRuntime.process(ServerRuntime.java:198)
    at org.glassfish.jersey.server.ApplicationHandler.handle(ApplicationHandler.java:946)
    at org.glassfish.jersey.servlet.WebComponent.service(WebComponent.java:323)
    at org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:372)
    at org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:335)
    at org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:218)
    at org.apache.catalina.core.StandardWrapper.service(StandardWrapper.java:1682)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:344)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:214)
    at org.glassfish.tyrus.servlet.TyrusServletFilter.doFilter(TyrusServletFilter.java:253)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:256)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:214)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:316)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:160)
    at org.apache.catalina.core.StandardPipeline.doInvoke(StandardPipeline.java:734)
    at org.apache.catalina.core.StandardPipeline.invoke(StandardPipeline.java:673)
    at com.sun.enterprise.web.WebPipeline.invoke(WebPipeline.java:99)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:174)
    at org.apache.catalina.connector.CoyoteAdapter.doService(CoyoteAdapter.java:357)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:260)
    at com.sun.enterprise.v3.services.impl.ContainerMapper.service(ContainerMapper.java:188)
    at org.glassfish.grizzly.http.server.HttpHandler.runService(HttpHandler.java:191)
    at org.glassfish.grizzly.http.server.HttpHandler.doHandle(HttpHandler.java:168)
    at org.glassfish.grizzly.http.server.HttpServerFilter.handleRead(HttpServerFilter.java:189)
    at org.glassfish.grizzly.filterchain.ExecutorResolver$9.execute(ExecutorResolver.java:119)
    at org.glassfish.grizzly.filterchain.DefaultFilterChain.executeFilter(DefaultFilterChain.java:288)
    at org.glassfish.grizzly.filterchain.DefaultFilterChain.executeChainPart(DefaultFilterChain.java:206)
    at org.glassfish.grizzly.filterchain.DefaultFilterChain.execute(DefaultFilterChain.java:136)
    at org.glassfish.grizzly.filterchain.DefaultFilterChain.process(DefaultFilterChain.java:114)
    at org.glassfish.grizzly.ProcessorExecutor.execute(ProcessorExecutor.java:77)
    at org.glassfish.grizzly.nio.transport.TCPNIOTransport.fireIOEvent(TCPNIOTransport.java:838)
    at org.glassfish.grizzly.strategies.AbstractIOStrategy.fireIOEvent(AbstractIOStrategy.java:113)
    at org.glassfish.grizzly.strategies.WorkerThreadIOStrategy.run0(WorkerThreadIOStrategy.java:115)
    at org.glassfish.grizzly.strategies.WorkerThreadIOStrategy.access$100(WorkerThreadIOStrategy.java:55)
    at org.glassfish.grizzly.strategies.WorkerThreadIOStrategy$WorkerThreadRunnable.run(WorkerThreadIOStrategy.java:135)
    at org.glassfish.grizzly.threadpool.AbstractThreadPool$Worker.doWork(AbstractThreadPool.java:564)
    at org.glassfish.grizzly.threadpool.AbstractThreadPool$Worker.run(AbstractThreadPool.java:544)
    at java.lang.Thread.run(Thread.java:745)

Leave a Reply