CometD :Google, Facebook Similar Chat in your Java Web Application
I was able to implement chatting application by using CometD and
more customizable chat windows opening on the browser which is exactly
similar to Google. This works almost all the modern browsers. This
article explains step by step, How to implement chatting application
from the scratch and also How to integrate chatting application to your
existing Java based web application.
You need to download the cometD from
their official web site. It has all the dependencies required to
implement the chatting application except 2 java script libraries. I
have written two Javascript libraries, one to create dynamic chat
windows like Google and other to handle CometD chatting functionality in
generic way. CometD documentation provides good details. But, I go
ahead with the tutorial by using those 2 libraries. I hope to share the
sample application with you and you can deploy it in your localhost and
test, how it works.
Step 1: Need to add require Plugins i.e Require Jar files.
In general using maven we can add maven dependency in pom.xml file.
The needed maven plugins are given below. Just copy it and add it to
your pom.xml file.
Other wise we can add the following jar files manually in /WEB-INF/lib Folder. The jar files are,
- bayeux-api-2.5.0.jar
- cometd-java-annotations-2.5.0.jar
- cometd-java-common-2.5.0.jar
- cometd-java-server-2.5.0.jar
- cometd-websocket-jetty-2.5.0.jar
- javax.inject-1.jar
- jetty-continuation-7.6.7.v20120910.jar
- jetty-http-7.6.7.v20120910.jar
- jetty-io-7.6.7.v20120910.jar
- jetty-jmx-7.6.7.v20120910.jar
- jetty-util-7.6.7.v20120910.jar
- jetty-websocket-7.6.7.v20120910.jar
- jsr250-api-1.0.jar
- slf4j-api-1.6.6.jar
- slf4j-simple-1.6.6.jar
Step 2: Adding Require Java script files
You need to link the following Javascript files.
- cometd.js
- AckExtension.js
- ReloadExtension.js
- jquery-1.8.2.js
- jquery.cookie.js
- jquery.cometd.js
- jquery.cometd-reload.js
- chat.window.js
- comet.chat.js
Here i have created two java script files are there. That two files are comet.chat.js and chat.window.js .
==> The comet.chat.js file used to communicate with chat service class. The chat service class use the bayeux server for communicate client.
==> The chat.window.js files used to create or populate the chat window in java application page.
Step 3: Change web.xml file should allow the client server communicate in some url. i.e the following code .
And also the following configuration for servlet mapping xml files also need to allow the cometd chat path
Step 4: Creating Chat service file for communicating with client or script request processing
/**
* @author Ramachandran
* CometD chat service.
*/
package com.shinnedhawks.cometd;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import javax.inject.Inject;
import org.cometd.annotation.Configure;
import org.cometd.annotation.Listener;
import org.cometd.annotation.Service;
import org.cometd.annotation.Session;
import org.cometd.bayeux.client.ClientSessionChannel;
import org.cometd.bayeux.server.BayeuxServer;
import org.cometd.bayeux.server.ConfigurableServerChannel;
import org.cometd.bayeux.server.ServerMessage;
import org.cometd.bayeux.server.ServerSession;
import org.cometd.server.authorizer.GrantAuthorizer;
import org.cometd.server.filter.DataFilter;
import org.cometd.server.filter.DataFilterMessageListener;
import org.cometd.server.filter.JSONDataFilter;
import org.cometd.server.filter.NoMarkupFilter;
@Service('chat')
public class ChatService {
private final ConcurrentMap> _members = new ConcurrentHashMap>();
@Inject
private BayeuxServer _bayeux;
@Session
private ServerSession _session;
@Configure ({'/chat/**','/members/**'})
protected void configureChatStarStar(ConfigurableServerChannel channel) {
DataFilterMessageListener noMarkup = new DataFilterMessageListener(new NoMarkupFilter(),new BadWordFilter());
channel.addListener(noMarkup);
channel.addAuthorizer(GrantAuthorizer.GRANT_ALL);
}
@Configure ('/service/members')
protected void configureMembers(ConfigurableServerChannel channel) {
channel.addAuthorizer(GrantAuthorizer.GRANT_PUBLISH);
channel.setPersistent(true);
}
@Listener('/service/members')
public void handleMembership(ServerSession client, ServerMessage message) {
Map data = message.getDataAsMap();
final String room = ((String)data.get('room')).substring('/chat/'.length());
Map roomMembers = _members.get(room);
if (roomMembers == null) {
Map new_room = new ConcurrentHashMap();
roomMembers = _members.putIfAbsent(room, new_room);
if (roomMembers == null) roomMembers = new_room;
}
final Map members = roomMembers;
String userName = (String)data.get('user');
members.put(userName, client.getId());
client.addListener(new ServerSession.RemoveListener() {
public void removed(ServerSession session, boolean timeout) {
members.values().remove(session.getId());
broadcastMembers(room, members.keySet());
}
});
broadcastMembers(room, members.keySet());
}
private void broadcastMembers(String room, Set members) {
// Broadcast the new members list
ClientSessionChannel channel = _session.getLocalSession().getChannel('/members/'+room);
channel.publish(members);
}
@Configure ('/service/privatechat')
protected void configurePrivateChat(ConfigurableServerChannel channel) {
DataFilterMessageListener noMarkup = new DataFilterMessageListener(new NoMarkupFilter(),new BadWordFilter());
channel.setPersistent(true);
channel.addListener(noMarkup);
channel.addAuthorizer(GrantAuthorizer.GRANT_PUBLISH);
}
@Listener('/service/privatechat')
protected void privateChat(ServerSession client, ServerMessage message) {
Map data = message.getDataAsMap();
String room = ((String)data.get('room')).substring('/chat/'.length());
Map membersMap = _members.get(room);
if (membersMap == null) {
Mapnew_room=new ConcurrentHashMap();
membersMap=_members.putIfAbsent(room,new_room);
if (membersMap==null)
membersMap=new_room;
}
String peerName = (String)data.get('peer');
String peerId = membersMap.get(peerName);
if (peerId != null) {
ServerSession peer = _bayeux.getSession(peerId);
if (peer != null) {
Map chat = new HashMap();
String text = (String)data.get('chat');
chat.put('chat', text);
chat.put('user', data.get('user'));
chat.put('scope', 'private');
chat.put('peer', peerName);
ServerMessage.Mutable forward = _bayeux.newMessage();
forward.setChannel('/chat/' + room);
forward.setId(message.getId());
forward.setData(chat);
if (text.lastIndexOf('lazy') > 0) {
forward.setLazy(true);
}
if (peer != client) {
peer.deliver(_session, forward);
}
client.deliver(_session, forward);
}
}
}
class BadWordFilter extends JSONDataFilter {
@Override
protected Object filterString(String string) {
if (string.indexOf('dang') >= 0) {
throw new DataFilter.Abort();
}
return string;
}
}
}
Step 5: Implementing Chat functionality
The online users can be displayed in div. Once you click on a particular user name, it will open a new chat window similar to Google. For each pair of users, it will open a new chat window. To get this behaviour, you should use ‘ chat.window.js‘ which I mentioned before. Chatting in between particular pair of users will continue through a dedicated chat window.
Just after user is logging into your web application as usual way, we should subscribe that user to chat channels. You can do it using the following way.
Note that, I have passed the ‘id’ of online user list container as a configuration parameter. Then, user should be joined with channel as follows.
And also the following configuration for servlet mapping xml files also need to allow the cometd chat path
Step 4: Creating Chat service file for communicating with client or script request processing
/**
* @author Ramachandran
* CometD chat service.
*/
package com.shinnedhawks.cometd;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import javax.inject.Inject;
import org.cometd.annotation.Configure;
import org.cometd.annotation.Listener;
import org.cometd.annotation.Service;
import org.cometd.annotation.Session;
import org.cometd.bayeux.client.ClientSessionChannel;
import org.cometd.bayeux.server.BayeuxServer;
import org.cometd.bayeux.server.ConfigurableServerChannel;
import org.cometd.bayeux.server.ServerMessage;
import org.cometd.bayeux.server.ServerSession;
import org.cometd.server.authorizer.GrantAuthorizer;
import org.cometd.server.filter.DataFilter;
import org.cometd.server.filter.DataFilterMessageListener;
import org.cometd.server.filter.JSONDataFilter;
import org.cometd.server.filter.NoMarkupFilter;
@Service('chat')
public class ChatService {
private final ConcurrentMap> _members = new ConcurrentHashMap>();
@Inject
private BayeuxServer _bayeux;
@Session
private ServerSession _session;
@Configure ({'/chat/**','/members/**'})
protected void configureChatStarStar(ConfigurableServerChannel channel) {
DataFilterMessageListener noMarkup = new DataFilterMessageListener(new NoMarkupFilter(),new BadWordFilter());
channel.addListener(noMarkup);
channel.addAuthorizer(GrantAuthorizer.GRANT_ALL);
}
@Configure ('/service/members')
protected void configureMembers(ConfigurableServerChannel channel) {
channel.addAuthorizer(GrantAuthorizer.GRANT_PUBLISH);
channel.setPersistent(true);
}
@Listener('/service/members')
public void handleMembership(ServerSession client, ServerMessage message) {
Map data = message.getDataAsMap();
final String room = ((String)data.get('room')).substring('/chat/'.length());
Map roomMembers = _members.get(room);
if (roomMembers == null) {
Map new_room = new ConcurrentHashMap();
roomMembers = _members.putIfAbsent(room, new_room);
if (roomMembers == null) roomMembers = new_room;
}
final Map members = roomMembers;
String userName = (String)data.get('user');
members.put(userName, client.getId());
client.addListener(new ServerSession.RemoveListener() {
public void removed(ServerSession session, boolean timeout) {
members.values().remove(session.getId());
broadcastMembers(room, members.keySet());
}
});
broadcastMembers(room, members.keySet());
}
private void broadcastMembers(String room, Set members) {
// Broadcast the new members list
ClientSessionChannel channel = _session.getLocalSession().getChannel('/members/'+room);
channel.publish(members);
}
@Configure ('/service/privatechat')
protected void configurePrivateChat(ConfigurableServerChannel channel) {
DataFilterMessageListener noMarkup = new DataFilterMessageListener(new NoMarkupFilter(),new BadWordFilter());
channel.setPersistent(true);
channel.addListener(noMarkup);
channel.addAuthorizer(GrantAuthorizer.GRANT_PUBLISH);
}
@Listener('/service/privatechat')
protected void privateChat(ServerSession client, ServerMessage message) {
Map data = message.getDataAsMap();
String room = ((String)data.get('room')).substring('/chat/'.length());
Map membersMap = _members.get(room);
if (membersMap == null) {
Mapnew_room=new ConcurrentHashMap();
membersMap=_members.putIfAbsent(room,new_room);
if (membersMap==null)
membersMap=new_room;
}
String peerName = (String)data.get('peer');
String peerId = membersMap.get(peerName);
if (peerId != null) {
ServerSession peer = _bayeux.getSession(peerId);
if (peer != null) {
Map chat = new HashMap();
String text = (String)data.get('chat');
chat.put('chat', text);
chat.put('user', data.get('user'));
chat.put('scope', 'private');
chat.put('peer', peerName);
ServerMessage.Mutable forward = _bayeux.newMessage();
forward.setChannel('/chat/' + room);
forward.setId(message.getId());
forward.setData(chat);
if (text.lastIndexOf('lazy') > 0) {
forward.setLazy(true);
}
if (peer != client) {
peer.deliver(_session, forward);
}
client.deliver(_session, forward);
}
}
}
class BadWordFilter extends JSONDataFilter {
@Override
protected Object filterString(String string) {
if (string.indexOf('dang') >= 0) {
throw new DataFilter.Abort();
}
return string;
}
}
}
Step 5: Implementing Chat functionality
The online users can be displayed in div. Once you click on a particular user name, it will open a new chat window similar to Google. For each pair of users, it will open a new chat window. To get this behaviour, you should use ‘ chat.window.js‘ which I mentioned before. Chatting in between particular pair of users will continue through a dedicated chat window.
Just after user is logging into your web application as usual way, we should subscribe that user to chat channels. You can do it using the following way.
Note that, I have passed the ‘id’ of online user list container as a configuration parameter. Then, user should be joined with channel as follows.
After the java script for creating chat window as follows
function getChatWindowByUserPair(loginUserName, peerUserName) {
var chatWindow;
for(var i = 0; i < chatWindowArray.length; i++) {
var windowInfo = chatWindowArray[i];
if (windowInfo.loginUserName == loginUserName && windowInfo.peerUserName == peerUserName) {
chatWindow = windowInfo.windowObj;
}
}
return chatWindow;
}
function createWindow(loginUserName, peerUserName) {
var chatWindow = getChatWindowByUserPair(loginUserName, peerUserName);
if (chatWindow == null) { //Not chat window created before for this user pair.
chatWindow = new ChatWindow(); //Create new chat window.
chatWindow.initWindow({
loginUserName:loginUserName,
peerUserName:peerUserName,
windowArray:chatWindowArray});
//collect all chat windows opended so far.
var chatWindowInfo = { peerUserName:peerUserName,
loginUserName:loginUserName,
windowObj:chatWindow
};
chatWindowArray.push(chatWindowInfo);
}
chatWindow.show();
return chatWindow;
}
The source code for chat.window.js
/**
* @authorRamachandran(ramakavanan@gmail.com)
*/
function ChatWindow(config) {
var _self = this;
var _peerUserName;
var _loginUserName;
var _config;
this._windowWidth = 200;
this._windowHeight = 200;
this.lastUser = null;
this.windowArray = [];
this.getWindowLeftPosition = function() {
return this.windowArray.length*this._windowWidth;
},
this.getPeerUserName = function() {
return this._peerUserName;
};
this.getLoginUserName = function() {
return this._loginUserName;
};
this.getMessageContainerID = function() {
return this.getLoginUserName() + "_" + this.getPeerUserName();
};
this.getTextInputID = function() {
return this.getLoginUserName() + "_" + this.getPeerUserName() + "_chatInput";
};
this.getWindowID = function() {
return this.getLoginUserName() + "_" + this.getPeerUserName() + "_window";
};
this.hide = function(_self) {
$("#" + _self.getWindowID()).css("display", "none");
};
this.show = function() {
$("#" + this.getWindowID()).css("display", "block");
};
/**
* Returns whether the chat window is currently visible or not
*/
this.isVisible = function() {
return $("#" + this.getWindowID()).css("display") == "none"?false:true;
};
this.addOnClickListener = function(el, fnHandler, context) {
$(el).bind("click", context, function(evt) {
if(context != undefined) {
fnHandler(context);
} else {
fnHandler();
}
return false;
});
};
this.appendMessage = function(fromUser, text, loginUser) {
var userNameCssClass = "";
var textMessageCssClass = "";
if (fromUser == loginUser) {
userNameCssClass = "fromUserName";
textMessageCssClass = "fromMessage";
} else {
userNameCssClass = "toUserName";
textMessageCssClass = "toMessage";
}
if (this.lastUser == fromUser) {
fromUser = "...";
} else {
this.lastUser = fromUser;
fromUser += ':';
}
var chatContainer = $("#" + this.getMessageContainerID());
var sb = [];
sb[sb.length] = '' + fromUser + '';
sb[sb.length] = '' + text + '
';
chatContainer.append(sb.join(""));
chatContainer[0].scrollTop = chatContainer[0].scrollHeight - chatContainer.outerHeight();
};
this.focusTextInput = function() {
$("#" + this.getTextInputID()).focus();
},
this.getWindowBody = function() {
var bodyDIV = document.createElement("div");
bodyDIV.setAttribute("id", this.getMessageContainerID());
bodyDIV.style.width = this._windowWidth + "px";
bodyDIV.style.height = "140px";
bodyDIV.style.position = 'absolute';
bodyDIV.style.left = 0;
bodyDIV.style.bottom = "30px";
bodyDIV.style.overflowY = 'auto';
return bodyDIV;
};
this.getWindowFooter = function() {
var footerDIV = document.createElement("div");
footerDIV.style.width = this._windowWidth + "px";
footerDIV.style.height = "30px";
footerDIV.style.backgroundColor = '#31B404';
footerDIV.style.position = 'absolute';
footerDIV.style.left = 0;
footerDIV.style.bottom = 0;
//create text input
var textInput = document.createElement("input");
textInput.setAttribute("id", this.getTextInputID());
textInput.setAttribute("type", "text");
textInput.setAttribute("name", "chatInput");
textInput.setAttribute("class", "chatInput");
$(textInput).attr('autocomplete', 'off');
$(textInput).keyup(function(e) {
if (e.keyCode == 13) {
$.cometChat.send($(textInput).val(), _self.getPeerUserName());
$(textInput).val('');
$(textInput).focus();
}
});
footerDIV.appendChild(textInput);
return footerDIV;
};
this.getWindowHeader = function() {
var headerDIV = document.createElement("div");
headerDIV.style.width = this._windowWidth + "px";
headerDIV.style.height = "30px";
headerDIV.style.backgroundColor = '#31B404';
headerDIV.style.position = 'relative';
headerDIV.style.top = 0;
headerDIV.style.left = 0;
var textUserName = document.createElement("span");
textUserName.setAttribute("class", "windowTitle");
textUserName.innerHTML = this.getPeerUserName();
var textClose = document.createElement("span");
textClose.setAttribute("class", "windowClose");
textClose.innerHTML = "[X]";
this.addOnClickListener(textClose, this.hide, this);
headerDIV.appendChild(textUserName);
headerDIV.appendChild(textClose);
return headerDIV;
};
this.getWindowHTML = function() {
var windowDIV = document.createElement("div");
windowDIV.setAttribute("id", this.getWindowID());
windowDIV.style.width = this._windowWidth + "px";
windowDIV.style.height = this._windowHeight +"px";
windowDIV.style.backgroundColor = '#FFFFFF';
windowDIV.style.position = 'absolute';
windowDIV.style.bottom = 0;
windowDIV.style.right = this.getWindowLeftPosition() + "px";
windowDIV.style.zIndex = 100;
windowDIV.style.border = '1px solid #31B404';
windowDIV.appendChild(this.getWindowHeader());
windowDIV.appendChild(this.getWindowBody());
windowDIV.appendChild(this.getWindowFooter());
return windowDIV;
};
this.initWindow = function(config) {
this._config = config;
this._peerUserName = config.peerUserName;
this._loginUserName = config.loginUserName;
this.windowArray = config.windowArray;
var body = document.getElementsByTagName('body')[0];
body.appendChild(this.getWindowHTML());
//focus text input just after opening window
this.focusTextInput();
};
}
The source code for comet.chat.js/**
* @author Ramachandran(ramakavanan@gmail.com)
*/
(function($){
$.cometChat = {
_connected:false,
loginUserName:undefined,
_disconnecting:undefined,
_chatSubscription:undefined,
_membersSubscription:undefined,
memberListContainerID:undefined, //'id' of a 'div' or 'span' tag which keeps list of online users.
/**
* This method can be invoked to disconnect from the chat server.
* When user logging off or user close the browser window, user should
* be disconnected from cometd server.
*/
leave: function() {
$.cometd.batch(function() {
$.cometChat._unsubscribe();
});
$.cometd.disconnect();
$.cometChat.loginUserName = null;
$.cometChat._disconnecting = true;
},
/**
* Handshake with the server. When user logging into your system, you can call this method
* to connect that user to cometd server. With that user will subscribe with tow channel.
* '/chat/demo' and '/members/demo' and start to listen to those channels.
*/
join: function(username) {
$.cometChat._disconnecting = false;
$.cometChat.loginUserName = username;
var cometdURL = location.protocol + "//" + location.host + config.contextPath + "/cometd";
$.cometd.configure({
url: cometdURL,
logLevel: 'debug'
});
$.cometd.websocketEnabled = false;
$.cometd.handshake();
},
/**
* Send the text message to peer as a private message. Private messages
* are visible only for relevant peer users.
*/
send:function(textMessage, peerUserName) {
if (!textMessage || !textMessage.length) return;
$.cometd.publish('/service/privatechat', {
room: '/chat/demo',
user: $.cometChat.loginUserName,
chat: textMessage,
peer: peerUserName
});
},
/**
* Updates the members list.
* This function is called when a message arrives on channel /chat/members
*/
members:function(message) {
var sb = [];
$.each(message.data, function() {
if ($.cometChat.loginUserName == this) { //login user
sb[sb.length] = "" + this + "
";
} else { //peer users
sb[sb.length] = "" + this + "
";
}
});
$('#' + $.cometChat.memberListContainerID).html(sb.join(""));
},
/**
* This function will be invoked every time when '/chat/demo' channel receives a message.
*/
receive :function(message) {
var fromUser = message.data.user;
var text = message.data.chat;
var toUser = message.data.peer;
//Handle receiving messages
if ($.cometChat.loginUserName == toUser) {
//'toUser' is the loginUser and 'fromUser' is the peer user.
var chatReceivingWindow = createWindow(toUser, fromUser);
chatReceivingWindow.appendMessage(fromUser, text, $.cometChat.loginUserName);
}
//Handle sending messages
if ($.cometChat.loginUserName == fromUser) {
//'fromUser' is the loginUser and 'toUser' is the peer user.
var chatSendingWindow = createWindow(fromUser, toUser);
chatSendingWindow.appendMessage(fromUser, text, $.cometChat.loginUserName);
}
},
_unsubscribe: function() {
if ($.cometChat._chatSubscription) {
$.cometd.unsubscribe($.cometChat._chatSubscription);
}
$.cometChat._chatSubscription = null;
if ($.cometChat._membersSubscription) {
$.cometd.unsubscribe($.cometChat._membersSubscription);
}
$.cometChat._membersSubscription = null;
},
_connectionEstablished: function() {
// connection establish (maybe not for first time), so just
// tell local user and update membership
$.cometd.publish('/service/members', {
user: $.cometChat.loginUserName,
room: '/chat/demo'
});
},
_connectionBroken: function() {
$('#' + $.cometChat.memberListContainerID).empty();
},
_connectionClosed: function() {
/* $.cometChat.receive({
data: {
user: 'system',
chat: 'Connection to Server Closed'
}
});*/
},
_metaConnect: function(message) {
if ($.cometChat._disconnecting) {
$.cometChat._connected = false;
$.cometChat._connectionClosed();
} else {
var wasConnected = $.cometChat._connected;
$.cometChat._connected = message.successful === true;
if (!wasConnected && $.cometChat._connected) {
$.cometChat._connectionEstablished();
} else if (wasConnected && !$.cometChat._connected) {
$.cometChat._connectionBroken();
}
}
},
_subscribe: function() {
$.cometChat._chatSubscription = $.cometd.subscribe('/chat/demo', $.cometChat.receive); //channel handling chat messages
$.cometChat._membersSubscription = $.cometd.subscribe('/members/demo', $.cometChat.members); //channel handling members.
},
_connectionInitialized: function() {
// first time connection for this client, so subscribe tell everybody.
$.cometd.batch(function() {
$.cometChat._subscribe();
});
},
_metaHandshake: function (message) {
if (message.successful) {
$.cometChat._connectionInitialized();
}
},
_initListeners: function() {
$.cometd.addListener('/meta/handshake', $.cometChat._metaHandshake);
$.cometd.addListener('/meta/connect', $.cometChat._metaConnect);
$(window).unload(function() {
$.cometd.reload();
$.cometd.disconnect();
});
},
onLoad: function(config) {
$.cometChat.memberListContainerID = config.memberListContainerID;
$.cometChat._initListeners();
}
};
})(jQuery);