Hire a web Developer and Designer to upgrade and boost your online presence with cutting edge Technologies

Tuesday, March 31, 2015

CometD Chat Integration with Java Web Application

 

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.


02    <dependency>
03        <groupId>org.cometd.java</groupId>
04        <artifactId>bayeux-api</artifactId>
05        <version>2.5.0</version>
06    </dependency>
07    <dependency>
08         <groupId>org.cometd.java</groupId>
09         <artifactId>cometd-java-server</artifactId>
10         <version>2.5.0</version>
11    </dependency>
12    <dependency>
13         <groupId>org.cometd.java</groupId>
14         <artifactId>cometd-websocket-jetty</artifactId>
15         <version>2.5.0</version>
16         <exclusions>
17             <exclusion>
18                 <groupId>org.cometd.java</groupId>
19                 <artifactId>cometd-java-client</artifactId>
20             </exclusion>
21         </exclusions>
22   </dependency>
23   <dependency>
24        <groupId>org.slf4j</groupId>
25        <artifactId>slf4j-simple</artifactId>
26        <version>1.6.6</version>
27   </dependency>
28   <dependency>
29        <groupId>org.cometd.java</groupId>
30        <artifactId>cometd-java-annotations</artifactId>
31        <version>2.5.0</version>
32   </dependency>
33</dependencies>

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 .


1<filter>
2     <filter-name>continuation</filter-name>
3     <filter-class>org.eclipse.jetty.continuation.ContinuationFilter</filter-class>
4</filter>
5<filter-mapping>
6     <filter-name>continuation</filter-name>
7     <url-pattern>/cometd/*</url-pattern>
8</filter-mapping>

And also the following configuration for servlet mapping xml files also need to allow the cometd chat path


01<servlet>
02    <servlet-name>cometd</servlet-name>
03    <servlet-class>org.cometd.annotation.AnnotationCometdServlet</servlet-class>
04    <init-param>
05         <param-name>timeout</param-name>
06         <param-value>20000</param-value>
07    </init-param>
08    <init-param>
09         <param-name>interval</param-name>
10         <param-value>0</param-value>
11    </init-param>
12    <init-param>
13         <param-name>maxInterval</param-name>
14         <param-value>10000</param-value>
15    </init-param>
16    <init-param>
17         <param-name>maxLazyTimeout</param-name>
18         <param-value>5000</param-value>
19    </init-param>
20    <init-param>
21         <param-name>long-polling.multiSessionInterval</param-name>
22         <param-value>2000</param-value>
23    </init-param>
24    <init-param>
25         <param-name>logLevel</param-name>
26         <param-value>0</param-value>
27    </init-param>
28    <init-param>
29         <param-name>transports</param-name>
30         <param-value>org.cometd.websocket.server.WebSocketTransport</param-value>
31    </init-param>
32    <init-param>
33         <param-name>services</param-name>
34         <param-value>com.shinnedhawks.cometd.ChatService</param-value>
35    </init-param>
36    <load-on-startup>1</load-on-startup>
37</servlet>
38<servlet-mapping>
39    <servlet-name>cometd</servlet-name>
40    <url-pattern>/cometd/*</url-pattern>
41</servlet-mapping>

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.


1$(document).ready(function(){
2    $.cometChat.onLoad({memberListContainerID:'members'});
3});

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.


1function join(userName){
2   $.cometChat.join(userName);
3}
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);