Thursday, January 26, 2012

JSP Practice: Simple chat

This post is about a simple JSP chat page, it's really simple, contains only:
2 jsp pages,
3 servlet,
1 css file
1 js file

and it use servlet3 annotation to do config so no web.xml

described as follows:

index.jsp

<%@ page isErrorPage="true" language="java"
    contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ page isELIgnored ="false" %>
<html>
    <head>
        <meta http-equiv="Content-Type" 
            content="text/html; charset=UTF-8"/>
        <title>Login page</title>
    </head>
    <body>
        <form action="login.go" method="post">
            <span>Type a name then press login to enter chat room</span>
            <input id="userId" type="text" value="Your ID" name="uid" />
            <input type="submit" value="login"/>
        </form>
    </body>
</html>

only a form, post to login.go with the user id

Login.java

package test.jsp.simplechat;

import java.io.IOException;
import java.util.*;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet(name="LoginServlet", urlPatterns={"/login.go"},
        loadOnStartup=1)
public class Login extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException {
        String uid = new String(req.getParameter("uid").getBytes("ISO-8859-1"), "UTF-8");
        String newUid = uid;
        int i = 2;
        Map chat = Chat.getChatMap();
        synchronized (chat) {
            // prevent uid conflict
            if ("you".equalsIgnoreCase(newUid))
                newUid = uid + i++;
            while (chat.containsKey(newUid))
                newUid = uid + i++;
            uid = newUid;
            chat.put(uid, new ArrayList());
        }
        req.getSession().setAttribute("UID", uid);
        resp.sendRedirect("chat.jsp");
    }
}

the login servlet, check the user id here, if the user id is 'you' then append a number to it, because 'you' is a key word for display self message. Also append number to solve any id conflict.

Note the

String uid = new String(req.getParameter("uid").getBytes("ISO-8859-1"), "UTF-8");

is required or the utf-8 char will not stored to map correctly.

Finally store uid in session then redirect to chat.jsp

chat.jsp

<%@ page isErrorPage="true" language="java"
    contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ page isELIgnored ="false" %>
<!-- Redirect to index.jsp if no UID -->
<c:if test="${UID == null}">
    <c:redirect url="index.jsp" />
</c:if>
<html>
    <head>
        <meta http-equiv="Content-Type" 
            content="text/html; charset=UTF-8"/>
        <title>Login page</title>
        <link href="css/chat.css" rel="stylesheet" type="text/css">
        <script type="text/javascript" src="js/chat.js"></script>
    </head>
    <body>
        <form action="logout.go" method="post">
            <div>This is chat page</div>
            <div>Type message then press ENTER key to send message</div>
            <div>Click logout to return the login page</div>
            <div>Your name: <span id="uid">${UID}</span></div>
            <div id="content" class="content"></div>
            <div>
                <!-- listen to keyup to send message if enter pressed -->
                <textarea class="msg-input" onkeyup="chat.dokeyup(event);">input text here</textarea>
            </div>
            <input type="submit" value="logout" />
        </form>
    </body>
</html>

First check UID and redirect to 'login.jsp' if UID is not in session.
link to style and js file, there is a timer will start while load js file.
listen to keyup event of input area to send content if enter pressed,
logout if logout button clicked.

chat.js

// init
window.chat = {};

