Introduction
The latest release of JDeveloper 12c (12.1.3.0) along with WebLogic Server 12.1.3 came up with some new Java EE 7 features. One of them is support of JSR 356 Java API for WebSockets. Actually the WebSocket Protocol (RFC 6455) has been supported starting from 12.1.2.0 release, but it was based on WebLogic specific implementation of the WebSocket API. Now this proprietary WebLogic Server WebSocket API has been deprecated. However, it is still supported for backward compatibility.In this post I am going to show an example of using JSR 356 Java API for WebSockets in a simple ADF application. The use case is about some sailing regatta which takes place in the Tasman Sea. There are three boats participating in the regatta and they are going to cross the Tasman Sea sailing from Australia to New Zealand coast. The goal of the sample application is to monitor the regatta and inform users about how it is going on, showing the positions of the boats on a map.
We're going to declare a WebSocket server endpoint in the application and when a user opens a page a Java script function opens a new WebSocket connection. The application uses a scheduled service which every second updates boats coordinates and sends a message containing new boats positions to all active WebSocket clients. On the client side a Java script function receives the message and adds markers to the Google map according to the GPS coordinates. So, each user, interested in the regatta, is going to see the same updated picture representing the current status of the competition.
WebSocket server endpoint
Let's start with declaring a WebSocket server endpoint. There is a small issue in the current implementation, which probably will be resolved in future releases. The WebSocket endpoints can not be mixed with ADF pages and they should be deployed in a separate WAR file. The easiest way to do that is to create a separate WebSocket project within the application and to declare all necessary endpoints in this project:This is also important to set up a readable Java EE Web Context Root for the project:
The next step is to create a Java class which is going to be a WebSocket end point. So, this is a usual class with a special annotation at the very beginning:
@ServerEndpoint(value = "/message") public class MessageEndPoint { public MessageEndPoint() { super(); } }
Note, that JDeveloper underlines the annotation with red. We are going to fix the issue by letting JDeveloper configure the project for Web Socket.
Having done that, JDeveloper is going to convert the project into a Web project adding the Web.xml file and add necessary library:
Furthermore, the endpoint class becomes runnable and we can just run it so as to check how it actually works:
In response JDeveloper generates the following URL at which the WebSocket endpoint is available. Note, that the URL contains the project context root (WebSocket) and the value property of the annotation (/message). If everything is ok then when we click the URL, we'll get the "Connected successfuly" information window:
By the way, there is a typo in the message.
And now let's add some implementation to the WebSocket endpoint class. According to the specification a new instance of the MessageEndPoint class is going to be created for each WebSocket connection. In order to hold a bunch of all active WebSocket sessions we're going to use a static queue:
public class MessageEndPoint { //A new instance of the MessageEndPoint class //is going to be created for each WebSocket connection //This queue contains all active WebSocket sessions final static Queue<Session> queue = new ConcurrentLinkedQueue<>(); @OnOpen public void open(Session session) { queue.add(session); } @OnClose public void closedConnection(Session session) { queue.remove(session); } @OnError public void error(Session session, Throwable t) { queue.remove(session); t.printStackTrace(); }The annotated methods open, closedConnection and error are going to be invoked respectively when a new connection has been established, when it has been closed and when something wrong has happened. As we have done that, we can use some static method to broadcast a text message to all clients:
public static void broadCastTex(String message) { for (Session session : queue) { try { session.getBasicRemote().sendText(message); } catch (IOException e) { e.printStackTrace(); } } }In our use case we have to notify users with new GPS coordinates of the boats, so we should be able to send via WebSockets something more complex than just text messages.
Sending an object
Basically, a business model of the sample application is represented by two plain Java classes Boat:public class Boat { private final String country; private final double startLongitude; private final double startLatitude; private double longitude; private double latitude; public String getCountry() { return country; } public double getLongitude() { return longitude; } public double getLatitude() { return latitude; } public Boat(String country, double longitude, double latitude) { this.country = country; this.startLongitude = longitude; this.startLatitude = latitude; } ...and Regatta:
public class Regatta { private final Boat[] participants = new Boat[] { new Boat("us", 151.644, -33.86), new Boat("ca", 151.344, -34.36), new Boat("nz", 151.044, -34.86) }; public Boat[] getParticipants() { return participants; } ...For our use case we're going to send an instance of the Regatta class to the WebSocket clients. The Regatta contains all regatta participants represented by the Boat class instances containing updated GPS coordinates (longitude and latitude).
This can be done by creating a custom implementation of the Encoder.Text<Regatta> interface, or in other words we're going to create an encoder which can transform a Regatta instance into a text and specify this encoder to be used by the WebSocket endpoint while sending an instance of the Regatta.
public class RegattaTextEncoder implements Encoder.Text<Regatta> { @Override public void init(EndpointConfig ec) { } @Override public void destroy() { } private JsonObject encodeBoat(Boat boat) throws EncodeException { JsonObject jsonBoat = Json.createObjectBuilder() .add("country", boat.getCountry()) .add("longitude", boat.getLongitude()) .add("latitude" , boat.getLatitude()).build(); return jsonBoat; } @Override public String encode(Regatta regatta) throws EncodeException { JsonArrayBuilder arrayBuilder = Json.createArrayBuilder(); for (Boat boat : regatta.getParticipants()) { arrayBuilder.add(encodeBoat(boat)); } return arrayBuilder.build().toString(); } }
@ServerEndpoint( value = "/message", encoders = {RegattaTextEncoder.class })Having done that, we can send objects to our clients:
public static void sendRegatta(Regatta regatta) { for (Session session : queue) { try { session.getBasicRemote().sendObject(regatta); } catch (EncodeException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }The RegattaTextEncoder represents a Regatta object as a list of boats using Json notation, so it is going to be something like this:
[{"country":"us","longitude":151.67,"latitude":-33.84},{"country":"ca", ...},{"country":"nz", ...}]
Receiving a message
On the client side we use a Java script function to open a new WebSocket connection:
//Open a new WebSocket connection //Invoked on page load function connectSocket() { websocket = new WebSocket(getWSUri()); websocket.onmessage = onMessage; }And when a message arrives, we're going to loop over array of boats and for each boat add a marker on the map:
function onMessage(evt) { var boats = JSON.parse(evt.data); for (i=0; i<boats.length; i++) { markBoat(boats[i]); } } function markBoat(boat) { var image = '../resources/images/'+boat.country+'.png'; var latLng = new google.maps.LatLng(boat.latitude,boat.longitude); mark = new google.maps.Marker({ position: latLng, map: map, title: boat.country, icon: image }); }You can learn down here how to integrate Google maps into your applications.
Run the regatta
In order to emulate a live show we use ScheduledExecutorService. Every second we are going to update GPS coordinates and broadcast the update to all subscribers:private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); private ScheduledFuture<?> runHandle; //Schedule a new regatta on Start button click public void startRegatta(ActionEvent actionEvent) { //Cancel the previous regatta if (runHandle != null) { runHandle.cancel(false); } runHandle = scheduler.scheduleAtFixedRate(new RegattaRun(), 1, 1, TimeUnit.SECONDS); } public class RegattaRun implements Runnable { private final static double FINISH_LONGITUDE = 18; private final Regatta regatta = new Regatta(); //Every second update GPS coordinates and broadcast //new positions of the boats public void run() { regatta.move(); MessageEndPoint.sendRegatta(regatta); if (regatta.getLongitude() >= FINISH_LONGITUDE) { runHandle.cancel(true); } } }
Bet on your boat
And finally, the result of our work looks like this:The sample application for this post requires JDeveloper 12.1.3. Have fun!
That's it!
No comments:
Post a Comment
Post Comment