by Frank W. Zammetti

AjaxChat: Chatting the AJAX Way, Part 2

news
Sep 18, 200635 mins

Start building your own AJAX-based chat room

Now that we have examined all the files that effectively make up the presentation of AjaxChat, we’ll now begin to explore the server-side code, starting with a simple class, AjaxChatConfig.

AjaxChatConfig.java

The AjaxChatConfig is a typical JavaBean with fields that correspond to the possible parameters in app-config.xml. It also contains what is my typical toString() method that I use in most beans:

 

/** * Overridden toString method. * * @return A reflexively built string representation of this bean. */ public String toString() {

String str = null; StringBuffer sb = new StringBuffer(1000); sb.append("[" + super.toString() + "]={"); boolean firstPropertyDisplayed = false; try { Field[] fields = this.getClass().getDeclaredFields(); for (int i = 0; i < fields.length; i++) { if (firstPropertyDisplayed) { sb.append(", "); } else { firstPropertyDisplayed = true; } sb.append(fields[i].getName() + "=" + fields[i].get(this)); } sb.append("}"); str = sb.toString().trim(); } catch (IllegalAccessException iae) { iae.printStackTrace(); } return str; } // End toString().

This code uses reflection to display the beans’ contents, which is very handy during debugging. Note that the fields and accessor methods are all static. This is an easy way to have a global settings cache in effect. You may notice that the mutator methods are not static. This is because Commons Digester is used to populate this object, and Digester requires that it be able to instantiate the object. Therefore, while there is probably no harm in making them static, there is no need either. It is therefore a little safer to make them nonstatic, so that if someone were inclined to improperly populate this object manually, they would have to instantiate it to do so.

LobbyActionForm.java (and all the ActionForms essentially)

Next up, we’ll look at the one ActionForm in AjaxChat, LobbyActionForm. As you will recall, a Struts ActionForm is really just a simple JavaBean that happens to extend from a specific class (ActionForm). It is, like our AjaxChatConfig object, just a collection of fields, accessors, and mutators. Also, again we see the same toString() method as before.

MessageDTO.java

AjaxChat makes use of three DTOs (data transfer objects) to pass information around. The first of these is the MessageDTO.

Notice here that the postedBy member is actually of type UserDTO, which we’ll see in a moment. This DTO contains all the pieces of information that together make up a message posted to a chat room, including who posted it, when the post was made, and of course the text of the message. Otherwise, it is once more just a simple, perfectly typical JavaBean.

RoomDTO.java

The next DTO is the RoomDTO. This DTO does a little more than other DTOs, as we’ll see. The reason this DTO is slightly different than the rest is that when AjaxChat starts up, an instance of this class will be instantiated for each room configured, and that object will be stored in the AjaxChatDAO that we’ll look at shortly. It is not simply a container for pieces of information, as most DTOs are. Instead, you could almost think of this DTO as more of a domain object than anything else because it contains some functional code as well. For instance, when a user is added to the room, the addUser() method is fired:

 

/** * Adds a user to the list of users chatting in the room. The user WILL NOT * be added if they are already in the collection, which should deal with * the user clicking the Refresh button on their browser. * * @param inUser The user to add to the list of users chatting in the room. */ public void addUser(UserDTO inUser) {

if (log.isDebugEnabled()) { log.debug("RoomDTO addUser()..."); } boolean userAlreadyInRoom = false; for (Iterator it = users.iterator(); it.hasNext();) { UserDTO user = (UserDTO)it.next(); if (user.getUsername().equalsIgnoreCase(inUser.getUsername())) { userAlreadyInRoom = true; } } if (!userAlreadyInRoom) { if (log.isDebugEnabled()) { log.info("Adding user to room: " + inUser); } users.add(inUser); Collections.sort(users); }

} // End addUser().

This method checks to see if the user is already in the room and, of course, does not allow them to join the room if they are. This should generally not happen; the check is performed because it theoretically could under certain very unusual circumstances. This would require certain things happening with precise timing, so it is extremely unlikely. Still, the check is performed to ensure a user is not put in a room twice. This method also sorts the list of users once the user has been added to the collection of users chatting in the room so that when it is displayed, it is in alphabetical order (better to do this sort only when someone joins the room to avoid the overhead of doing it with each user list update request).

Just in case you are new to Jakarta Commons Logging (JCL), let me mention that the following code is how logging is done in JCL:

 if (log.isDebugEnabled()) {
   log.debug("RoomDTO addUser()...");
} 

The if check you see first is called a “code guard.” The reason for this is that logging statements, in some cases, should be avoided if not necessary. One of the criteria that determines that necessity is often whether the application is being run in “debug” mode. So, logging statements should generally be wrapped in a code guard to see if the application is running at some specific logging level.