// post to send message to chat.do
chat.sendMsg = function (msg) {
    var request;

    msg = msg.replace(/&/g, '&amp;') // encode to prevent XSS
                .replace(/</g, '&lt;')
                .replace(/>/g, '&gt;')
                .replace(/"/g, '&quot;')
                .replace(/\n/g, '<br />'); // replace textarea newline to line break tag
    alert(msg);
    if (request = this.getXmlHttpRequest()) {
        request.open('POST', 'chat.do?action=send&msg='+msg+'&time='+new Date().getTime());
        request.send(null);
        chat.updateContent('<div>You said: '+msg+'</div>');
    }
};

// post 'get' action to chat.do to require new message if any
chat.startListen = function () {
    if (!chat.listen)
        chat.listen = setInterval (function () {
            var request;
            if (request = chat.getXmlHttpRequest()) {
                request.open('POST', 'chat.do?action=get&time='+new Date().getTime());
                request.send(null);
                request.onreadystatechange = function() {
                    if(request.readyState === 4) {
                        if(request.status === 200) {
                            var json = request.responseText;
                            // has new message
                            if (json && json.length) {
                                // parse to array
                                var obj = eval('(' + json + ')');
                                var msg = '';
                                for (var i = 0; i < obj.length; i++) {
                                    msg += '<div>'+obj[i]+'</div>';
                                }
                                chat.updateContent(msg);
                            }
                        } else if(request.status === 400 || request.status === 500)
                            document.location.href = 'index.jsp';
                    }
                };
            }
        }, 3000);
};

chat.updateContent = function (msg) {
    var content = document.getElementById('content'),
        atBottom = (content.scrollTop + content.offsetHeight) >= content.scrollHeight;
    content.innerHTML += msg;
    // only scroll to bottom if it is at bottom before msg added
    if (atBottom)
        content.scrollTop = content.scrollHeight;
};
chat.dokeyup = function (event) {
    if (!event) // IE will not pass event
        event = window.event;
    if (event.keyCode == 13 && !event.shiftKey) { // ENTER pressed
        var target = (event.currentTarget) ? event.currentTarget : event.srcElement,
            value = target.value;
        // make sure not only space char
        if (value && value.replace(/^\s\s*/, '').replace(/\s\s*$/, '').length > 0) {
            this.sendMsg(target.value);
            target.value = '';
        }
    }
};
// get the XmlHttpRequest object
chat.getXmlHttpRequest = function () {
    if (window.XMLHttpRequest
        && (window.location.protocol !== 'file:' 
        || !window.ActiveXObject))
        return new XMLHttpRequest();
    try {
        return new ActiveXObject('Microsoft.XMLHTTP');
    } catch(e) {
        throw new Error('XMLHttpRequest not supported');
    }
};
onload = function () {
    chat.startListen();
};

This file do two things,

It starts a timer after page loaded by chat.startListen();, it will periodically send an ajax request to server to get the latest message if any then put them into content div and scroll content to bottom as need.

The keyup event of input area will trigger the chat.dokeyup, then call chat.sendMsg if enter pressed and input area is not empty.

Chat.java

package test.jsp.simplechat;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.*;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import net.sf.json.JSONArray;

@WebServlet(name="ChatServlet", urlPatterns={"/chat.do"},
  loadOnStartup=1)
public class Chat extends HttpServlet {
 /**
  * 
  */
 private static final long serialVersionUID = 113880057049845876L;
 // message map, mapping user UID with a message list
 private static Map<String, List<String>> _chat = new HashMap<String, List<String>>();
 @Override
 protected void doPost(HttpServletRequest req, HttpServletResponse resp)
  throws ServletException, IOException {
  req.setCharacterEncoding("UTF-8");
  String action = req.getParameter("action");
  // send message
  if ("send".equals(action)) {
   // get param with UTF-8 enconding
   String msg = new String(req.getParameter("msg").getBytes("ISO-8859-1"), "UTF-8");
   String uid = (String)req.getSession().getAttribute("UID");
   for (String s : _chat.keySet()) {
    if (!s.equals(uid)) {
     synchronized (_chat.get(s)) {
      // put message to any other user's msg list
      _chat.get(s).add(uid+" said: "+msg);
     }
    }
   }
  } else if ("get".equals(action)) { // get message
   String uid = (String)req.getSession().getAttribute("UID");
   if (uid == null)
    resp.sendError(HttpServletResponse.SC_BAD_REQUEST);
   List<String> l = _chat.get(uid);
   synchronized (l) {
    if (l.size() > 0) {
     // for UTF-8 chars
     resp.setCharacterEncoding("UTF-8");
     PrintWriter out = resp.getWriter();
     JSONArray jsna = new JSONArray();
     // add all msg to json array and clear list
     while (l.size() > 0)
      jsna.add(l.remove(0));

     out.println(jsna);
     out.close();
    }
   }
  }
 }
 public static Map<String, List<String>> getChatMap () {
  return _chat;
 }
}

This class hold a map which mapping the user id with a message list,
when a user request send the message, it put the message to all other user's message list.
when a user request get message, it put all message from that user's message list into a json array and write it to response writer.

chat.css

.content {
 width: 600px;
 height: 400px;
 overflow: auto; 
 border: 1px solid #991111;
}
.msg-input {
 width: 600px;
 border: 1px solid #119911;
}

simply style the content and message input area.

Logout.java

package test.jsp.simplechat;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet(name="Logout", urlPatterns={"/logout.go"},
        loadOnStartup=1)
// practice: invalidate session
public class Logout extends HttpServlet {
    /**
     * 
     */
    private static final long serialVersionUID = -6175876557872938832L;

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException {
        req.getSession().invalidate();
        resp.sendRedirect("index.jsp");
    }
}

Simply invalidate the session and redirect to index.jsp

Download:
Download full project from github:
https://github.com/benbai123/JSP_Servlet_Practice/tree/master/Practice/JSPChat

Dependency:
The required jar files:
commons-beanutils-1.8.3.jar
commons-collections-3.2.1.jar
commons-lang-2.5.jar
commons-logging-1.1.1.jar
ezmorph-1.0.6.jar
json-lib-2.4-jdk15.jar                     // these six are for json
jstl-1.2.jar                                      // this one is for JSTL

35 comments:

  1. Hi, thanks for the tutorial, but can you tell some other kinf of communication between the servlet and the .js file. I mean can we use some kind of our own object type instead of JSONArray?

    ReplyDelete
    Replies
    1. You can create js object based on java object in your way as needed.

      The steps are as below:
      1. Build js string based on the specific object at server side.
      2. Response that string to client side.
      3. Evaluate that string to build js object as needed at client side.

      Delete
  2. Infact your code does'n work properly with special symbols like <>#, expecially with %, then throws exception "Character decoding failed. Parameter [msg] with value [%%3Cbr%20/%3E] has been ignored." Inside the Map we receive null. How can we overcome this thing?

    ReplyDelete
    Replies
    1. Regarding < and >, I believe they should work well since I encoded them in chat.js line 9 and 10. Regarding # and %, you can also encode them (i.e., add .replace(/%/g, '&#37;').replace(/#/g, '&#35;')) as needed, please refer to HTML ASCII Reference for more information

      http://www.w3schools.com/tags/ref_ascii.asp

      If there are still any issues you may need to check the character encoding of your system or web container.

      And yes, this is a simple practice, there should lots of places have to be improved.

      Delete
  3. hi,

    This is nice and simple code.

    I am trying your code and it is working for login page. after clicking on login that page gives me 404 erroe. How to over come this.
    Can i have to add any class in web.xml?

    ReplyDelete
    Replies
    1. This project works with servlet 3, you can run this project with the jars listed in Dependency sub section in Tomcat 7 with servlet 3.0 settings.

      Delete
    2. For servlet 2.4, please refer to the web.xml

      https://gist.github.com/benbai123/5346429#file-web-xml

      tested with tomcat 6

      Delete
    3. Thanks It is working... but i am facing some error on Chat. java page. Error is "The method add(String) is undefined for the type JSONArray"

      Delete
    4. Did you use the required jars listed in Dependency section? There are a lot of JSON implementation, they might have different API.

      Delete
  4. Thanks for your reply... i have check all required jars listed in Dependency section.. but no luck.. Getting same error.

    ReplyDelete
    Replies
    1. I'm not sure what is your problem, maybe you can try the war file directly

      http://dl.dropbox.com/u/90546423/JSPChat24.war

      Delete
  5. Hello, I have a problem. I have opened it in two browsers to try it and when I write in one of them i get a logout in the other window and I do not know what it is happening ¿any idea?

    ReplyDelete
    Replies
    1. You can check whether this line below within chat.js is executed:

      document.location.href = 'index.jsp';

      if so this means the ajax request cannot connect to your page

      Delete
    2. I have the same problem.What should i do to solve this??

      Delete

    3. it should be redirected to index.jsp by chat.js Line 44
      https://goo.gl/tJcjYd

      probably because server response a SC_BAD_REQUEST error
      ( Chat.java Line 45 https://goo.gl/21Geuo )

      or a HTTP Code 400 Error caused by
      the Web Server cannot manage the resource properly
      the sample just tested with Tomcat 7

      To check the status,
      you can check the status code with
      alert(request.status)
      or
      console.log(request.status)
      (if pressed F12 to open the console)

      To check the get action,
      you can output uid before Chat.java Line 44 by
      System.out.println(uid);

      You can also try to debug with debugger,
      some tutorial:
      Java Debugging with Eclipse - Tutorial
      http://goo.gl/kaNlJ9

      Using the Console
      https://goo.gl/XKVdGR

      Delete
  6. this is only shows my message not other messages

    ReplyDelete
    Replies
    1. This may related to your server/JEE version or some other reason, the test environment of this article is Tomcat7/JDK6 (if memory served :p)

      Delete
  7. what is the mean of chat.do logout.go

    ReplyDelete
    Replies
    1. in .jsp, it means the form will post to the url chat.go,

      in .java, it means the servlet will process request that post to chat.go

      Delete
  8. please submit the demo of your project

    ReplyDelete
  9. Your blog has excellent information On JSP.

    From
    www.javatportal.com

    ReplyDelete
  10. I got a index page ,login redirects to the chat page ,then what about messages they are not displayed ??

    ReplyDelete
    Replies
    1. With this sample they are just skipped, you will need to use a database if you want new logged in user can see all old message.

      Delete
  11. This comment has been removed by the author.

    ReplyDelete
  12. I can only see the messages which I type.But I am not getting any messages from the server .

    ReplyDelete
    Replies
    1. To see messages from multiople users, you need to login with another browser (for different session) and type messages in both browser.

      Delete
    2. will this work like a chat box ?just like any other chat engine ?

      Delete
    3. guess not, basically I do not know what other chat engine did, this is just a simple practice with respect to share message between multiple users.

      Delete
  13. I send a message and someone should be able to reply to the message does it wok like that ??

    ReplyDelete
    Replies

    1. Similar to that but not the same,

      chat.js Line 15 it sends message to server
      https://goo.gl/aKvfwu

      then Chat.java Line 38 store the message to current users' message List
      https://goo.gl/lOfZgN

      Note that the message will be sent to all users

      after that when a user POST a request that action=‘get’
      ( chat.js Line 27 https://goo.gl/E8ulE4 )

      the server will response all unread message
      ( Chat.java Line 55 https://goo.gl/s52FtV )

      to send message to specific user,
      you will need to add an input field for target user id,
      send this info to server with message,
      and put the message to only the list of target user.

      Delete
    2. I will try this but will it work like a messenger where 2 people can have a conversation??

      Delete
    3. nope, it will similar with the 'send private message' feature in a public chat room.

      to work like a private conversation you will need one more options (probably a checkbox or completely different page) to let user refuse all messages except the message from specific id

      Delete
  14. thank you for this application, i test the app with tow browsers, when start typing after Enter the other browser redirect to index, I traced the error the request.status is 500, how can i fix the error

    ReplyDelete
    Replies
    1. Regarding low browsers, do you mean IE7 or below? Are there any error in the console?

      Delete