Liferay 7 supports Websockets; see how you can implement your own Push support.
Introduction
I was recently polling my coworkers looking for new blog ideas, and Filipe Melo asked about Websockets. I haven't seen any other documentation or blogs about building Websocket solutions for Liferay, so I figured this would be an interesting challenge.
Websockets was introduced to provide a full duplex and realtime client/server communication path between remote clients and your server-side framework. JSR 356 was introduced to add Websocket support to Java applications, and Liferay added support for Websockets in 7.0.
Most of the time when you happen upon an article about Websockets, they present an implementation of a chat room, and we'll be implementing a similar thing here. All users will be in the chat room, anyone can send a message, and the messages will be broadcast to everyone. Unlike other examples though, our server is going to run in Liferay and will be cluster-aware and our client will be a simple portlet that can be added to a portal page.
Yeah I know, it's a lame example, but it is just an example. The model that I'm presenting here can be used for anything you have a need for.
The Websocket Whiteboard
Liferay 7 introduced an HTTP Whiteboard, and it allows OSGi modules to create servlets that live under the /o/my-servlet sort of path.
Liferay also introduced a Websocket Whiteboard to provide similar capabilities for OSGi modules to define new Websocket endpoints that will be available under the /o/websocket path.
To create our server-side endpoint, we need to extend the javax.websocket.Endpoint class, and since we're using OSGi we'll also need to add the @Component annotation:
@Component(
immediate = true,
property = {
"org.osgi.http.websocket.endpoint.path=/o/websocket/chat/{username}",
"org.osgi.http.websocket.endpoint.decoders="
+ "com.dnebinger.websockets.chat.ChatMessageDecoder",
"org.osgi.http.websocket.endpoint.encoders="
+ "com.dnebinger.websockets.chat.ChatMessageEncoder"
},
service = Endpoint.class
)
public class ChatEndpoint extends Endpoint {
/**
* onOpen: We must implement this method to be a real endpoint.
*
* @param session the session that has just been activated.
* @param config the configuration used to configure this endpoint.
*/
@Override
public void onOpen(final Session session, EndpointConfig config) {
}
}
This is our definition for the ChatEndpoint class. It extends Endpoint, and it is also registered as an @Component.
The properties set the main configuration for the endpoint.
| Property | Description |
|---|---|
org.osgi.http.websocket.endpoint.path |
Defines the path for the endpoint to match on. Paths must be unique in the Websocket Whiteboard. In this case we are registering under /o/websocket/chat, and no other Websocket can handle this same path. It also supports named path parameters. I recommend paths using the /o/websocket prefix to avoid conflict with other portal or portlet URLs. |
org.osgi.http.websocket.endpoint.decoders |
A comma-separated list of class names that implement the Decoder interface to transform incoming raw message data into Java objects. |
org.osgi.http.websocket.endpoint.encoders |
Comma-separated list of class names that implement the Encoder interface to transform Java objects into raw types that can be sent as a Websocket response. |
org.osgi.http.websocket.endpoint.subprotocol |
An optional comma-separated list of supported subprotocols. |
Receiving Messages
in the onOpen() method of our Endpoint implementation, we can register a new message handler:
public void onOpen(final Session session, EndpointConfig config) {
final String username = session.getPathParameters().get("username");
// add a message handler for new chat messages
session.addMessageHandler(new MessageHandler.Whole<ChatMessage>() {
/**
* onMessage: Called when the message has been fully received.
*
* @param message the message data.
*/
@Override
public void onMessage(ChatMessage message) {
message.setFrom(username);
// send to all active sessions on the current node
broadcast(message);
// broadcast to the cluster so they also will broadcast to
// their websocket sessions
chatMessageClusterSender.sendChatMessage(
message.getFrom(), message.getMessage());
}
});
session.addMessageHandler(new MessageHandler.Whole<String>() {
@Override
public void onMessage(String text) {
JSONObject json = JSONFactoryUtil.createJSONObject(text);
ChatMessage message = new ChatMessage(username, json.getString("message"));
broadcast(message);
chatMessageClusterSender.sendChatMessage(username, message.getMessage());
}
});
}
We can access the defined path parameters from the Session's getPathParameters() method and extract the one we want.
We then add a new MessageHandler implementation for a whole ChatMessage object (this will be created from the Decoders property). You can add multiple new MessageHandler implementations for different types of objects or even for the raw types as we did here by adding a MessageHandler using a raw String type (with the bug I spoke of above, the only MessageHandler I could register was this String method).
In the implementation above, we're using a static public method broadcast() (not shown) to send the message to all of the active websocket sessions on this node.
And, to be cluster-friendly, we're also going to use the cluster-wide messaging technique to get the chat message to all of the other nodes, and they too will invoke the broadcast() method to broadcast the message to the active websocket sessions they have registered.
We need to do this because, in a cluster, a client will establish a websocket session with only one node in the cluster. This means that each node in your cluster can have one or more active sessions on their own that the other nodes will not know about.
Leveraging Liferay's built-in cluster-wide messaging we can send the chat message to all of the other nodes in the cluster, and they can be responsible for sending the chat message to the active sessions they each are tracking.
Test Portlet
I created a super-simple portlet to just demonstrate how to create a websocket client that interacted with our new Chat server-side component. Here's the view.jsp:
<%@ include file="/init.jsp" %>
<form>
<input id="chatMessageText" type="text">
<input onclick="wsSendMessage();" value="Chat" type="button">
<input onclick="wsCloseConnection();" value="Disconnect" type="button">
</form>
<br>
<textarea id="echoText" rows="5" cols="30"></textarea>
<script type="text/javascript">
var chatUrl = "ws://localhost:8080/o/websocket/chat/<%= user.getScreenName() %>";
var webSocket = new WebSocket(chatUrl);
var echoText = document.getElementById("echoText");
echoText.value = "";
var message = document.getElementById("chatMessageText");
webSocket.onopen = function(message){ wsOpen(message);};
webSocket.onmessage = function(message){ wsGetMessage(message);};
webSocket.onclose = function(message){ wsClose(message);};
webSocket.onerror = function(message){ wsError(message);};
function wsOpen(message){
echoText.value += "Connected ... \n";
}
function wsSendMessage(){
var chatMsg = { from: "<%= user.getScreenName() %>", message: message.value };
webSocket.send(JSON.stringify(chatMsg));
echoText.value += "Message sent to the server : " + message.value + "\n";
message.value = "";
}
function wsCloseConnection(){
webSocket.close();
}
function wsGetMessage(message){
echoText.value += "Message received from to the server : " + message.data + "\n";
}
function wsClose(message){
echoText.value += "Disconnect ... \n";
}
function wsError(message){
echoText.value += "Error ... \n";
}
</script>
First I have a simple form for a message, a Chat button to send the message, a Disconnect button to drop the connection, and finally a text area to show the received messages.
There's a bunch of javascript to establish the Websocket connection and then a bunch of method wiring for sending the message and what not.
In the wsSendMessage() method, I'm building the JS object that the server side expects and sending it on the socket. In the wsGetMessage() method I'm just dumping the data, but I know I'm getting back the same JSON object that I'm sending so I could use the fields in a smarter way.
In my local environ I created a few different accounts and used different browsers to log in and share messages, here's an example from the chat:
Connected ...
Message received from to the server : {"from":"test","message":"Connected!"}
Message received from to the server : {"from":"37410","message":"Connected!"}
Message received from to the server : {"from":"alpha","message":"Connected!"}
Message received from to the server : {"from":"alpha","message":"hey who is on?"}
Message sent to the server : I'm on!
Message received from to the server : {"from":"test","message":"I'm on!"}
Message received from to the server : {"from":"alpha","message":"Have you seen beta?"}
Message sent to the server : No but I hear he might log in later...
Message received from to the server : {"from":"test","message":"No but I hear he might log in later..."}
Message received from to the server : {"from":"37410","message":"Connected!"}
Message received from to the server : {"from":"beta","message":"Connected!"}
Message received from to the server : {"from":"alpha","message":"Looks like beta has joined"}
Message received from to the server : {"from":"beta","message":"yeah I'm here too"}
Conclusion
So pretty simple, right? I was actually kind of surprised at how easy it was to get rolling...
Using this as a model, you could build duplex systems like this where the browsers are sending data and the server side is pushing data or you could focus on one-way data sending from the browser to the server or a push from the server to the browsers.
And with the cluster support that Liferay has, you can handle your Websockets across all of the nodes in the cluster; the browsers won't know or care what node they're connected to because all nodes will be able to send the same data.
The repo for these modules is available here: https://github.com/dnebing/websockets
Hope you find this useful! Let me know what kind of Websocket solutions you come up with...