When I said that logging should be avoided in some cases, interestingly, this is not one of those cases! When log.debug() is called, it will do the equivalent of log.isDebugEnabled(). So, you may think that is less efficient because the check will be performed twice here, and you would be right. However, imagine the following logging statement:

 log.debug("User: " + UserDTO.getName() + ", Age: " + UserDTO.getAge());  

If you were to execute that without a code guard, the check would only be performed once. However, the problem is that regardless of whether the message actually gets logged—that is, whether the check returns true or false—the string concatenation will always be performed first. Therefore, even if the application is not in debug mode, which means the message would not be logged, you still incur the overhead of the string construction, and that overhead far exceeds that of doing the check twice. So, wrapping such a line in a code guard means that if the message is not going to be logged, that will be determined before the message is constructed, making it considerably more efficient.

So, why is a code guard not necessary in this case? It should be obvious by now: there is no string construction, so there is no penalty for letting log.debug() do the check itself, and it would still only happen once. Is there any harm in doing this anyway? In absolute terms, yes, because you will always have a double comparison when the message is to be logged. However, it is a relatively small penalty to pay, and code guards are a good habit to have, especially if you ever need to add to the message later—it will be one less thing to think about.

Another place where there is more going on than in a typical DTO is in the getMessages() method:

 

/** * This method returns all messages after the given datetime. * * @param inDateTime The Datetime from which all subsequent messages * will be returned. * @return List of messages. */ public Vector getMessages(Date inDateTime) {

if (log.isDebugEnabled()) { log.debug("RoomDTO getMessages()..."); } // Scan through the list of messages for the room and find any that were // posted after the given datetime, add those to a list to return. Vector al = new Vector(); for (Iterator it = messages.iterator(); it.hasNext();) { MessageDTO message = (MessageDTO)it.next(); if (message.getPostedDateTime().after(inDateTime)) { if (log.isDebugEnabled()) { log.debug("Returning message: " + message); } al.add(message); } } return al;

} // End getMessages().

When the client requests the chat history be updated, this method is called. It looks at the date and time that the last such request was made and only returns messages posted subsequent to that.

One last item to point out is the postMessage() method:

 

/** * Posts a message to the room. * * @param inMessage A MessageDTO instance containing all the necessary * details for the message being posted. */ public void postMessage(MessageDTO inMessage) {

if (messages.size() > AjaxChatConfig.getMaxMessages()) { messages.clear(); } messages.add(inMessage);

} // End addMessage().

When a message is posted to the room, a check is made to see if the size of the collection of messages in the room has exceeded the limit configured in the app-config.xml file. If that threshold has been exceeded, the history is deleted. This is simply to conserve memory resources on the server. The messages collection in this DTO can really be thought of as a buffer. Remember that a client always gets only those messages posted subsequent to the last request they made for the list. Therefore, they will never be able to get the entire chat history this collection stores unless there happened to be more posts than the limit is configured for (250 by default). Remember that the update interval for the chat history is 1 second by default. What this all boils down to is that there would have to be more than 250 messages posted to the room in a single second before any client appears to have “lost” messages (i.e., did not see a message posted to the room). In other words, there is a buffer of 250 messages per room. You would not want the collection to grow unbounded because after a while, the memory utilization would become a problem. At the same time, you do not want the buffer to be so small that there is a chance of missing messages, and 250 seems like a reasonable value to attain both goals.

UserDTO.java

The last DTO in the queue to review is the UserDTO. Again, there is nothing especially unusual going on here. Note that the Comparable interface is implemented, which is why the compareTo() method is implemented. This allows us to sort the list of users as seen in the RoomDTO. This bean stores the username, as well as when the last AJAX request was received. This is how we determine which messages from the chat history to return with each update request.

GetMessagesAction.java

We now come to the action package, which contains all our Struts Actions. The first one we encounter is named GetMessagesAction.

The first thing done here is to get a reference to the current session. We then synchronize on the session. Why, you ask? Because some containers do not provide thread safety to the session object, which in fact is not mandated by the servlet spec, and therefore the application is responsible for ensuring noncorruption of data. Once that is done, we get from the session the UserDTO object. From it, we pull the UserDTO object and set its lastAjaxRequest field to the current date and time. This is used by the UserClearerDaemonThread to remove “stale” users from the room they were chatting in.

We also get the name of the room the user is chatting in, and we also look for an attribute named lastDateTime in session. This stores the date and time of the last message that was sent to the client. With these two pieces of information, we can call the getMessages() method of the AjaxChatDAO, which returns to us a Vector of messages.

The Action then iterates over that Vector and constructs the XML we previously saw from it and writes it to the response. Note that the postedDateTime is saved for each message, so that when the last message is processed, that value is stored in session under lastDateTime. Therefore, the next time this Action is executed, only messages posted after the last one sent during this cycle will be returned.

 // Now iterate over the collection of messages we got and construct our
// XML.
StringBuffer xmlOut = new StringBuffer(4096);
xmlOut.append("<messages>");
for (Iterator it = messages.iterator(); it.hasNext();) {
   MessageDTO message = (MessageDTO)it.next();
   xmlOut.append("<message>");
   xmlOut.append("<postedBy>" +
      StringEscapeUtils.escapeXml(message.getPostedBy().getUsername()) +
      "</postedBy>");
   xmlOut.append("<postedDateTime>" +
      new SimpleDateFormat().format(message.getPostedDateTime()) +
      "</postedDateTime>");
   xmlOut.append("<msgText>" +
      StringEscapeUtils.escapeXml(message.getText()) + "</msgText>");
   xmlOut.append("</message>");
l   astDateTime = message.getPostedDateTime();
}
xmlOut.append("</messages>"); 

Note that null is returned, which indicates to Struts that the response is now fully formed and no forward or redirect should take place. This is typical in an Action that services an AJAX request.

JoinRoomAction.java

Next up in our barrage of Struts Actions is JoinRoomAction. This is a pretty simple one. We retrieve the name of the room the user wants to join from the LobbyActionForm. We then store that in session, and call the addUserToRoom() method of the AjaxChatDAO, which takes care of all the heavy lifting. From there, we return the showroom forward, which sends the user the room.jsp page, and they are then in the room and ready to chat.

LeaveRoomAction.java

The LeaveRoomAction is next and is the antithesis of JoinRoomAction. This works essentially the same as JoinRoomAction, but the removeUserFromRoom() AjaxChatDAO method is used instead.

ListUsersInRoomAction.java

The ListUsersInRoomAction is used to, wait for it…list the users chatting in a room! This Action is called in response to an AJAX request, so we need to update the lastAjaxRequest field in the UserDTO object so that the UserClearedDaemonThread will not remove the user. After that, it is a simple matter of getting the name of the room the user is in from the session and passing that along to the getUserList() method of the AjaxChatDAO.

From that method, we get back a Vector of UserDTO objects, which we iterate over and construct XML for, as described earlier. Here again we return null so Struts knows the response is done, and that is that.

LobbyAction.java

The LobbyAction is the code that executes when the lobby page is shown. Before explaining this class, I would like to note the usage of Commons Logging throughout the AjaxChat codebase. Each class instantiates a static instance of Log, and this is used throughout for various messages, like so:

 /**
 * Log instance.
 */
private static Log log = LogFactory.getLog(LobbyUpdateStatsAction.class); 

Commons Logging, if you have never used it before, is nice because it sits between your code and the code of some logging package such as log4j. It insulates your code from the underlying logging implementation, so that if you later decide to use J2EE logging instead of log4j, your code will not need to be touched; only the configuration of the logging package does.

Moving on to the Action code itself, the first thing we see is again an update of the lastAjaxRequest field in the UserDTO because this Action services another AJAX request. After that, we call the getRoomsUserCounts() method of the AjaxChatDAO. This returns to us a LinkedHashMap. This collection was chosen because it allows us to have a known iteration order over the collection while still allowing for random access to the elements by key value. In fact, the next thing we do is indeed iterate over the collection.

We construct our XML during this iteration. Note the use of the escapeXml() method in StringEscapeUtils. This is a method in the Commons Lang package, which produces a String that is safe for insertion into XML—that is, certain characters are converted to entity strings. Since we cannot be sure what the developer has configured for the room names, this is a necessary safety. Once the XML is constructed and written to the response, we are done, so null is returned.

LoginAction.java

In Listing 1 we see the LoginAction class. This is worth looking at in its entirety here.

Listing 1. LoginAction Class

 

package com.apress.ajaxprojects.ajaxchat.action;

import java.util.Date; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.struts.action.Action; import org.apache.struts.action.ActionForm; import org.apache.struts.action.ActionForward; import org.apache.struts.action.ActionMapping; import org.apache.struts.action.ActionMessage; import org.apache.struts.action.ActionMessages; import com.apress.ajaxprojects.ajaxchat.dao.AjaxChatDAO; import com.apress.ajaxprojects.ajaxchat.dto.UserDTO; import com.apress.ajaxprojects.ajaxchat.filter.SessionCheckerFilter;

/** * This is a Struts Action that is called when the user clicks the Login button * on the welcome screen. * * @author <a href="mailto:fzammetti@omnytex.com">Frank W. Zammetti</a>. */ public class LoginAction extends Action {

/** * Log instance. */ private static Log log = LogFactory.getLog(LoginAction.class); /** * Execute. * * @param mapping ActionMapping. * @param inForm ActionForm. * @param request HttpServletRequest. * @param response HttpServletResponse. * @return ActionForward. * @throws Exception If anything goes wrong. */ public ActionForward execute(ActionMapping mapping, ActionForm inForm, HttpServletRequest request, HttpServletResponse response) throws Exception {

if (log.isDebugEnabled()) { log.debug("execute()..."); }

HttpSession session = request.getSession(); if (log.isDebugEnabled()) { log.debug("session = " + session); } synchronized (session) {

// Get the username the user entered. String username = (String)request.getParameter("username"); if (log.isDebugEnabled()) { log.debug("username = " + username); }

ActionForward af = null; if (session != null && session.getAttribute(SessionCheckerFilter.LOGGED_IN_FLAG) != null) { // User is already logged in, they probably hit refresh while in the // lobby, so go there now. if (log.isDebugEnabled()) { log.debug("User already logged in"); } // There is still a minor potential problem... if by chance the user // was logged in and the app was restarted and sessions were persisted, // the user object in session can be null. So, we'll check for that, // and re-create the user if applicable. if (session.getAttribute("user") == null) { if (log.isDebugEnabled()) { log.debug("User object null in session, so re-creating"); } UserDTO user = new UserDTO(username); user.setLastAJAXRequest(new Date()); session.setAttribute("user", user); } af = mapping.findForward("gotoLobby"); } else { if (username == null || username.equalsIgnoreCase("")) { // Username was not entered, so they can't come in. if (log.isDebugEnabled()) { log.debug("Username not entered"); } ActionMessages msgs = new ActionMessages(); msgs.add(ActionMessages.GLOBAL_MESSAGE, new ActionMessage("messages.usernameBlank")); saveErrors(request, msgs); af = mapping.findForward("fail"); } else { if (AjaxChatDAO.getInstance().isUsernameInUse(username)) { // The username is already in use, so they can't have it. if (log.isDebugEnabled()) { log.debug("Username already in use"); } ActionMessages msgs = new ActionMessages(); msgs.add(ActionMessages.GLOBAL_MESSAGE, new ActionMessage("messages.usernameInUse")); saveErrors(request, msgs); af = mapping.findForward("fail"); } else { // Everything is OK, so create a new UserDTO and put it in session. if (log.isDebugEnabled()) { log.debug("Username being logged in"); } UserDTO user = new UserDTO(username); user.setLastAJAXRequest(new Date()); session.setAttribute("user", user); session.setAttribute(SessionCheckerFilter.LOGGED_IN_FLAG , "yes"); // Lastly, add this user to the list of logged on users. AjaxChatDAO.getInstance().logUserIn(user); af = mapping.findForward("gotoLobby"); } } }

if (log.isDebugEnabled()) { log.debug("LoginAction complete, forwarding to " + af); } return af;

}

} // End execute().

} // End class.

The LoginAction is a bit of a misnomer because a login per se is not occurring. Instead, we are simply determining that the username entered is unique and that the user is not already logged in. So, the first check that is done is to be sure the session does not contain the LOGGED_IN_FLAG value. If it is present, the user is already logged in and we send them right to the lobby. We also in this case check for the UserDTO in session. If it is not present, we re-create it. This is done because in some rare circumstances, where the app server is persisting sessions and it is restarted while the user is logged in, the session can be re-created but not all the data within it. This may be a bug in one particular app server I ran this app on (Resin), but in any case, adding this code solved the problem, and it does no harm if things are working as expected anyway.

Next, if the LOGGED_IN_FLAG is not found in session, then the username request parameter is examined. If none was entered, the user will be returned back to the login screen with a message indicating the error. Assuming they did enter a username, though, we call on the AjaxChatDAO to see if the username is already in use. If it is, the user is sent back to the login page with a message indicating they need to select a new username. Lastly, if all of these checks are passed, a new UserDTO is created and populated with the username and the current date and time to represent the last AJAX request. Although there has not been an AJAX request yet, we want to start with a date and time recorded so that the UserClearerDaemonThread will not immediately boot the user, should that fire before the first real AJAX request is made in the lobby. Lastly, we ask the AjaxChatDAO to add this user to the list of logged-in users, and send them to the lobby to begin.

LogoutAction.java

Of course, a LoginAction would be little good without a LogoutAction! As the comments indicate, there is not much to do here. We call on the AjaxChatDAO to remove the user from all rooms. This is really redundant, as they would have been removed when exiting a room, but because a client might use the back button or other manual navigation mechanisms that might avoid the leave-room code, this avoids the problem of having users lingering in a room they are no longer chatting in. In addition, we return a nice message informing them they have logged out, invalidate the session, and finally forward back to index.jsp. The message is a standard Struts feature, and can be seen here:

 // Display a nice message for the user informing them they are logged out.
ActionMessages msgs = new ActionMessages();
msgs.add(ActionMessages.GLOBAL_MESSAGE,
   new ActionMessage("messages.loggedOut"));
saveErrors(request, msgs); 

We are pulling a message from our message bundle and setting it under the GLOBAL_MESSAGE key, which the Struts tags we saw earlier on the index.jsp page can find.

The SaveErrors() is a method of the Action base class that takes care of all the details of getting this back to the view (the JSP) for us.

PostMessageAction.java

Finally, as far as Actions go, we see the PostMessageAction, which, as you can guess, is called via AJAX to post a message to the room.

This Action makes use of the DynaActionForm declared in struts-config.xml to receive the text of the message to post. Using this, along with the username taken from the UserDTO object in session, a MessageDTO object can be constructed that is passed to the AjaxChatDAO for storage. The lastAjaxRequest field is also updated to avoid the user being booted for inactivity.

UserClearerDaemonThread.java

AjaxChat actually commits a Java Webapp faux pas by spawning a background thread to do some periodic processing. The UserClearerDaemonThread class is the culprit.

Spawning threads in a Webapp is generally frowned upon and should only be done with extreme caution. The reason is that the container is not in control of the resource and cannot deal with its lifecycle correctly. In this case, it is a fairly noncritical function and one that can be interrupted with no ill effect (i.e., you would not want to spawn a thread that updated a database because if the server shuts down in the middle of an update, you will probably wind up with inconsistent data). With a few caveats in mind, background threads like this are relatively safe, although you still generally want to try to find another way of accomplishing your goals if possible. Also, it is virtually never good to spawn a thread to process a request, so if you find yourself thinking of doing that, I highly recommend you revisit your design first.

The purpose of this thread is to clear out any users who do not properly log out of the application. If they navigate to another page, or close their browser, the application needs to clear them out as soon as possible. The problem is the obvious answer to this, a SessionListener, is not implemented precisely the same way in all containers. Some of them inform you when the session is about to be destroyed, others only after it has been destroyed. The problem is we always need access to the UserDTO object in session so that we can get the username and clear them out of the collection of chatters in each room. Therefore, we cannot count on a SessionListener to do the trick. So this background thread performs the function. It fires every few seconds, as configured in app-config.xml, and calls the removeInactiveUsers() method of the AjaxChatDAO, which we’ll be looking at next. This fulfills our needs and avoids any container differences.

It is also worth noting that this thread will be started as a daemon thread. This is necessary so that the container can shut down and not be held up by the thread, which will happen under some containers, Tomcat as an example. Making it a daemon thread avoids this problem.

AjaxChatDAO.java

The AjaxChatDAO is the largest single class in AjaxChat, and it is truly where the majority of the server-side logic of the application resides. Listing 2 shows the entire code for this class.

Listing 2. AjaxChatDAO Class

 

package com.apress.ajaxprojects.ajaxchat.dao;

import java.io.InputStream; import java.io.IOException; import java.util.Date; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Vector; import org.apache.commons.digester.Digester; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import com.apress.ajaxprojects.ajaxchat.AjaxChatConfig; import com.apress.ajaxprojects.ajaxchat.dto.MessageDTO; import com.apress.ajaxprojects.ajaxchat.dto.RoomDTO; import com.apress.ajaxprojects.ajaxchat.dto.UserDTO; import org.xml.sax.SAXException;

/** * This Data Access Object (DAO) is really the heart and soul of the app. All * the real work is done here in terms of recording messages, dealing with * users and rooms and most everything else. It's probably a bit more * than what a DAO is supposed to generally be, but in this case I don't think * it's a big deal. Besides, the idea is that if you want to make this a more * robust application, with real message persistence and such, then all you * should probably have to mess with is this class. That's the intent anyway. * * @author <a href="mailto:fzammetti@omnytex.com">Frank W. Zammetti</a>. */ public final class AjaxChatDAO {

/** * Log instance. */ private static Log log = LogFactory.getLog(AjaxChatDAO.class);

/** * This class is a singleton, so here's the one and only instance. */ private static AjaxChatDAO instance;

/** * Collection of RoomDTO objects. */ private LinkedHashMap rooms = new LinkedHashMap();

/** * Collection of UserDTO objects of currently logged in users. */ private Vector users = new Vector();

/** * Make sure instances of this class can't be created. */ private AjaxChatDAO() { } // End constructor.

/** * Complete the singleton pattern. This method is the only way to get an *instance of this class. * * @return The one and only instance of this class. */ public static AjaxChatDAO getInstance() {

if (log.isDebugEnabled()) { log.debug("getInstance()..."); } if (instance == null) { instance = new AjaxChatDAO(); instance.init(null); } return instance;

} // End getInstance().

/** * Initialize. Read in room-list.xml file and create RoomDTOs for each * and add it to the collection of rooms. Note that the first time * getInstance() is called, we pass in null for the isConfigFile parameter, * and hence the config file is not read. Before this DAO can really be * used, init() must be called, handing it an InputStream to the config * file. This is done from ContextListener. * * @param isConfigFile InputStream to the config file. */ public synchronized void init(InputStream isConfigFile) {

if (log.isDebugEnabled()) { log.debug("init()..."); } if (isConfigFile != null) { // Read in rooms config and create beans, hand off to DAO. Digester digester = new Digester(); digester.setValidating(false); digester.push(this); digester.addObjectCreate("rooms/room","com.apress.ajaxprojects.ajaxchat.dto.RoomDTO"); digester.addSetProperties("rooms/room"); digester.addSetNext("rooms/room", "addRoom"); try { digester.parse(isConfigFile); log.info("***** Rooms = " + rooms); } catch (IOException ioe) { ioe.printStackTrace(); } catch (SAXException se) { se.printStackTrace(); } }

} // End init().

/** * Adds a room to the collection of rooms. * * @param inRoom The room to add. */ public synchronized void addRoom(RoomDTO inRoom) {

if (log.isDebugEnabled()) { log.debug("addRoom()..."); } if (log.isDebugEnabled()) { log.debug("Adding room " + inRoom); } rooms.put(inRoom.getName(), inRoom);

} // End addRoom().

/** * Removes a room from the collection of rooms. * * @param inRoomName The namr of the room to remove. */ public synchronized void removeRoom(String inRoomName) {

if (log.isDebugEnabled()) { log.debug("removeRoom()..."); } RoomDTO room = (RoomDTO)rooms.get(inRoomName); if (room.getUserList().size() == 0) { rooms.remove(inRoomName); if (log.isDebugEnabled()) { log.debug("removeRoom() removed room " + inRoomName); } } else { if (log.isDebugEnabled()) { log.debug("removeRoom() Room not removed because " + "there are users in it"); } }

} // End removeRoom().

/** * Add a message to the list of messages in the named room. * * @param inRoom The name of the room to post the message to. * @param inMessage The message to post. */ public synchronized void postMessage(String inRoom, MessageDTO inMessage) {

if (log.isDebugEnabled()) { log.debug("postMessage(): inRoom = " + inRoom +" - inMessage = " + inMessage + "..."); } RoomDTO room = (RoomDTO)rooms.get(inRoom); room.postMessage(inMessage);

} // End postMessage().

/** * Gets all messages in a named room newer than the specified datetime. * * @param inRoom The name of the room to get messages for. * @param inDateTime The date/time to start retrieval from. We'll actually * get any message subsequent to this datetime. * @return List of messages for the named room. */ public synchronized Vector getMessages(String inRoom, Date inDateTime) {

if (log.isDebugEnabled()) { log.debug("getMessages(): inRoom = " + inRoom +" - inDateTime = " + inDateTime + "..."); } RoomDTO room = (RoomDTO)rooms.get(inRoom); return room.getMessages(inDateTime); } // End getMessages().

/** * Returns a list of all rooms. Note that this returns the room name only, * it DOES NOT return a list of RoomDTOs. * * @return List of all rooms names. */ public synchronized Vector getRoomList() {

if (log.isDebugEnabled()) { log.debug("getRoomList()..."); } Vector roomList = new Vector(); for (Iterator it = rooms.keySet().iterator(); it.hasNext();) { roomList.add((String)it.next()); } if (log.isDebugEnabled()) { log.debug("roomList = " + roomList); } return roomList;

} // End getRoomList().

/** * Returns a Map of rooms, keyed by room name, with the number of users * chatting in each as the value. * * @return List of all rooms and user counts. */ public synchronized LinkedHashMap getRoomUserCounts() {

if (log.isDebugEnabled()) { log.debug("getRoomUserCounts()..."); } LinkedHashMap roomList = new LinkedHashMap(); for (Iterator it = rooms.keySet().iterator(); it.hasNext();) { String roomName = (String)it.next(); roomList.put(roomName, new Integer(((RoomDTO)rooms.get(roomName)).getUserList().size())); } if (log.isDebugEnabled()) { log.debug("roomList = " + roomList); } return roomList;

} // End getRoomUserCounts(). /** * Returns a list of all users currently chatting in a given room. Note that * this returns the username only, it DOES NOT return a list of UserDTOs. * * @param inRoom The name of the room to get the user list for. * @return List of all usernames chatting in a named room. */ public synchronized Vector getUserList(String inRoom) {

if (log.isDebugEnabled()) { log.debug("getUserList(): inRoom = " + inRoom + "..."); } Vector userList = null; RoomDTO room = (RoomDTO)rooms.get(inRoom); userList = room.getUserList(); if (log.isDebugEnabled()) { log.debug("userList = " + userList); } return userList;

} // End getUserList().

/** * Adds a user to the specified room. * * @param inRoom The room to add to. * @param inUser The user to add. */ public synchronized void addUserToRoom(String inRoom, UserDTO inUser) {

if (log.isDebugEnabled()) { log.debug("addUserToRoom()..."); } RoomDTO room = (RoomDTO)rooms.get(inRoom); room.addUser(inUser);

} // End addUserToRoom().

/** * Removes a user from the specified room. * * @param inRoom The room to add to. * @param inUser The user to remove. */ public synchronized void removeUserFromRoom(String inRoom, UserDTO inUser) {

if (log.isDebugEnabled()) { log.debug("removeUserFromRoom()..."); } RoomDTO room = (RoomDTO)rooms.get(inRoom); room.removeUser(inUser);

} // End removeUserFromRoom().

/** * Removes a user from all rooms. This is kind of a safety net when a * user's session is destroyed. * * @param inUser The user to remove. */ public synchronized void removeUserFromAllRooms(UserDTO inUser) {

if (log.isDebugEnabled()) { log.debug("removeUserFromAllRooms()..."); } for (Iterator it = rooms.keySet().iterator(); it.hasNext();) { String roomName = (String)it.next(); RoomDTO room = (RoomDTO)rooms.get(roomName); room.removeUser(inUser); }

} // End removeUserFromAllRooms().

/** * Adds a user to the list of logged on users. * * @param inUser The user to log in. */ public synchronized void logUserIn(UserDTO inUser) {

if (log.isDebugEnabled()) { log.debug("logUserIn()..."); } users.add(inUser); if (log.isDebugEnabled()) { log.debug(inUser.getUsername() + " logged in"); }

} // End logUserIn().

/** * Removes a user from the list of logged on users. * * @param inUser The user to log out. */ public synchronized void logUserOut(UserDTO inUser) {

if (log.isDebugEnabled()) { log.debug("logUserOut()..."); } String usernameToLogOut = inUser.getUsername(); int i = 0; int indexToRemove = -1; for (Iterator it = users.iterator(); it.hasNext();) { UserDTO user = (UserDTO)it.next(); if (usernameToLogOut.equalsIgnoreCase(user.getUsername())) { indexToRemove = i; break; } i++; } if (indexToRemove != -1) { users.remove(indexToRemove); if (log.isDebugEnabled()) { log.debug(usernameToLogOut + " logged out"); } }

} // End logUserIn().

/** * Checks to see if a given username is in use in any room. * * @param inUsername The name to check. * @return True if the name is in use, false if not. */ public synchronized boolean isUsernameInUse(String inUsername) {

if (log.isDebugEnabled()) { log.debug("isUsernameInUse()..."); } boolean retVal = false; for (Iterator it = users.iterator(); it.hasNext();) { UserDTO user = (UserDTO)it.next(); if (inUsername.equalsIgnoreCase(user.getUsername())) { retVal = true; } } if (log.isDebugEnabled()) { log.debug("Returning " + retVal); } return retVal;

} // End isUsernameInUse().

/** * This method goes through the collection of users and determines which, if * any, are inactive. Any that are inactive are removed. This is called * from the UserClearerDaemon thread. */ public synchronized void removeInactiveUsers() {

if (log.isDebugEnabled()) { log.debug("removeInactiveUsers()..."); } Vector usersToRemove = new Vector(); for (Iterator it = users.iterator(); it.hasNext();) { UserDTO user = (UserDTO)it.next(); long now = new Date().getTime(); long lastAJAXRequest = user.getLastAJAXRequest().getTime(); if ((now - lastAJAXRequest) >= (AjaxChatConfig.getUserInactivitySeconds() * 1000)) { if (log.isDebugEnabled()) { log.debug("User " + user.getUsername() + " will be removed"); } usersToRemove.add(user);

} } for (Iterator it = usersToRemove.iterator(); it.hasNext();) { UserDTO user = (UserDTO)it.next(); removeUserFromAllRooms(user); logUserOut(user); }

} // End removeInactiveUsers().

} // End class.

The first important note is that this is a Singleton class. This is done so that we have one unique collection of rooms and users to deal with.

Recall that in the ContextListener, we get a stream to the rooms-list.xml config file and pass it to the init() method of the AjaxChatDAO. Now we can see here that Commons Digester is used to parse that file and create from it a collection of RoomDTO objects for each room. Note that to do this, the DAO (data access object) itself is pushed onto the Digester stack so that the addRoom() method can be called for each <room> element encountered. AddRoom() is responsible for adding the RoomDTO to the rooms collection. Note that there is a corresponding removeRoom() method, although it is not used in this application. I wrote that so that I could create an admin screen later for maintaining the room list on the fly, but that does not exist today (Feel free to add it!).

The postMessage() method is next, and it is just a pass-through to the postMessage() method of the RoomDTO that the message is bound for, and we have already examined that. The same is true of getMessages() as well as addUserToRoom() and removeUserFromRoom(). Therefore, I will not go into these methods since we have already touched on them in examining the DTOs.

The next method in the DAO is getRoomList(), which returns a Vector of strings, where each string is the room name.

After that is getRoomUserCounts(). This simply iterates over the list of rooms and for each, gets the list of users chatting in it and gets the size of that collection. It wraps this in an Integer and puts it into a LinkedHashMap where the key names are the room names. This is done so that the order of the rooms is maintained yet we can get the values for an individual room by name.

The getUserList() method that follows returns the list of users for a given room. Note that like the getRoomList() method, this just returns a Vector of strings where each element is the username. It does not return a collection of UserDTOs.

The removeUserFromAllRooms() method is called when the user logs out and is just a safety net to be sure the user gets cleared out of all rooms. This should always be a redundant thing to do, but better safe than sorry. To do this, the method simply iterates over the rooms collection and calls removeUser() from each.

After that is the logUserId() method, which simply adds the user to the users collection. That is all “logging in” means in this context. The corresponding logUserOut() method removes the user from the collection, but note that it has to do a little more work to accomplish this because trying to modify a collection that you are iterating over causes an exception. Since we have a Vector as the collection, we in fact do have to iterate over it to find the user to remove. So we first locate the element in the Vector to remove, and after the iteration loop is done, only then do we remove the element.

The isUsernameInUse() method is used during login to ensure that the user entered a unique username. It simply iterates over the users collection and looks for a match (ignoring case). It returns true if the username is found, and returns false otherwise.

The last method in the DAO is removeInactiveUsers(). This method is called from the UserClearerDaemonThread. It iterates over the users collection and for each item it checks the lastAjaxRequest attribute. If the difference between the current time and the last request is greater than our inactivity threshold, the user is marked for removal. After the iteration loop completes, we then iterate over the collection of users to remove what was created during the first iteration, and for each user, we call the removeUserFromAllRooms() method and then the logUserOut() method.

You may have noticed that every method in AjaxChatDAO is synchronized. The purpose is to maintain thread safety when modifying the rooms and users collections, as well as, in effect, the underlying RoomDTOs and UserDTOs. This approach limits scalability as all incoming requests are in effect serialized. However, without a more robust mechanism—a relational database perhaps—such synchronization is pretty well unavoidable. It may be possible to create a more robust memory-only DAO that avoids at least some of the synchronization, but the overhead of synchronization involved is no longer as drastic as it was in prior JVMs, and as such, it is not quite as big a concern as it once was. That being said, there is no denying the scalability of AjaxChat is inherently limited using this DAO.

SessionCheckerFilter.java

AjaxChat makes use of a single servlet filter named SessionCheckerFilter. You can think of this as the “security” of AjaxChat. When fired, it first looks at the incoming request path. Because a session will not be established for a user until they log in, we must not process any paths that occur before that point. That means any requests for index.jsp, styles.css, any of the image files (assuming they are all GIFs), the login action mapping, and the logout action mapping will not be checked (in essence, the filter will ignore this request and just let it pass). Any other path will be checked, though. When one of these other paths is requested, we grab the session object and then look for the attribute defined by the LOGGED_IN_FLAG constant. If it is present, the user has been logged in and the request can continue. If it is not present, then the user was not logged in, and the filter redirects to the index.jsp page. It also puts a Struts ActionMessage in the request, which will be displayed to the user.

ContextListener.java

The final class to examine in AjaxChat is the ContextListener class. This listener performs two important functions. First, it gets a stream on the rooms-config.xml file and passes it along to the AjaxChatDAO to be read. The DAO will read the file and create RoomDTO objects for each configured room. The other important task of this listener is to kick off our UserClearerDaemonThread. Note, as previously mentioned, that it is started as a daemon thread to allow for proper container shutdown. Also note that the priority of the thread is bumped down as low as possible. This thread, while important, is not time-critical, so the lower priority is acceptable.

 // Lastly, start a background daemon thread that will periodically clear
// out inactive users from rooms. This was originally done via
// SessionListener, but because of some problems seem in some container
// implementations (Resin, I'm looking at you!), this had to be done
// instead.
Thread userClearerDaemonThread = new UserClearerDaemonThread();
userClearerDaemonThread.setPriority(Thread.MIN_PRIORITY);
userClearerDaemonThread.setDaemon(true);
userClearerDaemonThread.start(); 

Whew, what a ride! That was quite a bit of code to go through, but I hope you will agree that it was not anything incredibly complex. More important, I hope it demonstrates how AJAX makes this app possible and better than could be done without it (And without applets, ActiveX, or similar technologies… it is still straight HTML and JavaScript after all!).

Frank W. Zammetti is a Web architect specialist for a leading worldwide financial company by day, and a PocketPC and open source developer by night. He is the founder and chief software architect of Omnytex Technologies, a PocketPC development house.